From d07d4f7b53924eeca803585e36006123fdd06452 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 16:52:21 -0400 Subject: [PATCH 001/289] Initial P3589R2 implementation --- clang/include/clang/Basic/Attr.td | 39 +++ clang/include/clang/Basic/AttrDocs.td | 40 +++ .../include/clang/Basic/AttributeCommonInfo.h | 2 +- .../clang/Basic/DiagnosticParseKinds.td | 12 + .../clang/Basic/DiagnosticSemaKinds.td | 18 ++ clang/include/clang/Basic/LangOptions.def | 1 + clang/include/clang/Basic/Module.h | 3 + clang/include/clang/Options/Options.td | 3 + clang/include/clang/Parse/Parser.h | 13 + clang/include/clang/Sema/Sema.h | 45 ++- .../include/clang/Serialization/ASTBitCodes.h | 3 + clang/lib/Basic/Attributes.cpp | 3 +- clang/lib/Parse/ParseDeclCXX.cpp | 289 ++++++++++++++++++ clang/lib/Parse/ParseStmt.cpp | 3 + clang/lib/Parse/Parser.cpp | 15 +- clang/lib/Sema/Sema.cpp | 94 ++++++ clang/lib/Sema/SemaCast.cpp | 2 + clang/lib/Sema/SemaDeclAttr.cpp | 101 ++++++ clang/lib/Sema/SemaModule.cpp | 81 ++++- clang/lib/Sema/SemaStmtAttr.cpp | 30 ++ clang/lib/Serialization/ASTReader.cpp | 12 + clang/lib/Serialization/ASTWriter.cpp | 11 + clang/test/Parser/cxx-profiles-framework.cpp | 37 +++ .../test/SemaCXX/safety-profile-framework.cpp | 34 +++ .../test/SemaCXX/safety-profile-type-cast.cpp | 27 ++ 25 files changed, 903 insertions(+), 15 deletions(-) create mode 100644 clang/test/Parser/cxx-profiles-framework.cpp create mode 100644 clang/test/SemaCXX/safety-profile-framework.cpp create mode 100644 clang/test/SemaCXX/safety-profile-type-cast.cpp diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 8ab4aaa6f5781..5b3b56f2d2975 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -441,6 +441,8 @@ def ObjCNonFragileRuntime def HLSL : LangOpt<"HLSL">; +def Profiles : LangOpt<"Profiles">; + // Language option for CMSE extensions def Cmse : LangOpt<"Cmse">; @@ -3454,6 +3456,43 @@ def Suppress : DeclOrStmtAttr { let Documentation = [SuppressDocs]; } +// C++ Profiles framework (P3589R2) + +def ProfilesEnforce : Attr { + let Spellings = [CXX11<"profiles", "enforce">]; + let Args = [ + VariadicStringArgument<"ProfileNames">, + VariadicStringArgument<"ProfileDesignators"> + ]; + let LangOpts = [Profiles]; + let HasCustomParsing = 1; + let Documentation = [ProfilesEnforceDocs]; +} + +def ProfilesSuppress : DeclOrStmtAttr { + let Spellings = [CXX11<"profiles", "suppress">]; + let Args = [ + StringArgument<"ProfileName">, + StringArgument<"Justification", 1>, + StringArgument<"Rule", 1>, + VariadicStringArgument<"RawArguments"> + ]; + let LangOpts = [Profiles]; + let HasCustomParsing = 1; + let Subjects = SubjectList<[ + Stmt, Var, Field, Function, Record, Namespace, Empty + ], ErrorDiag, "variables, functions, structs, and namespaces">; + let Documentation = [ProfilesSuppressDocs]; +} + +def ProfilesRequire : Attr { + let Spellings = [CXX11<"profiles", "require">]; + let Args = [StringArgument<"Designator">]; + let LangOpts = [Profiles]; + let HasCustomParsing = 1; + let Documentation = [ProfilesRequireDocs]; +} + def SysVABI : DeclOrTypeAttr { let Spellings = [GCC<"sysv_abi">]; // let Subjects = [Function, ObjCMethod]; diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 43f827b4c60ee..b2d2a8b5cf90e 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -6445,6 +6445,46 @@ the ``int`` parameter is the one that represents the error. }]; } +def ProfilesEnforceDocs : Documentation { + let Category = DocCatStmt; + let Content = [{ +The ``[[profiles::enforce]]`` attribute requests enforcement of one or more +C++ profiles (P3589R2) for the remainder of the translation unit. It must +appear on an empty-declaration or module-declaration before any non-empty +declaration. + +.. code-block:: c++ + + [[profiles::enforce(std::type)]]; + void f(); // subject to std::type profile rules + }]; +} + +def ProfilesSuppressDocs : Documentation { + let Category = DocCatStmt; + let Content = [{ +The ``[[profiles::suppress]]`` attribute locally suppresses enforcement of +a C++ profile (P3589R2) for the appertaining declaration or statement. + +.. code-block:: c++ + + [[profiles::suppress(std::type, justification: "legacy code")]] + void legacy_function(); + }]; +} + +def ProfilesRequireDocs : Documentation { + let Category = DocCatStmt; + let Content = [{ +The ``[[profiles::require]]`` attribute on a module-import-declaration +validates that the imported module enforces the specified profile (P3589R2). + +.. code-block:: c++ + + import MyModule [[profiles::require(std::type)]]; + }]; +} + def SuppressDocs : Documentation { let Category = DocCatStmt; let Content = [{ diff --git a/clang/include/clang/Basic/AttributeCommonInfo.h b/clang/include/clang/Basic/AttributeCommonInfo.h index 77b5eb8a1a7cc..2c2004683224f 100644 --- a/clang/include/clang/Basic/AttributeCommonInfo.h +++ b/clang/include/clang/Basic/AttributeCommonInfo.h @@ -72,7 +72,7 @@ class AttributeCommonInfo { IgnoredAttribute, UnknownAttribute, }; - enum class Scope { NONE, CLANG, GNU, MSVC, OMP, HLSL, VK, GSL, RISCV }; + enum class Scope { NONE, CLANG, GNU, MSVC, OMP, HLSL, VK, GSL, RISCV, PROFILES }; enum class AttrArgsInfo { None, Optional, diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index de10dbe5d0628..2820bf45d2a2d 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1890,4 +1890,16 @@ def err_hlsl_number_literal_underflow : Error< "float literal has a magnitude that is too small to be represented as a float type">; def err_hlsl_rootsig_non_zero_flag : Error<"flag value is neither a literal 0 nor a named value">; +// C++ Profiles framework (P3589R2) +def err_profiles_expected_profile_name : Error< + "expected profile name">; +def err_profiles_expected_lparen : Error< + "expected '(' in '%0' attribute">; +def err_profiles_expected_rparen : Error< + "expected ')' in '%0' attribute">; +def err_profiles_expected_argument : Error< + "expected argument after ':' in profile argument">; +def err_profiles_invalid_argument_token : Error< + "invalid token in profile argument">; + } // end of Parser diagnostics diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 0c25eb2443d5e..5085c92464e28 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14115,4 +14115,22 @@ def err_cuda_device_kernel_launch_not_supported def err_cuda_device_kernel_launch_require_rdc : Error<"kernel launch from __device__ or __global__ function requires " "relocatable device code (i.e. requires -fgpu-rdc)">; +// C++ Profiles framework (P3589R2) +def err_profiles_enforce_not_empty_decl : Error< + "'profiles::enforce' attribute only allowed on empty-declarations and module-declarations">; +def err_profiles_enforce_not_at_tu_scope : Error< + "'profiles::enforce' attribute on empty-declaration must be at translation unit scope">; +def err_profiles_enforce_after_decl : Error< + "'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations">; +def err_profiles_enforce_mismatch : Error< + "repeated enforcement of profile '%0' with different designator">; +def err_profiles_require_not_on_import : Error< + "'profiles::require' attribute only allowed on module-import-declarations">; +def err_profiles_require_not_enforced : Error< + "required profile '%0' is not enforced by imported module">; +def err_profiles_suppress_justification_not_string : Error< + "'justification' argument of 'profiles::suppress' must be a string literal">; +def warn_profile_type_cast_reinterpret : Warning< + "'reinterpret_cast' is unsafe under profile '%0'">, + InGroup>; } // end of sema component. diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def index 2c93e60b48cc5..6e15a22ea272a 100644 --- a/clang/include/clang/Basic/LangOptions.def +++ b/clang/include/clang/Basic/LangOptions.def @@ -154,6 +154,7 @@ LANGOPT(EmitAllDecls , 1, 0, Benign, "emitting all declarations") LANGOPT(MathErrno , 1, 1, NotCompatible, "errno in math functions") LANGOPT(Modules , 1, 0, NotCompatible, "modules semantics") LANGOPT(CPlusPlusModules , 1, 0, Compatible, "C++ modules syntax") +LANGOPT(Profiles , 1, 0, NotCompatible, "C++ profiles framework") LANGOPT(SkipODRCheckInGMF , 1, 0, NotCompatible, "Skip ODR checks for decls in the global module fragment") LANGOPT(BuiltinHeadersInSystemModules, 1, 0, NotCompatible, "builtin headers belong to system modules, and _Builtin_ modules are ignored for cstdlib headers") ENUM_LANGOPT(CompilingModule, CompilingModuleKind, 3, CMK_None, Benign, diff --git a/clang/include/clang/Basic/Module.h b/clang/include/clang/Basic/Module.h index 69a1de6f79b35..35b06ea59d889 100644 --- a/clang/include/clang/Basic/Module.h +++ b/clang/include/clang/Basic/Module.h @@ -457,6 +457,9 @@ class alignas(8) Module { /// module depends. llvm::SmallSetVector Imports; + /// Profile designators enforced on this module's declaration (P3589R2). + SmallVector EnforcedProfileDesignators; + /// The set of top-level modules that affected the compilation of this module, /// but were not imported. llvm::SmallSetVector AffectingClangModules; diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index 1d89d983fe79a..3ebff8eb1d887 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -8835,6 +8835,9 @@ def split_dwarf_file : Separate<["-"], "split-dwarf-file">, let Visibility = [CC1Option] in { +def fprofiles : Flag<["-"], "fprofiles">, + HelpText<"Enable C++ profiles framework (P3589R2)">, + MarshallingInfoFlag>; def fblocks_runtime_optional : Flag<["-"], "fblocks-runtime-optional">, HelpText<"Weakly link in the blocks runtime">, MarshallingInfoFlag>; diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index 5ae02e2b4e8ad..ffc806197e262 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2274,6 +2274,19 @@ class Parser : public CodeCompletionHandler { IdentifierInfo *ScopeName, SourceLocation ScopeLoc, ParsedAttr::Form Form); + bool ParseProfilesAttributeArgs(IdentifierInfo *AttrName, + SourceLocation AttrNameLoc, + ParsedAttributes &Attrs, + SourceLocation *EndLoc, + IdentifierInfo *ScopeName, + SourceLocation ScopeLoc); + bool ParseProfileName(std::string &Name); + bool ParseProfileDesignator(std::string &Name, std::string &Designator); + bool ParseProfileDesignatorList(SmallVectorImpl &Names, + SmallVectorImpl &Designators); + bool ParseProfileArgumentList(SmallVectorImpl &Args); + bool ParseNonCommaBalancedToken(std::string &Spelling); + void MaybeParseCXX11Attributes(Declarator &D) { if (isAllowedCXX11AttributeSpecifier()) { ParsedAttributes Attrs(AttrFactory); diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 832e46286194a..8b62c85f72f39 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1034,6 +1034,45 @@ class Sema final : public SemaBase { void ActOnEndOfTranslationUnit(); void ActOnEndOfTranslationUnitFragment(TUFragmentKind Kind); + // C++ Profiles framework (P3589R2) + + struct ProfileEnforcement { + std::string ProfileName; + std::string CanonicalDesignator; + SourceLocation EnforceLoc; + }; + SmallVector EnforcedProfiles; + + struct ProfileSuppressEntry { + std::string ProfileName; + std::string RuleName; + }; + SmallVector ProfileSuppressStack; + + bool isProfileEnforced(StringRef ProfileName) const; + const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; + + void pushProfileSuppression(StringRef ProfileName, StringRef RuleName); + void popProfileSuppressions(unsigned Count); + + bool isProfileSuppressedByStmt(StringRef ProfileName, + StringRef RuleName = "") const; + bool isProfileSuppressedByDeclAttr(StringRef ProfileName, + StringRef RuleName = "") const; + bool isProfileSuppressed(StringRef ProfileName, + StringRef RuleName = "") const; + bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, unsigned DiagID); + + class ProfileSuppressRAII { + Sema &S; + unsigned Count; + + public: + ProfileSuppressRAII(Sema &S, const ParsedAttributesView &Attrs); + ~ProfileSuppressRAII(); + }; + /// Determines the active Scope associated with the given declaration /// context. /// @@ -9953,7 +9992,8 @@ class Sema final : public SemaBase { SourceLocation ModuleLoc, ModuleDeclKind MDK, ModuleIdPath Path, ModuleIdPath Partition, ModuleImportState &ImportState, - bool SeenNoTrivialPPDirective); + bool SeenNoTrivialPPDirective, + const ParsedAttributesView &Attrs = {}); /// The parser has processed a global-module-fragment declaration that begins /// the definition of the global module fragment of the current module unit. @@ -9984,6 +10024,9 @@ class Sema final : public SemaBase { SourceLocation ImportLoc, Module *M, ModuleIdPath Path = {}); + void ActOnModuleImportAttrs(Decl *ImportDecl, + const ParsedAttributesView &Attrs); + /// The parser has processed a module import translated from a /// #include or similar preprocessing directive. void ActOnAnnotModuleInclude(SourceLocation DirectiveLoc, Module *Mod); diff --git a/clang/include/clang/Serialization/ASTBitCodes.h b/clang/include/clang/Serialization/ASTBitCodes.h index 5db0b08f877ce..30321ebcf4b37 100644 --- a/clang/include/clang/Serialization/ASTBitCodes.h +++ b/clang/include/clang/Serialization/ASTBitCodes.h @@ -881,6 +881,9 @@ enum SubmoduleRecordTypes { /// Specifies affecting modules that were not imported. SUBMODULE_AFFECTING_MODULES = 18, + + /// Specifies enforced profile designators (P3589R2). + SUBMODULE_ENFORCED_PROFILES = 19, }; /// Record types used within a comments block. diff --git a/clang/lib/Basic/Attributes.cpp b/clang/lib/Basic/Attributes.cpp index 5878a4e3f83a4..af5c42ab5c492 100644 --- a/clang/lib/Basic/Attributes.cpp +++ b/clang/lib/Basic/Attributes.cpp @@ -236,7 +236,8 @@ getScopeFromNormalizedScopeName(StringRef ScopeName) { .Case("vk", AttributeCommonInfo::Scope::VK) .Case("msvc", AttributeCommonInfo::Scope::MSVC) .Case("omp", AttributeCommonInfo::Scope::OMP) - .Case("riscv", AttributeCommonInfo::Scope::RISCV); + .Case("riscv", AttributeCommonInfo::Scope::RISCV) + .Case("profiles", AttributeCommonInfo::Scope::PROFILES); } unsigned AttributeCommonInfo::calculateAttributeSpellingListIndex() const { diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 274c354d59808..c7fd051b4e714 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -12,6 +12,7 @@ #include "clang/AST/ASTContext.h" #include "clang/AST/DeclTemplate.h" +#include "clang/AST/Expr.h" #include "clang/AST/PrettyDeclStackTrace.h" #include "clang/Basic/AttributeCommonInfo.h" #include "clang/Basic/Attributes.h" @@ -4493,6 +4494,12 @@ bool Parser::ParseCXX11AttributeArgs( return true; } + if (ScopeName && ScopeName->isStr("profiles")) { + if (ParseProfilesAttributeArgs(AttrName, AttrNameLoc, Attrs, EndLoc, + ScopeName, ScopeLoc)) + return true; + } + unsigned NumArgs; // Some Clang-scoped attributes have some special parsing behavior. if (ScopeName && (ScopeName->isStr("clang") || ScopeName->isStr("_Clang"))) @@ -5024,3 +5031,285 @@ void Parser::ParseMicrosoftIfExistsClassDeclaration( Braces.consumeClose(); } + +//===----------------------------------------------------------------------===// +// C++ Profiles framework (P3589R2) +//===----------------------------------------------------------------------===// + +static Expr *MakeProfileStringLiteral(Parser &P, StringRef Str, + SourceLocation Loc) { + ASTContext &Ctx = P.getActions().Context; + QualType StrTy = Ctx.getStringLiteralArrayType(Ctx.CharTy, Str.size()); + return StringLiteral::Create(Ctx, Str, StringLiteralKind::Ordinary, false, + StrTy, {Loc}); +} + +bool Parser::ParseProfileName(std::string &Name) { + if (!Tok.is(tok::identifier)) { + Diag(Tok, diag::err_profiles_expected_profile_name); + return true; + } + Name = Tok.getIdentifierInfo()->getName().str(); + ConsumeToken(); + + while (Tok.is(tok::coloncolon)) { + ConsumeToken(); + if (!Tok.is(tok::identifier)) { + Diag(Tok, diag::err_profiles_expected_profile_name); + return true; + } + Name += "::"; + Name += Tok.getIdentifierInfo()->getName(); + ConsumeToken(); + } + return false; +} + +bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { + if (Tok.isOneOf(tok::l_paren, tok::l_square, tok::l_brace)) { + tok::TokenKind Open = Tok.getKind(); + tok::TokenKind Close = Open == tok::l_paren ? tok::r_paren + : Open == tok::l_square ? tok::r_square + : tok::r_brace; + Spelling = PP.getSpelling(Tok); + ConsumeAnyToken(); + unsigned Depth = 1; + while (Depth > 0 && !Tok.is(tok::eof)) { + if (Tok.is(Open)) + ++Depth; + else if (Tok.is(Close)) + --Depth; + if (Depth > 0) { + Spelling += " "; + Spelling += PP.getSpelling(Tok); + ConsumeAnyToken(); + } + } + if (Tok.is(Close)) { + Spelling += " "; + Spelling += PP.getSpelling(Tok); + ConsumeAnyToken(); + } + return false; + } + + if (Tok.isOneOf(tok::comma, tok::r_paren, tok::r_square, tok::r_brace, + tok::eof)) { + Diag(Tok, diag::err_profiles_invalid_argument_token); + return true; + } + + Spelling = PP.getSpelling(Tok); + ConsumeAnyToken(); + return false; +} + +bool Parser::ParseProfileArgumentList(SmallVectorImpl &Args) { + while (true) { + if (Tok.is(tok::identifier) && NextToken().is(tok::colon)) { + std::string Key = Tok.getIdentifierInfo()->getName().str(); + ConsumeToken(); + ConsumeToken(); + + std::string Value; + if (ParseNonCommaBalancedToken(Value)) + return true; + Args.push_back(Key + " : " + Value); + } else { + std::string Spelling; + if (ParseNonCommaBalancedToken(Spelling)) + return true; + Args.push_back(std::move(Spelling)); + } + + if (!TryConsumeToken(tok::comma)) + break; + } + return false; +} + +bool Parser::ParseProfileDesignator(std::string &Name, + std::string &Designator) { + if (ParseProfileName(Name)) + return true; + + Designator = Name; + + if (!Tok.is(tok::l_paren)) + return false; + + Designator += "("; + ConsumeParen(); + + SmallVector Args; + if (ParseProfileArgumentList(Args)) + return true; + + for (unsigned I = 0; I < Args.size(); ++I) { + if (I > 0) + Designator += ", "; + Designator += Args[I]; + } + + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_profiles_expected_rparen) << "enforce"; + return true; + } + Designator += ")"; + ConsumeParen(); + return false; +} + +bool Parser::ParseProfileDesignatorList( + SmallVectorImpl &Names, + SmallVectorImpl &Designators) { + while (true) { + std::string Name, Designator; + if (ParseProfileDesignator(Name, Designator)) + return true; + Names.push_back(std::move(Name)); + Designators.push_back(std::move(Designator)); + + if (!TryConsumeToken(tok::comma)) + break; + } + return false; +} + +bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, + SourceLocation AttrNameLoc, + ParsedAttributes &Attrs, + SourceLocation *EndLoc, + IdentifierInfo *ScopeName, + SourceLocation ScopeLoc) { + assert(ScopeName && ScopeName->isStr("profiles")); + + auto SkipToRParen = [&]() { + SkipUntil(tok::r_paren, StopAtSemi | StopBeforeMatch); + if (Tok.is(tok::r_paren)) + ConsumeParen(); + }; + + if (AttrName->isStr("enforce")) { + if (!Tok.is(tok::l_paren)) { + Diag(Tok, diag::err_profiles_expected_lparen) << "enforce"; + return true; + } + ConsumeParen(); + + SmallVector Names, Designators; + if (ParseProfileDesignatorList(Names, Designators)) { + SkipToRParen(); + return true; + } + + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_profiles_expected_rparen) << "enforce"; + SkipToRParen(); + return true; + } + SourceLocation RParen = Tok.getLocation(); + ConsumeParen(); + if (EndLoc) + *EndLoc = RParen; + + SmallVector ArgExprs; + for (unsigned I = 0; I < Names.size(); ++I) { + ArgExprs.push_back(MakeProfileStringLiteral(*this, Names[I], AttrNameLoc)); + ArgExprs.push_back( + MakeProfileStringLiteral(*this, Designators[I], AttrNameLoc)); + } + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), ArgExprs.data(), + ArgExprs.size(), ParsedAttr::Form::CXX11()); + return true; + } + + if (AttrName->isStr("suppress")) { + if (!Tok.is(tok::l_paren)) { + Diag(Tok, diag::err_profiles_expected_lparen) << "suppress"; + return true; + } + ConsumeParen(); + + std::string ProfileName; + if (ParseProfileName(ProfileName)) { + SkipToRParen(); + return true; + } + + std::string Justification, Rule; + SmallVector RawArgs; + + if (TryConsumeToken(tok::comma)) { + if (ParseProfileArgumentList(RawArgs)) { + SkipToRParen(); + return true; + } + + for (const auto &Arg : RawArgs) { + StringRef ArgRef(Arg); + if (ArgRef.starts_with("justification : ")) + Justification = ArgRef.substr(strlen("justification : ")).str(); + else if (ArgRef.starts_with("rule : ")) + Rule = ArgRef.substr(strlen("rule : ")).str(); + } + } + + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_profiles_expected_rparen) << "suppress"; + SkipToRParen(); + return true; + } + SourceLocation RParen = Tok.getLocation(); + ConsumeParen(); + if (EndLoc) + *EndLoc = RParen; + + SmallVector ArgExprs; + ArgExprs.push_back( + MakeProfileStringLiteral(*this, ProfileName, AttrNameLoc)); + ArgExprs.push_back( + MakeProfileStringLiteral(*this, Justification, AttrNameLoc)); + ArgExprs.push_back(MakeProfileStringLiteral(*this, Rule, AttrNameLoc)); + for (const auto &Arg : RawArgs) + ArgExprs.push_back(MakeProfileStringLiteral(*this, Arg, AttrNameLoc)); + + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), ArgExprs.data(), + ArgExprs.size(), ParsedAttr::Form::CXX11()); + return true; + } + + if (AttrName->isStr("require")) { + if (!Tok.is(tok::l_paren)) { + Diag(Tok, diag::err_profiles_expected_lparen) << "require"; + return true; + } + ConsumeParen(); + + std::string Name, Designator; + if (ParseProfileDesignator(Name, Designator)) { + SkipToRParen(); + return true; + } + + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_profiles_expected_rparen) << "require"; + SkipToRParen(); + return true; + } + SourceLocation RParen = Tok.getLocation(); + ConsumeParen(); + if (EndLoc) + *EndLoc = RParen; + + ArgsUnion Arg = MakeProfileStringLiteral(*this, Designator, AttrNameLoc); + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), &Arg, 1, + ParsedAttr::Form::CXX11()); + return true; + } + + return false; +} diff --git a/clang/lib/Parse/ParseStmt.cpp b/clang/lib/Parse/ParseStmt.cpp index 1a45ed66950be..e148891813efe 100644 --- a/clang/lib/Parse/ParseStmt.cpp +++ b/clang/lib/Parse/ParseStmt.cpp @@ -20,6 +20,7 @@ #include "clang/Parse/Parser.h" #include "clang/Parse/RAIIObjectsForParser.h" #include "clang/Sema/DeclSpec.h" +#include "clang/Sema/Sema.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" #include "clang/Sema/SemaCodeCompletion.h" @@ -75,6 +76,8 @@ StmtResult Parser::ParseStatementOrDeclaration(StmtVector &Stmts, if (getLangOpts().HLSL) MaybeParseMicrosoftAttributes(GNUOrMSAttrs); + Sema::ProfileSuppressRAII ProfileSuppressGuard(Actions, CXX11Attrs); + StmtResult Res = ParseStatementOrDeclarationAfterAttributes( Stmts, StmtCtx, TrailingElseLoc, CXX11Attrs, GNUOrMSAttrs, PrecedingLabel); diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp index 5d18414b1a746..6eb4b464daebf 100644 --- a/clang/lib/Parse/Parser.cpp +++ b/clang/lib/Parse/Parser.cpp @@ -2378,13 +2378,8 @@ Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { if (!Tok.isOneOf(tok::semi, tok::l_square)) SkipUntil(tok::semi, SkipUntilFlags::StopBeforeMatch); - // We don't support any module attributes yet; just parse them and diagnose. ParsedAttributes Attrs(AttrFactory); MaybeParseCXX11Attributes(Attrs); - ProhibitCXX11Attributes(Attrs, diag::err_attribute_not_module_attr, - diag::err_keyword_not_module_attr, - /*DiagnoseEmptyAttrs=*/false, - /*WarnOnUnknownAttrs=*/true); if (ExpectAndConsumeSemi(diag::err_expected_semi_after_module_or_import, tok::getKeywordSpelling(tok::kw_module))) @@ -2392,7 +2387,8 @@ Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { return Actions.ActOnModuleDecl(StartLoc, ModuleLoc, MDK, Path, Partition, ImportState, - Introducer.hasSeenNoTrivialPPDirective()); + Introducer.hasSeenNoTrivialPPDirective(), + Attrs); } Decl *Parser::ParseModuleImport(SourceLocation AtLoc, @@ -2438,11 +2434,6 @@ Decl *Parser::ParseModuleImport(SourceLocation AtLoc, ParsedAttributes Attrs(AttrFactory); MaybeParseCXX11Attributes(Attrs); - // We don't support any module import attributes yet. - ProhibitCXX11Attributes(Attrs, diag::err_attribute_not_import_attr, - diag::err_keyword_not_import_attr, - /*DiagnoseEmptyAttrs=*/false, - /*WarnOnUnknownAttrs=*/true); if (PP.hadModuleLoaderFatalFailure()) { // With a fatal failure in the module loader, we abort parsing. @@ -2517,6 +2508,8 @@ Decl *Parser::ParseModuleImport(SourceLocation AtLoc, if (Import.isInvalid()) return nullptr; + Actions.ActOnModuleImportAttrs(Import.get(), Attrs); + // Using '@import' in framework headers requires modules to be enabled so that // the header is parseable. Emit a warning to make the user aware. if (IsObjCAtImport && AtLoc.isValid()) { diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 3065b5e1e66d3..98d7d25ec25b2 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -31,6 +31,7 @@ #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/HeaderSearchOptions.h" #include "clang/Lex/Preprocessor.h" +#include "clang/Sema/Attr.h" #include "clang/Sema/CXXFieldCollector.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/ExternalSemaSource.h" @@ -2978,3 +2979,96 @@ Attr *Sema::CreateAnnotationAttr(const ParsedAttr &AL) { return CreateAnnotationAttr(AL, Str, Args); } + +//===----------------------------------------------------------------------===// +// C++ Profiles framework (P3589R2) +//===----------------------------------------------------------------------===// + +bool Sema::isProfileEnforced(StringRef ProfileName) const { + return getProfileEnforcement(ProfileName) != nullptr; +} + +const Sema::ProfileEnforcement * +Sema::getProfileEnforcement(StringRef ProfileName) const { + for (const auto &E : EnforcedProfiles) + if (E.ProfileName == ProfileName) + return &E; + return nullptr; +} + +void Sema::pushProfileSuppression(StringRef ProfileName, StringRef RuleName) { + ProfileSuppressStack.push_back( + {ProfileName.str(), RuleName.str()}); +} + +void Sema::popProfileSuppressions(unsigned Count) { + assert(ProfileSuppressStack.size() >= Count); + ProfileSuppressStack.pop_back_n(Count); +} + +bool Sema::isProfileSuppressedByStmt(StringRef ProfileName, + StringRef RuleName) const { + for (const auto &E : ProfileSuppressStack) { + if (E.ProfileName != ProfileName) + continue; + if (E.RuleName.empty() || E.RuleName == RuleName) + return true; + } + return false; +} + +bool Sema::isProfileSuppressedByDeclAttr(StringRef ProfileName, + StringRef RuleName) const { + for (const DeclContext *DC = CurContext; DC; DC = DC->getParent()) { + if (const auto *D = dyn_cast(DC)) { + for (const auto *A : D->specific_attrs()) { + if (A->getProfileName() != ProfileName) + continue; + if (A->getRule().empty() || A->getRule() == RuleName) + return true; + } + } + } + return false; +} + +bool Sema::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName) const { + return isProfileSuppressedByStmt(ProfileName, RuleName) || + isProfileSuppressedByDeclAttr(ProfileName, RuleName); +} + +bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, unsigned DiagID) { + if (!isProfileEnforced(ProfileName)) + return false; + if (isProfileSuppressed(ProfileName, RuleName)) + return false; + Diag(Loc, DiagID) << ProfileName; + return true; +} + +Sema::ProfileSuppressRAII::ProfileSuppressRAII( + Sema &S, const ParsedAttributesView &Attrs) + : S(S), Count(0) { + for (const auto &AL : Attrs) { + if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) + continue; + if (AL.getNumArgs() < 1) + continue; + if (const auto *SL = + dyn_cast(AL.getArgAsExpr(0))) { + StringRef Rule; + if (AL.getNumArgs() >= 3) + if (const auto *RuleSL = + dyn_cast(AL.getArgAsExpr(2))) + Rule = RuleSL->getString(); + S.pushProfileSuppression(SL->getString(), Rule); + ++Count; + } + } +} + +Sema::ProfileSuppressRAII::~ProfileSuppressRAII() { + S.popProfileSuppressions(Count); +} diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index 5360f8a2908bf..4aa26ab9aa0b5 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -401,6 +401,8 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); } + checkProfileViolation("test::type_cast", "", OpLoc, + diag::warn_profile_type_cast_reinterpret); return Op.complete(CXXReinterpretCastExpr::Create(Context, Op.ResultType, Op.ValueKind, Op.Kind, Op.SrcExpr.get(), nullptr, DestTInfo, OpLoc, diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 6fc749464586d..d3392d776dcd4 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5451,6 +5451,97 @@ static void handleSuppressAttr(Sema &S, Decl *D, const ParsedAttr &AL) { DiagnosticIdentifiers.size())); } +static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + if (!isa(D)) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_not_empty_decl); + return; + } + + if (!isa(D->getDeclContext())) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_not_at_tu_scope); + return; + } + + // P3589R2 [decl.attr.enforce]p1: enforce on empty-declaration shall precede + // any non-empty-declaration. + auto *TU = cast(D->getDeclContext()); + for (const auto *Prev : TU->decls()) { + if (Prev->isImplicit() || !Prev->getLocation().isValid()) + continue; + if (!isa(Prev)) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_after_decl); + S.Diag(Prev->getLocation(), diag::note_previous_decl) << "declaration"; + return; + } + } + + // Args are interleaved: name0, desig0, name1, desig1, ... + unsigned NumDesignators = AL.getNumArgs() / 2; + SmallVector Names, Designators; + for (unsigned I = 0; I < NumDesignators; ++I) { + StringRef Name, Desig; + if (!S.checkStringLiteralArgumentAttr(AL, I * 2, Name) || + !S.checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) + return; + + if (const auto *Existing = S.getProfileEnforcement(Name)) { + if (Existing->CanonicalDesignator != Desig) { + S.Diag(AL.getLoc(), diag::err_profiles_enforce_mismatch) << Name; + S.Diag(Existing->EnforceLoc, diag::note_previous_attribute); + return; + } + continue; + } + + S.EnforcedProfiles.push_back({Name.str(), Desig.str(), AL.getLoc()}); + Names.push_back(Name); + Designators.push_back(Desig); + } + + D->addAttr(::new (S.Context) + ProfilesEnforceAttr(S.Context, AL, Names.data(), Names.size(), + Designators.data(), Designators.size())); +} + +static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, + const ParsedAttr &AL) { + if (AL.getNumArgs() < 1) + return; + + StringRef ProfileName; + if (!S.checkStringLiteralArgumentAttr(AL, 0, ProfileName)) + return; + + StringRef Justification, Rule; + if (AL.getNumArgs() >= 2) + S.checkStringLiteralArgumentAttr(AL, 1, Justification); + if (AL.getNumArgs() >= 3) + S.checkStringLiteralArgumentAttr(AL, 2, Rule); + + // P3589R2 [decl.attr.suppress]p2: justification must be a string-literal. + if (!Justification.empty()) { + bool LooksLikeString = + Justification.starts_with("\"") || Justification.starts_with("L\"") || + Justification.starts_with("u\"") || Justification.starts_with("U\"") || + Justification.starts_with("u8\""); + if (!LooksLikeString) { + // The parser already extracted the value from the string literal token, + // so this validation is handled at parse time by checking the token kind. + } + } + + SmallVector RawArgs; + for (unsigned I = 3; I < AL.getNumArgs(); ++I) { + StringRef Arg; + if (S.checkStringLiteralArgumentAttr(AL, I, Arg)) + RawArgs.push_back(Arg); + } + + D->addAttr(::new (S.Context) ProfilesSuppressAttr( + S.Context, AL, ProfileName, Justification, Rule, RawArgs.data(), + RawArgs.size())); +} + static void handleLifetimeCategoryAttr(Sema &S, Decl *D, const ParsedAttr &AL) { TypeSourceInfo *DerefTypeLoc = nullptr; QualType ParmType; @@ -7881,6 +7972,16 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, case ParsedAttr::AT_Suppress: handleSuppressAttr(S, D, AL); break; + case ParsedAttr::AT_ProfilesEnforce: + handleProfilesEnforceAttr(S, D, AL); + break; + case ParsedAttr::AT_ProfilesSuppress: + handleProfilesSuppressDeclAttr(S, D, AL); + break; + case ParsedAttr::AT_ProfilesRequire: + // Require is handled in SemaModule.cpp on import-declarations. + S.Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); + break; case ParsedAttr::AT_Owner: case ParsedAttr::AT_Pointer: handleLifetimeCategoryAttr(S, D, AL); diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 8fc3464ed3e0c..0ff5b5d1ce726 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -249,7 +249,8 @@ Sema::DeclGroupPtrTy Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, ModuleDeclKind MDK, ModuleIdPath Path, ModuleIdPath Partition, ModuleImportState &ImportState, - bool SeenNoTrivialPPDirective) { + bool SeenNoTrivialPPDirective, + const ParsedAttributesView &Attrs) { assert(getLangOpts().CPlusPlusModules && "should only have module decl in standard C++ modules"); @@ -459,6 +460,36 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, TU->setModuleOwnershipKind(Decl::ModuleOwnershipKind::ReachableWhenImported); TU->setLocalOwningModule(Mod); + // Process [[profiles::enforce]] on the module-declaration. + for (const auto &AL : Attrs) { + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) { + unsigned NumDesignators = AL.getNumArgs() / 2; + for (unsigned I = 0; I < NumDesignators; ++I) { + StringRef Name, Desig; + if (!checkStringLiteralArgumentAttr(AL, I * 2, Name) || + !checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) + continue; + + if (const auto *Existing = getProfileEnforcement(Name)) { + if (Existing->CanonicalDesignator != Desig) { + Diag(AL.getLoc(), diag::err_profiles_enforce_mismatch) << Name; + Diag(Existing->EnforceLoc, diag::note_previous_attribute); + continue; + } + continue; + } + + EnforcedProfiles.push_back({Name.str(), Desig.str(), AL.getLoc()}); + + if (MDK == ModuleDeclKind::Interface) + Mod->EnforcedProfileDesignators.push_back(Desig.str()); + } + } else { + Diag(AL.getLoc(), diag::warn_unknown_attribute_ignored) + << AL.getAttrName(); + } + } + // We are in the module purview, but before any other (non import) // statements, so imports are allowed. ImportState = ModuleImportState::ImportAllowed; @@ -468,6 +499,18 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, if (auto *Listener = getASTMutationListener()) Listener->EnteringModulePurview(); + // P3589R2 [decl.attr.enforce]p4: propagate interface's enforced profiles to + // implementation unit. + if (Interface) { + for (const auto &Desig : Interface->EnforcedProfileDesignators) { + std::string Name = Desig; + if (auto Paren = Name.find('('); Paren != std::string::npos) + Name = Name.substr(0, Paren); + if (!isProfileEnforced(Name)) + EnforcedProfiles.push_back({Name, Desig, ModuleLoc}); + } + } + // We already potentially made an implicit import (in the case of a module // implementation unit importing its interface). Make this module visible // and return the import decl to be added to the current TU. @@ -1600,3 +1643,39 @@ void Sema::checkReferenceToTULocalFromOtherTU( PendingCheckReferenceForTULocal.push_back( std::make_pair(FD, PointOfInstantiation)); } + +void Sema::ActOnModuleImportAttrs(Decl *D, + const ParsedAttributesView &Attrs) { + if (!D) + return; + + auto *ID = dyn_cast(D); + Module *ImportedMod = ID ? ID->getImportedModule() : nullptr; + + for (const auto &AL : Attrs) { + if (AL.getKind() == ParsedAttr::AT_ProfilesRequire) { + if (!ImportedMod) { + Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); + continue; + } + + StringRef Desig; + if (!checkStringLiteralArgumentAttr(AL, 0, Desig)) + continue; + + bool Found = false; + for (const auto &Enforced : ImportedMod->EnforcedProfileDesignators) { + if (Enforced == Desig) { + Found = true; + break; + } + } + + if (!Found) + Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; + } else { + Diag(AL.getLoc(), diag::warn_unknown_attribute_ignored) + << AL.getAttrName(); + } + } +} diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp index 27fd5563cc40e..85f590b79aa58 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -71,6 +71,34 @@ static Attr *handleSuppressAttr(Sema &S, Stmt *St, const ParsedAttr &A, S.Context, A, DiagnosticIdentifiers.data(), DiagnosticIdentifiers.size()); } +static Attr *handleProfilesSuppressStmtAttr(Sema &S, Stmt *St, + const ParsedAttr &A, + SourceRange Range) { + if (A.getNumArgs() < 1) + return nullptr; + + StringRef ProfileName; + if (!S.checkStringLiteralArgumentAttr(A, 0, ProfileName)) + return nullptr; + + StringRef Justification, Rule; + if (A.getNumArgs() >= 2) + S.checkStringLiteralArgumentAttr(A, 1, Justification); + if (A.getNumArgs() >= 3) + S.checkStringLiteralArgumentAttr(A, 2, Rule); + + SmallVector RawArgs; + for (unsigned I = 3; I < A.getNumArgs(); ++I) { + StringRef Arg; + if (S.checkStringLiteralArgumentAttr(A, I, Arg)) + RawArgs.push_back(Arg); + } + + return ::new (S.Context) ProfilesSuppressAttr( + S.Context, A, ProfileName, Justification, Rule, RawArgs.data(), + RawArgs.size()); +} + static Attr *handleLoopHintAttr(Sema &S, Stmt *St, const ParsedAttr &A, SourceRange) { IdentifierLoc *PragmaNameLoc = A.getArgAsIdent(0); @@ -704,6 +732,8 @@ static Attr *ProcessStmtAttribute(Sema &S, Stmt *St, const ParsedAttr &A, return handleOpenCLUnrollHint(S, St, A, Range); case ParsedAttr::AT_Suppress: return handleSuppressAttr(S, St, A, Range); + case ParsedAttr::AT_ProfilesSuppress: + return handleProfilesSuppressStmtAttr(S, St, A, Range); case ParsedAttr::AT_NoMerge: return handleNoMergeAttr(S, St, A, Range); case ParsedAttr::AT_NoInline: diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 515eaf8d1caed..0a1ef371dc790 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -6568,6 +6568,18 @@ llvm::Error ASTReader::ReadSubmoduleBlock(ModuleFile &F, CurrentModule->ExportAsModule = Blob.str(); ModMap.addLinkAsDependency(CurrentModule); break; + + case SUBMODULE_ENFORCED_PROFILES: { + unsigned Idx = 0; + while (Idx < Record.size()) { + unsigned Len = Record[Idx++]; + std::string Desig(Record.begin() + Idx, + Record.begin() + Idx + Len); + Idx += Len; + CurrentModule->EnforcedProfileDesignators.push_back(std::move(Desig)); + } + break; + } } } } diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index e21a86b688dbf..2c31e161fc9ae 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -1015,6 +1015,7 @@ void ASTWriter::WriteBlockInfoBlock() { RECORD(SUBMODULE_PRIVATE_TEXTUAL_HEADER); RECORD(SUBMODULE_INITIALIZERS); RECORD(SUBMODULE_EXPORT_AS); + RECORD(SUBMODULE_ENFORCED_PROFILES); // Comments Block. BLOCK(COMMENTS_BLOCK); @@ -3255,6 +3256,16 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { Stream.EmitRecordWithBlob(ExportAsAbbrev, Record, Mod->ExportAsModule); } + // Emit enforced profile designators (P3589R2). + if (!Mod->EnforcedProfileDesignators.empty()) { + RecordData Record; + for (const auto &Desig : Mod->EnforcedProfileDesignators) { + Record.push_back(Desig.size()); + Record.append(Desig.begin(), Desig.end()); + } + Stream.EmitRecord(SUBMODULE_ENFORCED_PROFILES, Record); + } + // Queue up the submodules of this module. for (auto *M : Mod->submodules()) Q.push(M); diff --git a/clang/test/Parser/cxx-profiles-framework.cpp b/clang/test/Parser/cxx-profiles-framework.cpp new file mode 100644 index 0000000000000..ff67c5c519414 --- /dev/null +++ b/clang/test/Parser/cxx-profiles-framework.cpp @@ -0,0 +1,37 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++20 %s + +// =================================================================== +// Valid enforce forms +// =================================================================== + +[[profiles::enforce(std::type)]]; + +// =================================================================== +// Valid suppress forms +// =================================================================== + +[[profiles::suppress(std::type)]] +void suppress_no_args(); + +[[profiles::suppress(std::type, justification: "legacy")]] +void suppress_with_justification(); + +[[profiles::suppress(std::type, rule: "reinterpret_cast")]] +void suppress_with_rule(); + +[[profiles::suppress(std::type, justification: "legacy", rule: "cast")]] +void suppress_with_both(); + +// =================================================================== +// Parse errors +// =================================================================== + +// enforce with empty parens: parse error +[[profiles::enforce()]]; // expected-error {{expected profile name}} + +// =================================================================== +// Profile name with :: separators +// =================================================================== + +[[profiles::suppress(a::b::c)]] +void deep_name(); diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp new file mode 100644 index 0000000000000..14bbffdc69114 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -0,0 +1,34 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++20 %s + +// =================================================================== +// Enforce on empty-declaration at TU scope: OK +// =================================================================== +[[profiles::enforce(test::type_cast)]]; // #enforce1 + +// =================================================================== +// Enforce: exact repetition is OK +// =================================================================== +[[profiles::enforce(test::type_cast)]]; + +// =================================================================== +// Enforce: mismatch is an error +// =================================================================== +[[profiles::enforce(test::type_cast(strict: true))]]; // expected-error {{repeated enforcement of profile 'test::type_cast' with different designator}} \ + // expected-note@#enforce1 {{previous attribute is here}} + +// =================================================================== +// Suppress on declarations +// =================================================================== +[[profiles::suppress(test::type_cast)]] +int suppressed_var; + +[[profiles::suppress(test::type_cast)]] +void suppressed_func(); + +// =================================================================== +// Suppress on statements +// =================================================================== +void test_stmt_suppress() { + [[profiles::suppress(test::type_cast)]] int x = 0; + [[profiles::suppress(test::type_cast)]] { int y = 0; } +} diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp new file mode 100644 index 0000000000000..af620bea3d99e --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -0,0 +1,27 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++20 %s + +[[profiles::enforce(test::type_cast)]]; + +void test_violation() { + int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +[[profiles::suppress(test::type_cast)]] +void test_suppress_decl() { + int *p = reinterpret_cast(0); +} + +void test_suppress_stmt() { + [[profiles::suppress(test::type_cast)]] { + int *p = reinterpret_cast(0); + } + int *q = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +void test_suppress_stmt_with_rule() { + // Rule-specific suppress only suppresses matching rules; empty rule from the + // test profile check means this suppress does NOT match. + [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { + int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +} From 21f565e38ad2ce9ab2513b8fd2ca941a2e8ce7de Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 17:35:00 -0400 Subject: [PATCH 002/289] Expand tests --- .../clang/Basic/DiagnosticParseKinds.td | 2 - clang/lib/Parse/ParseDeclCXX.cpp | 17 ++++++-- clang/lib/Sema/SemaCast.cpp | 4 +- clang/lib/Sema/SemaDeclAttr.cpp | 19 ++++----- clang/lib/Sema/SemaModule.cpp | 14 ++++--- clang/test/Parser/cxx-profiles-framework.cpp | 32 +++++++++++++-- .../safety-profile-framework-modules.cppm | 36 +++++++++++++++++ .../test/SemaCXX/safety-profile-framework.cpp | 39 +++++++++++++++++++ .../test/SemaCXX/safety-profile-type-cast.cpp | 10 +++++ 9 files changed, 146 insertions(+), 27 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-framework-modules.cppm diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index 2820bf45d2a2d..d1e9551d3fd69 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1897,8 +1897,6 @@ def err_profiles_expected_lparen : Error< "expected '(' in '%0' attribute">; def err_profiles_expected_rparen : Error< "expected ')' in '%0' attribute">; -def err_profiles_expected_argument : Error< - "expected argument after ':' in profile argument">; def err_profiles_invalid_argument_token : Error< "invalid token in profile argument">; diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index c7fd051b4e714..9489000044afd 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5249,9 +5249,20 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, for (const auto &Arg : RawArgs) { StringRef ArgRef(Arg); - if (ArgRef.starts_with("justification : ")) - Justification = ArgRef.substr(strlen("justification : ")).str(); - else if (ArgRef.starts_with("rule : ")) + if (ArgRef.starts_with("justification : ")) { + StringRef JustVal = ArgRef.substr(strlen("justification : ")); + // P3589R2 [decl.attr.suppress]p2: justification must be a + // string-literal. Check the spelling recorded by the parser. + if (!JustVal.starts_with("\"") && !JustVal.starts_with("L\"") && + !JustVal.starts_with("u\"") && !JustVal.starts_with("U\"") && + !JustVal.starts_with("u8\"")) { + Diag(AttrNameLoc, + diag::err_profiles_suppress_justification_not_string); + SkipToRParen(); + return true; + } + Justification = JustVal.str(); + } else if (ArgRef.starts_with("rule : ")) Rule = ArgRef.substr(strlen("rule : ")).str(); } } diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index 4aa26ab9aa0b5..8312cdafc2d8c 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -400,9 +400,9 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, if (Op.SrcExpr.isInvalid()) return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); + checkProfileViolation("test::type_cast", "", OpLoc, + diag::warn_profile_type_cast_reinterpret); } - checkProfileViolation("test::type_cast", "", OpLoc, - diag::warn_profile_type_cast_reinterpret); return Op.complete(CXXReinterpretCastExpr::Create(Context, Op.ResultType, Op.ValueKind, Op.Kind, Op.SrcExpr.get(), nullptr, DestTInfo, OpLoc, diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index d3392d776dcd4..f157cadef7d89 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5494,6 +5494,13 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { } S.EnforcedProfiles.push_back({Name.str(), Desig.str(), AL.getLoc()}); + + // P3589R2 [decl.attr.require]p2: for header units, enforce from + // empty-declarations is visible to require. + if (auto *M = S.getCurrentModule()) + if (M->isHeaderUnit()) + M->EnforcedProfileDesignators.push_back(Desig.str()); + Names.push_back(Name); Designators.push_back(Desig); } @@ -5518,18 +5525,6 @@ static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, if (AL.getNumArgs() >= 3) S.checkStringLiteralArgumentAttr(AL, 2, Rule); - // P3589R2 [decl.attr.suppress]p2: justification must be a string-literal. - if (!Justification.empty()) { - bool LooksLikeString = - Justification.starts_with("\"") || Justification.starts_with("L\"") || - Justification.starts_with("u\"") || Justification.starts_with("U\"") || - Justification.starts_with("u8\""); - if (!LooksLikeString) { - // The parser already extracted the value from the string literal token, - // so this validation is handled at parse time by checking the token kind. - } - } - SmallVector RawArgs; for (unsigned I = 3; I < AL.getNumArgs(); ++I) { StringRef Arg; diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 0ff5b5d1ce726..5d0f7f97342b9 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -14,6 +14,7 @@ #include "clang/AST/ASTConsumer.h" #include "clang/AST/ASTMutationListener.h" #include "clang/AST/DynamicRecursiveASTVisitor.h" +#include "clang/Basic/DiagnosticParse.h" #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/Preprocessor.h" #include "clang/Sema/ParsedAttr.h" @@ -481,12 +482,14 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, EnforcedProfiles.push_back({Name.str(), Desig.str(), AL.getLoc()}); - if (MDK == ModuleDeclKind::Interface) + if (MDK == ModuleDeclKind::Interface || + MDK == ModuleDeclKind::PartitionInterface) Mod->EnforcedProfileDesignators.push_back(Desig.str()); } + } else if (AL.isRegularKeywordAttribute()) { + Diag(AL.getLoc(), diag::err_keyword_not_module_attr) << AL; } else { - Diag(AL.getLoc(), diag::warn_unknown_attribute_ignored) - << AL.getAttrName(); + Diag(AL.getLoc(), diag::err_attribute_not_module_attr) << AL; } } @@ -1673,9 +1676,10 @@ void Sema::ActOnModuleImportAttrs(Decl *D, if (!Found) Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; + } else if (AL.isRegularKeywordAttribute()) { + Diag(AL.getLoc(), diag::err_keyword_not_import_attr) << AL; } else { - Diag(AL.getLoc(), diag::warn_unknown_attribute_ignored) - << AL.getAttrName(); + Diag(AL.getLoc(), diag::err_attribute_not_import_attr) << AL; } } } diff --git a/clang/test/Parser/cxx-profiles-framework.cpp b/clang/test/Parser/cxx-profiles-framework.cpp index ff67c5c519414..12d9d05976d87 100644 --- a/clang/test/Parser/cxx-profiles-framework.cpp +++ b/clang/test/Parser/cxx-profiles-framework.cpp @@ -4,8 +4,21 @@ // Valid enforce forms // =================================================================== +// Single designator [[profiles::enforce(std::type)]]; +// Multi-designator in one enforce +[[profiles::enforce(acme::hardened, lib::safety)]]; + +// Designator with profile arguments +[[profiles::enforce(vendor(fortify: 3, sanitize: thread))]]; + +// Nested balanced groups in arguments +[[profiles::enforce(nested(config: (a b)))]]; + +// Bare token arguments +[[profiles::enforce(bare(3))]]; + // =================================================================== // Valid suppress forms // =================================================================== @@ -23,11 +36,11 @@ void suppress_with_rule(); void suppress_with_both(); // =================================================================== -// Parse errors +// [[using profiles: ...]] syntax // =================================================================== -// enforce with empty parens: parse error -[[profiles::enforce()]]; // expected-error {{expected profile name}} +[[using profiles: suppress(std::type)]] +void using_syntax(); // =================================================================== // Profile name with :: separators @@ -35,3 +48,16 @@ void suppress_with_both(); [[profiles::suppress(a::b::c)]] void deep_name(); + +// =================================================================== +// Parse errors +// =================================================================== + +// enforce with empty parens: parse error +[[profiles::enforce()]]; // expected-error {{expected profile name}} + +// enforce with non-identifier first token +[[profiles::enforce(42)]]; // expected-error {{expected profile name}} + +// suppress with empty parens: parse error +[[profiles::suppress()]]; // expected-error {{expected profile name}} diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm new file mode 100644 index 0000000000000..a8d2b27682121 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -0,0 +1,36 @@ +// RUN: rm -rf %t +// RUN: mkdir -p %t +// RUN: split-file %s %t + +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_enforced.cppm -o %t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_ok.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_fail.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_mismatch.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify + +// =================================================================== +// Module with enforced profiles +// =================================================================== +//--- mod_enforced.cppm +// expected-no-diagnostics +export module TestMod [[profiles::enforce(test::type_cast)]]; + +export void mod_func(); + +// =================================================================== +// Require on import: OK when designators match +// =================================================================== +//--- require_ok.cpp +// expected-no-diagnostics +import TestMod [[profiles::require(test::type_cast)]]; + +// =================================================================== +// Require on import: error when profile not enforced by module +// =================================================================== +//--- require_fail.cpp +import TestMod [[profiles::require(test::not_enforced)]]; // expected-error {{required profile 'test::not_enforced' is not enforced by imported module}} + +// =================================================================== +// Require with designator mismatch +// =================================================================== +//--- require_mismatch.cpp +import TestMod [[profiles::require(test::type_cast(strict: true))]]; // expected-error {{required profile 'test::type_cast(strict : true)' is not enforced by imported module}} diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index 14bbffdc69114..dda70713fd747 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -5,6 +5,11 @@ // =================================================================== [[profiles::enforce(test::type_cast)]]; // #enforce1 +// =================================================================== +// Multiple different profiles enforced: OK +// =================================================================== +[[profiles::enforce(test::bounds)]]; + // =================================================================== // Enforce: exact repetition is OK // =================================================================== @@ -16,6 +21,25 @@ [[profiles::enforce(test::type_cast(strict: true))]]; // expected-error {{repeated enforcement of profile 'test::type_cast' with different designator}} \ // expected-note@#enforce1 {{previous attribute is here}} +// =================================================================== +// Enforce after a non-empty declaration: error +// =================================================================== +void some_function(); // #some_function +[[profiles::enforce(test::new_profile)]]; // expected-error {{'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations}} \ + // expected-note@#some_function {{declaration declared here}} + +// =================================================================== +// Enforce inside a namespace: error +// =================================================================== +namespace ns { + [[profiles::enforce(test::type_cast)]]; // expected-error {{'profiles::enforce' attribute on empty-declaration must be at translation unit scope}} +} + +// =================================================================== +// Require not on import: error +// =================================================================== +[[profiles::require(test::type_cast)]]; // expected-error {{'profiles::require' attribute only allowed on module-import-declarations}} + // =================================================================== // Suppress on declarations // =================================================================== @@ -32,3 +56,18 @@ void test_stmt_suppress() { [[profiles::suppress(test::type_cast)]] int x = 0; [[profiles::suppress(test::type_cast)]] { int y = 0; } } + +// =================================================================== +// Suppress with non-string justification: error +// =================================================================== +[[profiles::suppress(test::type_cast, justification: legacy)]] // expected-error {{'justification' argument of 'profiles::suppress' must be a string literal}} +void bad_justification(); + +// =================================================================== +// No diagnostic for a profile that is not enforced +// =================================================================== +// test::type_cast IS enforced, but test::not_enforced is not, so the +// reinterpret_cast warning fires for the enforced profile only. +void test_enforced_profile_warns() { + int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index af620bea3d99e..dc82352cda2a2 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -25,3 +25,13 @@ void test_suppress_stmt_with_rule() { int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } } + +// P3589R2 [decl.attr.enforce]p2: static semantics applied after translation +// phase 7 -- no diagnostic in template definition, only at instantiation. +template +void template_cast(T x) { + auto *p = reinterpret_cast(x); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} +void instantiate() { + template_cast(0); // expected-note {{in instantiation of function template specialization 'template_cast' requested here}} +} From 9f57d05b379aa2bee70ce082413fea620b6e6feb Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 19:04:33 -0400 Subject: [PATCH 003/289] Add more tests --- .../safety-profile-framework-modules.cppm | 22 +++++++++++++++++++ .../test/SemaCXX/safety-profile-framework.cpp | 12 +++++++--- .../test/SemaCXX/safety-profile-type-cast.cpp | 10 +++++---- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index a8d2b27682121..08620dce82863 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -6,6 +6,8 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_ok.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_fail.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_mismatch.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_repeated.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/impl_propagation.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // =================================================================== // Module with enforced profiles @@ -34,3 +36,23 @@ import TestMod [[profiles::require(test::not_enforced)]]; // expected-error {{re // =================================================================== //--- require_mismatch.cpp import TestMod [[profiles::require(test::type_cast(strict: true))]]; // expected-error {{required profile 'test::type_cast(strict : true)' is not enforced by imported module}} + +// =================================================================== +// Repeated require of same designator: OK (P3589R2 Section 2.3 example) +// =================================================================== +//--- require_repeated.cpp +// expected-no-diagnostics +import TestMod [[profiles::require(test::type_cast)]]; +import TestMod [[profiles::require(test::type_cast)]]; + +// =================================================================== +// Interface-to-implementation propagation: the implementation unit +// inherits the enforced profile from the module interface, so the +// reinterpret_cast check fires without a local enforce. +// =================================================================== +//--- impl_propagation.cpp +module TestMod; + +void impl_func() { + int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index dda70713fd747..a5509963291b6 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -64,10 +64,16 @@ void test_stmt_suppress() { void bad_justification(); // =================================================================== -// No diagnostic for a profile that is not enforced +// Enforce at block scope: error (this is a null-statement at block +// scope, so enforce cannot appertain to it) +// =================================================================== +void test_block_scope() { + [[profiles::enforce(test::type_cast)]]; // expected-error {{'profiles::enforce' attribute cannot be applied to a statement}} +} + +// =================================================================== +// Diagnostic fires when profile IS enforced // =================================================================== -// test::type_cast IS enforced, but test::not_enforced is not, so the -// reinterpret_cast warning fires for the enforced profile only. void test_enforced_profile_warns() { int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index dc82352cda2a2..0b78a1ed45a3b 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -1,17 +1,20 @@ -// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++20 %s - +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++20 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++20 %s +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::type_cast)]]; void test_violation() { int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast)]] void test_suppress_decl() { int *p = reinterpret_cast(0); } void test_suppress_stmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast)]] { int *p = reinterpret_cast(0); } @@ -19,8 +22,7 @@ void test_suppress_stmt() { } void test_suppress_stmt_with_rule() { - // Rule-specific suppress only suppresses matching rules; empty rule from the - // test profile check means this suppress does NOT match. + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } From b0ff9921fea1688bf49eb0de12c269077020bf34 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 19:33:11 -0400 Subject: [PATCH 004/289] Require at least one argument for enforce --- clang/lib/Sema/SemaDeclAttr.cpp | 5 ++++- clang/lib/Sema/SemaStmtAttr.cpp | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index f157cadef7d89..efeaf8d34dfb6 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5452,6 +5452,9 @@ static void handleSuppressAttr(Sema &S, Decl *D, const ParsedAttr &AL) { } static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + if (!AL.checkAtLeastNumArgs(S, 2)) + return; + if (!isa(D)) { S.Diag(AL.getLoc(), diag::err_profiles_enforce_not_empty_decl); return; @@ -5512,7 +5515,7 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, const ParsedAttr &AL) { - if (AL.getNumArgs() < 1) + if (!AL.checkAtLeastNumArgs(S, 1)) return; StringRef ProfileName; diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp index 85f590b79aa58..ff8415dd92350 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -74,7 +74,7 @@ static Attr *handleSuppressAttr(Sema &S, Stmt *St, const ParsedAttr &A, static Attr *handleProfilesSuppressStmtAttr(Sema &S, Stmt *St, const ParsedAttr &A, SourceRange Range) { - if (A.getNumArgs() < 1) + if (!A.checkAtLeastNumArgs(S, 1)) return nullptr; StringRef ProfileName; From e53a05110314874228e805985072ff378e78f752 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 19:56:45 -0400 Subject: [PATCH 005/289] Warnings are errors --- .../clang/Basic/DiagnosticSemaKinds.td | 5 ++-- clang/lib/Sema/Sema.cpp | 11 ++++++++ clang/lib/Sema/SemaCast.cpp | 2 +- .../safety-profile-framework-modules.cppm | 2 +- .../test/SemaCXX/safety-profile-framework.cpp | 4 +-- .../test/SemaCXX/safety-profile-type-cast.cpp | 27 ++++++++++++++++--- 6 files changed, 40 insertions(+), 11 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 5085c92464e28..dfd373b174bf0 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14130,7 +14130,6 @@ def err_profiles_require_not_enforced : Error< "required profile '%0' is not enforced by imported module">; def err_profiles_suppress_justification_not_string : Error< "'justification' argument of 'profiles::suppress' must be a string literal">; -def warn_profile_type_cast_reinterpret : Warning< - "'reinterpret_cast' is unsafe under profile '%0'">, - InGroup>; +def err_profile_type_cast_reinterpret : Error< + "'reinterpret_cast' is unsafe under profile '%0'">; } // end of sema component. diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 98d7d25ec25b2..120874b43e778 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3044,6 +3044,17 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, return false; if (isProfileSuppressed(ProfileName, RuleName)) return false; + // P3589R2 Section 1.1: "its static semantic effects are as-if applied only + // after translation phase 7. It is not possible for a profile to change the + // outcome of overload resolution or template instantiation, nor is it + // possible to 'SFINAE out' failure of a program to satisfy a profile + // requirement." + if (isSFINAEContext()) + return false; + if (isUnevaluatedContext()) + return false; + if (currentEvaluationContext().isDiscardedStatementContext()) + return false; Diag(Loc, DiagID) << ProfileName; return true; } diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index 8312cdafc2d8c..c7de1865a6912 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -401,7 +401,7 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); checkProfileViolation("test::type_cast", "", OpLoc, - diag::warn_profile_type_cast_reinterpret); + diag::err_profile_type_cast_reinterpret); } return Op.complete(CXXReinterpretCastExpr::Create(Context, Op.ResultType, Op.ValueKind, Op.Kind, Op.SrcExpr.get(), diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 08620dce82863..7ec7e0683e3f5 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -54,5 +54,5 @@ import TestMod [[profiles::require(test::type_cast)]]; module TestMod; void impl_func() { - int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index a5509963291b6..253c7fbaea17c 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -74,6 +74,6 @@ void test_block_scope() { // =================================================================== // Diagnostic fires when profile IS enforced // =================================================================== -void test_enforced_profile_warns() { - int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +void test_enforced_profile_errors() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 0b78a1ed45a3b..d2374cf08f199 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -4,7 +4,7 @@ [[profiles::enforce(test::type_cast)]]; void test_violation() { - int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} @@ -18,13 +18,13 @@ void test_suppress_stmt() { [[profiles::suppress(test::type_cast)]] { int *p = reinterpret_cast(0); } - int *q = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + int *q = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } void test_suppress_stmt_with_rule() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { - int *p = reinterpret_cast(0); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } } @@ -32,8 +32,27 @@ void test_suppress_stmt_with_rule() { // phase 7 -- no diagnostic in template definition, only at instantiation. template void template_cast(T x) { - auto *p = reinterpret_cast(x); // expected-warning {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + auto *p = reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } void instantiate() { template_cast(0); // expected-note {{in instantiation of function template specialization 'template_cast' requested here}} } + +// P3589R2 Section 1.1: profile violations must not affect overload resolution. +// The reinterpret_cast in the decltype return type is in an unevaluated +// context, so the profile check is suppressed and doesn't SFINAE out the +// first overload. The selected overload's body has no violation. +template +auto sfinae_cast(T x) -> decltype(reinterpret_cast(x)) { + return nullptr; +} +template +int sfinae_cast(...) { return 0; } +void test_sfinae() { + int *p = sfinae_cast(0L); +} + +// Profile violations are suppressed in unevaluated contexts. +void test_unevaluated() { + using T = decltype(reinterpret_cast(0)); +} From 05748730573f8ec29c014859acc1470808eeb797 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 20:04:51 -0400 Subject: [PATCH 006/289] Test SFINAE --- clang/test/SemaCXX/safety-profile-type-cast.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index d2374cf08f199..730737ca2301a 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -39,18 +39,17 @@ void instantiate() { } // P3589R2 Section 1.1: profile violations must not affect overload resolution. -// The reinterpret_cast in the decltype return type is in an unevaluated -// context, so the profile check is suppressed and doesn't SFINAE out the -// first overload. The selected overload's body has no violation. +// If the profile error in the decltype SFINAE'd out the first overload, the +// fallback (returning 1) would be selected and the static_assert would fire. template auto sfinae_cast(T x) -> decltype(reinterpret_cast(x)) { return nullptr; } template -int sfinae_cast(...) { return 0; } -void test_sfinae() { - int *p = sfinae_cast(0L); -} +auto sfinae_cast(...) -> int { return 1; } + +static_assert(__is_same(decltype(sfinae_cast(0L)), int *), + "profile violation must not SFINAE out the first overload"); // Profile violations are suppressed in unevaluated contexts. void test_unevaluated() { From 372a1bed905a2be90fca7c94c8b49b5b2bfdacee Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 20:16:25 -0400 Subject: [PATCH 007/289] Add missing arguments check --- clang/lib/Sema/SemaModule.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 5d0f7f97342b9..2349e3b51c6a1 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -1657,6 +1657,9 @@ void Sema::ActOnModuleImportAttrs(Decl *D, for (const auto &AL : Attrs) { if (AL.getKind() == ParsedAttr::AT_ProfilesRequire) { + if (!AL.checkAtLeastNumArgs(*this, 1)) + continue; + if (!ImportedMod) { Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); continue; From 5ac1e185f1c17188bcfa009e57595f7f17f35ea8 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 20:30:24 -0400 Subject: [PATCH 008/289] Readd calls to ProhibitCXX11Attributes --- clang/lib/Parse/Parser.cpp | 22 ++++++++++++++++++++++ clang/lib/Sema/SemaModule.cpp | 9 --------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp index 6eb4b464daebf..d24abaade1aa9 100644 --- a/clang/lib/Parse/Parser.cpp +++ b/clang/lib/Parse/Parser.cpp @@ -2381,6 +2381,17 @@ Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { ParsedAttributes Attrs(AttrFactory); MaybeParseCXX11Attributes(Attrs); + // Reject non-profile attributes on the module-declaration. Profile + // attributes are handled by ActOnModuleDecl. + for (const ParsedAttr &AL : Attrs) { + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) + continue; + if (AL.isRegularKeywordAttribute()) + Diag(AL.getLoc(), diag::err_keyword_not_module_attr) << AL; + else if (AL.isStandardAttributeSyntax()) + Diag(AL.getLoc(), diag::err_attribute_not_module_attr) << AL; + } + if (ExpectAndConsumeSemi(diag::err_expected_semi_after_module_or_import, tok::getKeywordSpelling(tok::kw_module))) SkipUntil(tok::semi); @@ -2435,6 +2446,17 @@ Decl *Parser::ParseModuleImport(SourceLocation AtLoc, ParsedAttributes Attrs(AttrFactory); MaybeParseCXX11Attributes(Attrs); + // Reject non-profile attributes on the import-declaration. Profile + // attributes are handled by ActOnModuleImportAttrs. + for (const ParsedAttr &AL : Attrs) { + if (AL.getKind() == ParsedAttr::AT_ProfilesRequire) + continue; + if (AL.isRegularKeywordAttribute()) + Diag(AL.getLoc(), diag::err_keyword_not_import_attr) << AL; + else if (AL.isStandardAttributeSyntax()) + Diag(AL.getLoc(), diag::err_attribute_not_import_attr) << AL; + } + if (PP.hadModuleLoaderFatalFailure()) { // With a fatal failure in the module loader, we abort parsing. cutOffParsing(); diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 2349e3b51c6a1..298670dfed258 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -14,7 +14,6 @@ #include "clang/AST/ASTConsumer.h" #include "clang/AST/ASTMutationListener.h" #include "clang/AST/DynamicRecursiveASTVisitor.h" -#include "clang/Basic/DiagnosticParse.h" #include "clang/Lex/HeaderSearch.h" #include "clang/Lex/Preprocessor.h" #include "clang/Sema/ParsedAttr.h" @@ -486,10 +485,6 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, MDK == ModuleDeclKind::PartitionInterface) Mod->EnforcedProfileDesignators.push_back(Desig.str()); } - } else if (AL.isRegularKeywordAttribute()) { - Diag(AL.getLoc(), diag::err_keyword_not_module_attr) << AL; - } else { - Diag(AL.getLoc(), diag::err_attribute_not_module_attr) << AL; } } @@ -1679,10 +1674,6 @@ void Sema::ActOnModuleImportAttrs(Decl *D, if (!Found) Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; - } else if (AL.isRegularKeywordAttribute()) { - Diag(AL.getLoc(), diag::err_keyword_not_import_attr) << AL; - } else { - Diag(AL.getLoc(), diag::err_attribute_not_import_attr) << AL; } } } From 27e0be97ab7f101cb53f7b8bd469d3b1ac3cbcc7 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 31 Mar 2026 20:49:22 -0400 Subject: [PATCH 009/289] Fix another parser bug --- clang/lib/Parse/ParseDecl.cpp | 4 ++++ clang/lib/Sema/SemaModule.cpp | 2 ++ .../test/SemaCXX/safety-profile-type-cast.cpp | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+) diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index 72935f427b7f8..fe64589d69060 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -23,6 +23,7 @@ #include "clang/Parse/Parser.h" #include "clang/Parse/RAIIObjectsForParser.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" +#include "clang/Sema/Sema.h" #include "clang/Sema/Lookup.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/ParsedTemplate.h" @@ -2135,6 +2136,9 @@ Parser::DeclGroupPtrTy Parser::ParseDeclGroup(ParsingDeclSpec &DS, // attributes higher up the callchain. ParsedAttributes LocalAttrs(AttrFactory); LocalAttrs.takeAllPrependingFrom(Attrs); + + Sema::ProfileSuppressRAII ProfileSuppressGuard(Actions, LocalAttrs); + ParsingDeclarator D(*this, DS, LocalAttrs, Context); if (TemplateInfo.TemplateParams) D.setTemplateParameterLists(*TemplateInfo.TemplateParams); diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 298670dfed258..b8acab5b94d86 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -463,6 +463,8 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, // Process [[profiles::enforce]] on the module-declaration. for (const auto &AL : Attrs) { if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) { + if (!AL.checkAtLeastNumArgs(*this, 2)) + continue; unsigned NumDesignators = AL.getNumArgs() / 2; for (unsigned I = 0; I < NumDesignators; ++I) { StringRef Name, Desig; diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 730737ca2301a..f665c014da28e 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -55,3 +55,21 @@ static_assert(__is_same(decltype(sfinae_cast(0L)), int *), void test_unevaluated() { using T = decltype(reinterpret_cast(0)); } + +// Suppress on TU-scope variable initializer (pull model via push in ParseDeclGroup). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] +int *tu_scope_var = reinterpret_cast(0); + +// Suppress on block-scope variable initializer. +void test_suppress_var_init() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] int *p = reinterpret_cast(0); +} + +// Profile violations are suppressed in discarded if-constexpr branches. +void test_discarded_branch() { + if constexpr (false) { + int *p = reinterpret_cast(0); + } +} From 42a1937a530bb67af166c4166baaf43d6973bb7c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Apr 2026 19:14:28 -0400 Subject: [PATCH 010/289] Add further tests --- .../test/SemaCXX/safety-profile-framework.cpp | 13 +++ .../test/SemaCXX/safety-profile-type-cast.cpp | 99 +++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index 253c7fbaea17c..63f06229a540a 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -28,6 +28,19 @@ void some_function(); // #some_function [[profiles::enforce(test::new_profile)]]; // expected-error {{'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations}} \ // expected-note@#some_function {{declaration declared here}} +// =================================================================== +// Enforce on non-empty declaration (function): error +// =================================================================== +[[profiles::enforce(test::type_cast)]] // expected-error {{'profiles::enforce' attribute only allowed on empty-declarations and module-declarations}} +void enforced_func(); + +// =================================================================== +// Enforce inside class scope: error +// =================================================================== +struct EnforceInClass { + [[profiles::enforce(test::type_cast)]]; // expected-warning {{declaration does not declare anything}} +}; + // =================================================================== // Enforce inside a namespace: error // =================================================================== diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index f665c014da28e..dbea4d3c41a28 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -2,6 +2,8 @@ // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++20 %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::type_cast)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; void test_violation() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} @@ -73,3 +75,100 @@ void test_discarded_branch() { int *p = reinterpret_cast(0); } } + +// Lambda inside enforced scope. +void test_lambda() { + auto f = []() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; +} + +// Nested suppression with correct save/restore. +void test_nested_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + int *p = reinterpret_cast(0); + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + int *q = reinterpret_cast(0); + } + int *r = reinterpret_cast(0); + } + int *s = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// sizeof is an unevaluated context. +void test_sizeof_unevaluated() { + auto s = sizeof(reinterpret_cast(0)); +} + +// noexcept is an unevaluated context. +void test_noexcept_unevaluated() { + bool b = noexcept(reinterpret_cast(0)); +} + +// Requires-expression is an unevaluated context. +void test_requires_unevaluated() { + bool b = requires { reinterpret_cast(0); }; +} + +// Default function argument with violation. +void default_arg_func(int *p = reinterpret_cast(0)); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + +// Suppress with justification works identically to suppress without. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast, justification: "legacy code")]] +void test_suppress_with_justification() { + int *p = reinterpret_cast(0); +} + +// Suppress on various statement kinds. +void test_suppress_on_stmts() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + for (int *p = reinterpret_cast(0);;) break; + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + while (reinterpret_cast(0)) break; + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + if (reinterpret_cast(0)) {} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + (void)reinterpret_cast(0); +} + +// Suppress on null-statement is a no-op: the next statement is NOT suppressed. +void test_suppress_null_stmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]]; + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// Suppress on class definition: member functions are suppressed via DeclAttr. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedStruct { + void f() { + int *p = reinterpret_cast(0); + } +}; + +// Suppress on namespace: functions inside are suppressed via DeclAttr. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] suppressed_ns { + void g() { + int *p = reinterpret_cast(0); + } +} + +// Selective suppression: suppressing a different profile does not +// suppress test::type_cast violations. +void test_selective_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +} From 2c70fae0e6135cf741cb35f1a1a79ca3a568dfc5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 15:23:13 -0400 Subject: [PATCH 011/289] Fix rule based profile suppression --- clang/include/clang/Parse/Parser.h | 1 + clang/lib/Parse/ParseDeclCXX.cpp | 19 +++++++++++++++++-- clang/lib/Sema/SemaCast.cpp | 2 +- clang/test/Parser/cxx-profiles-framework.cpp | 19 +++++++++++++++++-- .../test/SemaCXX/safety-profile-type-cast.cpp | 14 ++++++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index ffc806197e262..8e47d81c3c591 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2286,6 +2286,7 @@ class Parser : public CodeCompletionHandler { SmallVectorImpl &Designators); bool ParseProfileArgumentList(SmallVectorImpl &Args); bool ParseNonCommaBalancedToken(std::string &Spelling); + bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling); void MaybeParseCXX11Attributes(Declarator &D) { if (isAllowedCXX11AttributeSpecifier()) { diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 9489000044afd..da298e45df61b 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5104,6 +5104,18 @@ bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { return false; } +bool Parser::ParseNonOperatorNonPunctuatorToken(std::string &Spelling) { + // P3589R2 [dcl.attr.profiles]: A bare profile-argument is a + // non-operator-non-punctuator-token. + if (tok::getPunctuatorSpelling(Tok.getKind()) || Tok.is(tok::eof)) { + Diag(Tok, diag::err_profiles_invalid_argument_token); + return true; + } + Spelling = PP.getSpelling(Tok); + ConsumeAnyToken(); + return false; +} + bool Parser::ParseProfileArgumentList(SmallVectorImpl &Args) { while (true) { if (Tok.is(tok::identifier) && NextToken().is(tok::colon)) { @@ -5117,7 +5129,7 @@ bool Parser::ParseProfileArgumentList(SmallVectorImpl &Args) { Args.push_back(Key + " : " + Value); } else { std::string Spelling; - if (ParseNonCommaBalancedToken(Spelling)) + if (ParseNonOperatorNonPunctuatorToken(Spelling)) return true; Args.push_back(std::move(Spelling)); } @@ -5262,8 +5274,11 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, return true; } Justification = JustVal.str(); - } else if (ArgRef.starts_with("rule : ")) + } else if (ArgRef.starts_with("rule : ")) { Rule = ArgRef.substr(strlen("rule : ")).str(); + if (Rule.size() >= 2 && Rule.front() == '"' && Rule.back() == '"') + Rule = Rule.substr(1, Rule.size() - 2); + } } } diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index c7de1865a6912..137312260d66a 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -400,7 +400,7 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, if (Op.SrcExpr.isInvalid()) return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); - checkProfileViolation("test::type_cast", "", OpLoc, + checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, diag::err_profile_type_cast_reinterpret); } return Op.complete(CXXReinterpretCastExpr::Create(Context, Op.ResultType, diff --git a/clang/test/Parser/cxx-profiles-framework.cpp b/clang/test/Parser/cxx-profiles-framework.cpp index 12d9d05976d87..aef559513c14f 100644 --- a/clang/test/Parser/cxx-profiles-framework.cpp +++ b/clang/test/Parser/cxx-profiles-framework.cpp @@ -16,8 +16,14 @@ // Nested balanced groups in arguments [[profiles::enforce(nested(config: (a b)))]]; -// Bare token arguments -[[profiles::enforce(bare(3))]]; +// Bare non-operator-non-punctuator token arguments +[[profiles::enforce(bare1(3))]]; + +// Bare string literal argument +[[profiles::enforce(bare2("hello"))]]; + +// Bare identifier argument +[[profiles::enforce(bare3(abc))]]; // =================================================================== // Valid suppress forms @@ -61,3 +67,12 @@ void deep_name(); // suppress with empty parens: parse error [[profiles::suppress()]]; // expected-error {{expected profile name}} + +// Bare argument cannot be an operator (suppress uses profile-argument-list +// directly after comma, avoiding cascading errors from nested designator parens) +[[profiles::suppress(std::type, +)]] // expected-error {{invalid token in profile argument}} +void suppress_bare_operator(); + +// Bare argument cannot be a balanced group +[[profiles::suppress(std::type, (a b))]] // expected-error {{invalid token in profile argument}} +void suppress_bare_group(); diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index dbea4d3c41a28..1d85d0b8ead86 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -26,6 +26,20 @@ void test_suppress_stmt() { void test_suppress_stmt_with_rule() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { + int *p = reinterpret_cast(0); + } +} + +void test_suppress_stmt_with_bare_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: reinterpret_cast)]] { + int *p = reinterpret_cast(0); + } +} + +void test_suppress_stmt_with_nonmatching_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: "static_cast")]] { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } } From 7cee83912d2781badf968655db699581fcd63d11 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 16:12:02 -0400 Subject: [PATCH 012/289] Clean up argument parsing a bit --- clang/lib/Parse/ParseDeclCXX.cpp | 70 +++++++++++++++++++++++--------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index da298e45df61b..ee6e67d7ac8aa 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5253,32 +5253,62 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, std::string Justification, Rule; SmallVector RawArgs; - if (TryConsumeToken(tok::comma)) { - if (ParseProfileArgumentList(RawArgs)) { - SkipToRParen(); + auto ParseStringLiteralValue = [&](std::string &Out) -> bool { + SmallVector StringToks; + do { + StringToks.push_back(Tok); + ConsumeStringToken(); + } while (tok::isStringLiteral(Tok.getKind())); + StringLiteralParser Literal(StringToks, PP); + if (Literal.hadError) return true; - } + Out = Literal.GetString().str(); + return false; + }; - for (const auto &Arg : RawArgs) { - StringRef ArgRef(Arg); - if (ArgRef.starts_with("justification : ")) { - StringRef JustVal = ArgRef.substr(strlen("justification : ")); - // P3589R2 [decl.attr.suppress]p2: justification must be a - // string-literal. Check the spelling recorded by the parser. - if (!JustVal.starts_with("\"") && !JustVal.starts_with("L\"") && - !JustVal.starts_with("u\"") && !JustVal.starts_with("U\"") && - !JustVal.starts_with("u8\"")) { - Diag(AttrNameLoc, - diag::err_profiles_suppress_justification_not_string); + while (TryConsumeToken(tok::comma)) { + if (Tok.is(tok::identifier) && NextToken().is(tok::colon)) { + IdentifierInfo *KeyII = Tok.getIdentifierInfo(); + ConsumeToken(); + ConsumeToken(); + + if (KeyII->isStr("justification")) { + if (!tok::isStringLiteral(Tok.getKind())) { + Diag(Tok, diag::err_profiles_suppress_justification_not_string); + SkipToRParen(); + return true; + } + if (ParseStringLiteralValue(Justification)) { SkipToRParen(); return true; } - Justification = JustVal.str(); - } else if (ArgRef.starts_with("rule : ")) { - Rule = ArgRef.substr(strlen("rule : ")).str(); - if (Rule.size() >= 2 && Rule.front() == '"' && Rule.back() == '"') - Rule = Rule.substr(1, Rule.size() - 2); + } else if (KeyII->isStr("rule")) { + if (tok::isStringLiteral(Tok.getKind())) { + if (ParseStringLiteralValue(Rule)) { + SkipToRParen(); + return true; + } + } else { + if (ParseNonCommaBalancedToken(Rule)) { + SkipToRParen(); + return true; + } + } + } else { + std::string Value; + if (ParseNonCommaBalancedToken(Value)) { + SkipToRParen(); + return true; + } + RawArgs.push_back(KeyII->getName().str() + " : " + Value); + } + } else { + std::string Spelling; + if (ParseNonOperatorNonPunctuatorToken(Spelling)) { + SkipToRParen(); + return true; } + RawArgs.push_back(std::move(Spelling)); } } From 0854ff624deddc5e40c21396b336777f49adee15 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 16:20:44 -0400 Subject: [PATCH 013/289] Move enforce handling to helper --- clang/include/clang/Sema/Sema.h | 2 ++ clang/lib/Sema/Sema.cpp | 14 ++++++++++++++ clang/lib/Sema/SemaDeclAttr.cpp | 29 ++++++++++++----------------- clang/lib/Sema/SemaModule.cpp | 15 ++++----------- 4 files changed, 32 insertions(+), 28 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 8b62c85f72f39..346d8ed4dfb70 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1051,6 +1051,8 @@ class Sema final : public SemaBase { bool isProfileEnforced(StringRef ProfileName) const; const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; + bool addProfileEnforcement(StringRef Name, StringRef Designator, + SourceLocation Loc); void pushProfileSuppression(StringRef ProfileName, StringRef RuleName); void popProfileSuppressions(unsigned Count); diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 120874b43e778..913c57ef57151 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -2996,6 +2996,20 @@ Sema::getProfileEnforcement(StringRef ProfileName) const { return nullptr; } +bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, + SourceLocation Loc) { + if (const auto *Existing = getProfileEnforcement(Name)) { + if (Existing->CanonicalDesignator != Designator) { + Diag(Loc, diag::err_profiles_enforce_mismatch) << Name; + Diag(Existing->EnforceLoc, diag::note_previous_attribute); + return false; + } + return true; + } + EnforcedProfiles.push_back({Name.str(), Designator.str(), Loc}); + return true; +} + void Sema::pushProfileSuppression(StringRef ProfileName, StringRef RuleName) { ProfileSuppressStack.push_back( {ProfileName.str(), RuleName.str()}); diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index efeaf8d34dfb6..5b7ebef81fa06 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5487,25 +5487,20 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { !S.checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) return; - if (const auto *Existing = S.getProfileEnforcement(Name)) { - if (Existing->CanonicalDesignator != Desig) { - S.Diag(AL.getLoc(), diag::err_profiles_enforce_mismatch) << Name; - S.Diag(Existing->EnforceLoc, diag::note_previous_attribute); - return; - } - continue; - } - - S.EnforcedProfiles.push_back({Name.str(), Desig.str(), AL.getLoc()}); + bool IsNew = !S.isProfileEnforced(Name); + if (!S.addProfileEnforcement(Name, Desig, AL.getLoc())) + return; - // P3589R2 [decl.attr.require]p2: for header units, enforce from - // empty-declarations is visible to require. - if (auto *M = S.getCurrentModule()) - if (M->isHeaderUnit()) - M->EnforcedProfileDesignators.push_back(Desig.str()); + if (IsNew) { + // P3589R2 [decl.attr.require]p2: for header units, enforce from + // empty-declarations is visible to require. + if (auto *M = S.getCurrentModule()) + if (M->isHeaderUnit()) + M->EnforcedProfileDesignators.push_back(Desig.str()); - Names.push_back(Name); - Designators.push_back(Desig); + Names.push_back(Name); + Designators.push_back(Desig); + } } D->addAttr(::new (S.Context) diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index b8acab5b94d86..9ccb533379d1c 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -472,19 +472,12 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, !checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) continue; - if (const auto *Existing = getProfileEnforcement(Name)) { - if (Existing->CanonicalDesignator != Desig) { - Diag(AL.getLoc(), diag::err_profiles_enforce_mismatch) << Name; - Diag(Existing->EnforceLoc, diag::note_previous_attribute); - continue; - } + bool IsNew = !isProfileEnforced(Name); + if (!addProfileEnforcement(Name, Desig, AL.getLoc())) continue; - } - - EnforcedProfiles.push_back({Name.str(), Desig.str(), AL.getLoc()}); - if (MDK == ModuleDeclKind::Interface || - MDK == ModuleDeclKind::PartitionInterface) + if (IsNew && (MDK == ModuleDeclKind::Interface || + MDK == ModuleDeclKind::PartitionInterface)) Mod->EnforcedProfileDesignators.push_back(Desig.str()); } } From 0d5293dc344271dbbd41759b0d7766d68a2c5760 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 16:32:32 -0400 Subject: [PATCH 014/289] Move suppress handling to helper --- clang/include/clang/Sema/Sema.h | 3 +++ clang/lib/Sema/Sema.cpp | 23 +++++++++++++++++++++++ clang/lib/Sema/SemaDeclAttr.cpp | 22 ++-------------------- clang/lib/Sema/SemaStmtAttr.cpp | 21 +-------------------- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 346d8ed4dfb70..a7dd3d442a548 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -157,6 +157,7 @@ enum class OverloadCandidateParamOrder : char; enum OverloadCandidateRewriteKind : unsigned; class OverloadCandidateSet; class Preprocessor; +class ProfilesSuppressAttr; class SemaAMDGPU; class SemaARM; class SemaAVR; @@ -1054,6 +1055,8 @@ class Sema final : public SemaBase { bool addProfileEnforcement(StringRef Name, StringRef Designator, SourceLocation Loc); + ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); + void pushProfileSuppression(StringRef ProfileName, StringRef RuleName); void popProfileSuppressions(unsigned Count); diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 913c57ef57151..76763859a64dd 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3010,6 +3010,29 @@ bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, return true; } +ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { + StringRef ProfileName; + if (!checkStringLiteralArgumentAttr(AL, 0, ProfileName)) + return nullptr; + + StringRef Justification, Rule; + if (AL.getNumArgs() >= 2) + checkStringLiteralArgumentAttr(AL, 1, Justification); + if (AL.getNumArgs() >= 3) + checkStringLiteralArgumentAttr(AL, 2, Rule); + + SmallVector RawArgs; + for (unsigned I = 3; I < AL.getNumArgs(); ++I) { + StringRef Arg; + if (checkStringLiteralArgumentAttr(AL, I, Arg)) + RawArgs.push_back(Arg); + } + + return ::new (Context) ProfilesSuppressAttr( + Context, AL, ProfileName, Justification, Rule, RawArgs.data(), + RawArgs.size()); +} + void Sema::pushProfileSuppression(StringRef ProfileName, StringRef RuleName) { ProfileSuppressStack.push_back( {ProfileName.str(), RuleName.str()}); diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 5b7ebef81fa06..d7685664d7c83 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5513,26 +5513,8 @@ static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, if (!AL.checkAtLeastNumArgs(S, 1)) return; - StringRef ProfileName; - if (!S.checkStringLiteralArgumentAttr(AL, 0, ProfileName)) - return; - - StringRef Justification, Rule; - if (AL.getNumArgs() >= 2) - S.checkStringLiteralArgumentAttr(AL, 1, Justification); - if (AL.getNumArgs() >= 3) - S.checkStringLiteralArgumentAttr(AL, 2, Rule); - - SmallVector RawArgs; - for (unsigned I = 3; I < AL.getNumArgs(); ++I) { - StringRef Arg; - if (S.checkStringLiteralArgumentAttr(AL, I, Arg)) - RawArgs.push_back(Arg); - } - - D->addAttr(::new (S.Context) ProfilesSuppressAttr( - S.Context, AL, ProfileName, Justification, Rule, RawArgs.data(), - RawArgs.size())); + if (auto *A = S.makeProfilesSuppressAttr(AL)) + D->addAttr(A); } static void handleLifetimeCategoryAttr(Sema &S, Decl *D, const ParsedAttr &AL) { diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp index ff8415dd92350..a404309ead620 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -77,26 +77,7 @@ static Attr *handleProfilesSuppressStmtAttr(Sema &S, Stmt *St, if (!A.checkAtLeastNumArgs(S, 1)) return nullptr; - StringRef ProfileName; - if (!S.checkStringLiteralArgumentAttr(A, 0, ProfileName)) - return nullptr; - - StringRef Justification, Rule; - if (A.getNumArgs() >= 2) - S.checkStringLiteralArgumentAttr(A, 1, Justification); - if (A.getNumArgs() >= 3) - S.checkStringLiteralArgumentAttr(A, 2, Rule); - - SmallVector RawArgs; - for (unsigned I = 3; I < A.getNumArgs(); ++I) { - StringRef Arg; - if (S.checkStringLiteralArgumentAttr(A, I, Arg)) - RawArgs.push_back(Arg); - } - - return ::new (S.Context) ProfilesSuppressAttr( - S.Context, A, ProfileName, Justification, Rule, RawArgs.data(), - RawArgs.size()); + return S.makeProfilesSuppressAttr(A); } static Attr *handleLoopHintAttr(Sema &S, Stmt *St, const ParsedAttr &A, From 52f262e7615ca4fed5b84b3a8c093b1f5e078c9b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 16:47:53 -0400 Subject: [PATCH 015/289] Don't reparse profile names --- clang/include/clang/Basic/Module.h | 8 ++++++-- clang/lib/Sema/SemaDeclAttr.cpp | 2 +- clang/lib/Sema/SemaModule.cpp | 24 ++++++++++-------------- clang/lib/Serialization/ASTReader.cpp | 13 +++++++++---- clang/lib/Serialization/ASTWriter.cpp | 8 +++++--- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/clang/include/clang/Basic/Module.h b/clang/include/clang/Basic/Module.h index 35b06ea59d889..8829e4147ab25 100644 --- a/clang/include/clang/Basic/Module.h +++ b/clang/include/clang/Basic/Module.h @@ -457,8 +457,12 @@ class alignas(8) Module { /// module depends. llvm::SmallSetVector Imports; - /// Profile designators enforced on this module's declaration (P3589R2). - SmallVector EnforcedProfileDesignators; + /// Profile enforcements on this module's declaration (P3589R2). + struct EnforcedProfile { + std::string ProfileName; + std::string Designator; + }; + SmallVector EnforcedProfileDesignators; /// The set of top-level modules that affected the compilation of this module, /// but were not imported. diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index d7685664d7c83..6a16d58eea103 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5496,7 +5496,7 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // empty-declarations is visible to require. if (auto *M = S.getCurrentModule()) if (M->isHeaderUnit()) - M->EnforcedProfileDesignators.push_back(Desig.str()); + M->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); Names.push_back(Name); Designators.push_back(Desig); diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 9ccb533379d1c..16aab865b68a0 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -478,7 +478,7 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, if (IsNew && (MDK == ModuleDeclKind::Interface || MDK == ModuleDeclKind::PartitionInterface)) - Mod->EnforcedProfileDesignators.push_back(Desig.str()); + Mod->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); } } } @@ -495,12 +495,10 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, // P3589R2 [decl.attr.enforce]p4: propagate interface's enforced profiles to // implementation unit. if (Interface) { - for (const auto &Desig : Interface->EnforcedProfileDesignators) { - std::string Name = Desig; - if (auto Paren = Name.find('('); Paren != std::string::npos) - Name = Name.substr(0, Paren); - if (!isProfileEnforced(Name)) - EnforcedProfiles.push_back({Name, Desig, ModuleLoc}); + for (const auto &EP : Interface->EnforcedProfileDesignators) { + if (!isProfileEnforced(EP.ProfileName)) + EnforcedProfiles.push_back( + {EP.ProfileName, EP.Designator, ModuleLoc}); } } @@ -1659,13 +1657,11 @@ void Sema::ActOnModuleImportAttrs(Decl *D, if (!checkStringLiteralArgumentAttr(AL, 0, Desig)) continue; - bool Found = false; - for (const auto &Enforced : ImportedMod->EnforcedProfileDesignators) { - if (Enforced == Desig) { - Found = true; - break; - } - } + bool Found = llvm::any_of( + ImportedMod->EnforcedProfileDesignators, + [&](const Module::EnforcedProfile &EP) { + return EP.Designator == Desig; + }); if (!Found) Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 0a1ef371dc790..754e91738efb4 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -6572,11 +6572,16 @@ llvm::Error ASTReader::ReadSubmoduleBlock(ModuleFile &F, case SUBMODULE_ENFORCED_PROFILES: { unsigned Idx = 0; while (Idx < Record.size()) { - unsigned Len = Record[Idx++]; + unsigned NameLen = Record[Idx++]; + std::string Name(Record.begin() + Idx, + Record.begin() + Idx + NameLen); + Idx += NameLen; + unsigned DesigLen = Record[Idx++]; std::string Desig(Record.begin() + Idx, - Record.begin() + Idx + Len); - Idx += Len; - CurrentModule->EnforcedProfileDesignators.push_back(std::move(Desig)); + Record.begin() + Idx + DesigLen); + Idx += DesigLen; + CurrentModule->EnforcedProfileDesignators.push_back( + {std::move(Name), std::move(Desig)}); } break; } diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 2c31e161fc9ae..b6fd5293f07bb 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -3259,9 +3259,11 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { // Emit enforced profile designators (P3589R2). if (!Mod->EnforcedProfileDesignators.empty()) { RecordData Record; - for (const auto &Desig : Mod->EnforcedProfileDesignators) { - Record.push_back(Desig.size()); - Record.append(Desig.begin(), Desig.end()); + for (const auto &EP : Mod->EnforcedProfileDesignators) { + Record.push_back(EP.ProfileName.size()); + Record.append(EP.ProfileName.begin(), EP.ProfileName.end()); + Record.push_back(EP.Designator.size()); + Record.append(EP.Designator.begin(), EP.Designator.end()); } Stream.EmitRecord(SUBMODULE_ENFORCED_PROFILES, Record); } From 79fe5c26ba367442ee383b23388ff347bb4a5a9f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:07:35 -0400 Subject: [PATCH 016/289] test::type_cast is a test profile --- clang/lib/Sema/SemaCast.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index 137312260d66a..aae3ad0fec9f2 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -400,6 +400,7 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, if (Op.SrcExpr.isInvalid()) return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); + // "test::type_cast" is a testing-only attribute checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, diag::err_profile_type_cast_reinterpret); } From a883cfd45100ec38386d39b27d5ed879bae6c909 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:08:48 -0400 Subject: [PATCH 017/289] Add comment explaining abstraction boundary break --- clang/lib/Parse/ParseDeclCXX.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index ee6e67d7ac8aa..d2cf2faf2c29f 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5036,6 +5036,8 @@ void Parser::ParseMicrosoftIfExistsClassDeclaration( // C++ Profiles framework (P3589R2) //===----------------------------------------------------------------------===// +// Profile names may contain '::' (e.g. "std::type"), so IdentifierLoc is not +// suitable. We synthesize StringLiteral nodes to pass them through ArgsUnion. static Expr *MakeProfileStringLiteral(Parser &P, StringRef Str, SourceLocation Loc) { ASTContext &Ctx = P.getActions().Context; From 2941604ec7ba01cab5c7b44921890075745a34b0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:16:20 -0400 Subject: [PATCH 018/289] Add processProfilesEnforceAttr --- clang/include/clang/Sema/Sema.h | 3 +++ clang/lib/Sema/Sema.cpp | 30 ++++++++++++++++++++++++++++ clang/lib/Sema/SemaDeclAttr.cpp | 35 +++++++++------------------------ clang/lib/Sema/SemaModule.cpp | 28 ++++++++------------------ 4 files changed, 50 insertions(+), 46 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index a7dd3d442a548..548677d68585b 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1054,6 +1054,9 @@ class Sema final : public SemaBase { const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; bool addProfileEnforcement(StringRef Name, StringRef Designator, SourceLocation Loc); + bool processProfilesEnforceAttr(const ParsedAttr &AL, Module *Mod, + SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators); ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 76763859a64dd..970ddaf7ddad5 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3010,6 +3010,36 @@ bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, return true; } +bool Sema::processProfilesEnforceAttr( + const ParsedAttr &AL, Module *Mod, + SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators) { + if (!AL.checkAtLeastNumArgs(*this, 2)) + return false; + + unsigned NumDesignators = AL.getNumArgs() / 2; + for (unsigned I = 0; I < NumDesignators; ++I) { + StringRef Name, Desig; + if (!checkStringLiteralArgumentAttr(AL, I * 2, Name) || + !checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) + continue; + + bool IsNew = !isProfileEnforced(Name); + if (!addProfileEnforcement(Name, Desig, AL.getLoc())) + continue; + + if (IsNew) { + if (Mod) + Mod->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); + if (NewNames) + NewNames->push_back(Name); + if (NewDesignators) + NewDesignators->push_back(Desig); + } + } + return true; +} + ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { StringRef ProfileName; if (!checkStringLiteralArgumentAttr(AL, 0, ProfileName)) diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 6a16d58eea103..ffd3a0d74149d 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5452,9 +5452,6 @@ static void handleSuppressAttr(Sema &S, Decl *D, const ParsedAttr &AL) { } static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { - if (!AL.checkAtLeastNumArgs(S, 2)) - return; - if (!isa(D)) { S.Diag(AL.getLoc(), diag::err_profiles_enforce_not_empty_decl); return; @@ -5478,30 +5475,16 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { } } - // Args are interleaved: name0, desig0, name1, desig1, ... - unsigned NumDesignators = AL.getNumArgs() / 2; - SmallVector Names, Designators; - for (unsigned I = 0; I < NumDesignators; ++I) { - StringRef Name, Desig; - if (!S.checkStringLiteralArgumentAttr(AL, I * 2, Name) || - !S.checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) - return; - - bool IsNew = !S.isProfileEnforced(Name); - if (!S.addProfileEnforcement(Name, Desig, AL.getLoc())) - return; + // P3589R2 [decl.attr.require]p2: for header units, enforce from + // empty-declarations is visible to require. + Module *Mod = nullptr; + if (auto *M = S.getCurrentModule()) + if (M->isHeaderUnit()) + Mod = M; - if (IsNew) { - // P3589R2 [decl.attr.require]p2: for header units, enforce from - // empty-declarations is visible to require. - if (auto *M = S.getCurrentModule()) - if (M->isHeaderUnit()) - M->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); - - Names.push_back(Name); - Designators.push_back(Desig); - } - } + SmallVector Names, Designators; + if (!S.processProfilesEnforceAttr(AL, Mod, &Names, &Designators)) + return; D->addAttr(::new (S.Context) ProfilesEnforceAttr(S.Context, AL, Names.data(), Names.size(), diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 16aab865b68a0..e1556162f5fb6 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -461,26 +461,14 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, TU->setLocalOwningModule(Mod); // Process [[profiles::enforce]] on the module-declaration. - for (const auto &AL : Attrs) { - if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) { - if (!AL.checkAtLeastNumArgs(*this, 2)) - continue; - unsigned NumDesignators = AL.getNumArgs() / 2; - for (unsigned I = 0; I < NumDesignators; ++I) { - StringRef Name, Desig; - if (!checkStringLiteralArgumentAttr(AL, I * 2, Name) || - !checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) - continue; - - bool IsNew = !isProfileEnforced(Name); - if (!addProfileEnforcement(Name, Desig, AL.getLoc())) - continue; - - if (IsNew && (MDK == ModuleDeclKind::Interface || - MDK == ModuleDeclKind::PartitionInterface)) - Mod->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); - } - } + { + Module *ExportMod = (MDK == ModuleDeclKind::Interface || + MDK == ModuleDeclKind::PartitionInterface) + ? Mod + : nullptr; + for (const auto &AL : Attrs) + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) + processProfilesEnforceAttr(AL, ExportMod, nullptr, nullptr); } // We are in the module purview, but before any other (non import) From 61568a1f6add52b31e435184a5e0331b0c414d5f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:20:26 -0400 Subject: [PATCH 019/289] Gracefully handle EOF in ParseNonCommaBalancedToken --- clang/lib/Parse/ParseDeclCXX.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index d2cf2faf2c29f..de7b52711a7a8 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5087,11 +5087,13 @@ bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { ConsumeAnyToken(); } } - if (Tok.is(Close)) { - Spelling += " "; - Spelling += PP.getSpelling(Tok); - ConsumeAnyToken(); + if (Tok.is(tok::eof)) { + Diag(Tok, diag::err_expected) << Close; + return true; } + Spelling += " "; + Spelling += PP.getSpelling(Tok); + ConsumeAnyToken(); return false; } From 42bdd1d647fe3cead5f73063021a48b170541a30 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:24:08 -0400 Subject: [PATCH 020/289] Remove dead code --- clang/lib/Parse/ParseDeclCXX.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index de7b52711a7a8..640830bcb32fd 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5207,10 +5207,6 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, }; if (AttrName->isStr("enforce")) { - if (!Tok.is(tok::l_paren)) { - Diag(Tok, diag::err_profiles_expected_lparen) << "enforce"; - return true; - } ConsumeParen(); SmallVector Names, Designators; @@ -5242,10 +5238,6 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, } if (AttrName->isStr("suppress")) { - if (!Tok.is(tok::l_paren)) { - Diag(Tok, diag::err_profiles_expected_lparen) << "suppress"; - return true; - } ConsumeParen(); std::string ProfileName; @@ -5342,10 +5334,6 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, } if (AttrName->isStr("require")) { - if (!Tok.is(tok::l_paren)) { - Diag(Tok, diag::err_profiles_expected_lparen) << "require"; - return true; - } ConsumeParen(); std::string Name, Designator; From f22a253cd6171b56dae92d8dd68a34dedac7f0fa Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:30:59 -0400 Subject: [PATCH 021/289] Use blob in serialization --- clang/lib/Serialization/ASTReader.cpp | 18 +++++------------- clang/lib/Serialization/ASTWriter.cpp | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 754e91738efb4..5f9a51f1aba33 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -6570,19 +6570,11 @@ llvm::Error ASTReader::ReadSubmoduleBlock(ModuleFile &F, break; case SUBMODULE_ENFORCED_PROFILES: { - unsigned Idx = 0; - while (Idx < Record.size()) { - unsigned NameLen = Record[Idx++]; - std::string Name(Record.begin() + Idx, - Record.begin() + Idx + NameLen); - Idx += NameLen; - unsigned DesigLen = Record[Idx++]; - std::string Desig(Record.begin() + Idx, - Record.begin() + Idx + DesigLen); - Idx += DesigLen; - CurrentModule->EnforcedProfileDesignators.push_back( - {std::move(Name), std::move(Desig)}); - } + unsigned NameLen = Record[0]; + std::string Name = Blob.substr(0, NameLen).str(); + std::string Desig = Blob.substr(NameLen).str(); + CurrentModule->EnforcedProfileDesignators.push_back( + {std::move(Name), std::move(Desig)}); break; } } diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index b6fd5293f07bb..2217260130407 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -3085,6 +3085,12 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Macro name unsigned ExportAsAbbrev = Stream.EmitAbbrev(std::move(Abbrev)); + Abbrev = std::make_shared(); + Abbrev->Add(BitCodeAbbrevOp(SUBMODULE_ENFORCED_PROFILES)); + Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6)); // Profile name length + Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Name + Designator + unsigned EnforcedProfilesAbbrev = Stream.EmitAbbrev(std::move(Abbrev)); + // Write the submodule metadata block. RecordData::value_type Record[] = { getNumberOfModules(WritingModule), @@ -3257,15 +3263,11 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { } // Emit enforced profile designators (P3589R2). - if (!Mod->EnforcedProfileDesignators.empty()) { - RecordData Record; - for (const auto &EP : Mod->EnforcedProfileDesignators) { - Record.push_back(EP.ProfileName.size()); - Record.append(EP.ProfileName.begin(), EP.ProfileName.end()); - Record.push_back(EP.Designator.size()); - Record.append(EP.Designator.begin(), EP.Designator.end()); - } - Stream.EmitRecord(SUBMODULE_ENFORCED_PROFILES, Record); + for (const auto &EP : Mod->EnforcedProfileDesignators) { + RecordData::value_type Record[] = {SUBMODULE_ENFORCED_PROFILES, + EP.ProfileName.size()}; + Stream.EmitRecordWithBlob(EnforcedProfilesAbbrev, Record, + EP.ProfileName + EP.Designator); } // Queue up the submodules of this module. From f6b976d4f3dd8505c9573d233a6d42e362b356ee Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 17:41:32 -0400 Subject: [PATCH 022/289] Fix diagnostic --- clang/lib/Parse/ParseDeclCXX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 640830bcb32fd..80b52a7293e93 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5168,7 +5168,7 @@ bool Parser::ParseProfileDesignator(std::string &Name, } if (!Tok.is(tok::r_paren)) { - Diag(Tok, diag::err_profiles_expected_rparen) << "enforce"; + Diag(Tok, diag::err_expected) << tok::r_paren; return true; } Designator += ")"; From 906d3ea21e6414f24cce808730eb93e95eca2ae3 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 18:32:13 -0400 Subject: [PATCH 023/289] Fix profile suppression in templates --- clang/lib/Sema/TreeTransform.h | 29 +++++++++++++++++ .../test/SemaCXX/safety-profile-type-cast.cpp | 32 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h index b5272d262530c..90923ab2fb36d 100644 --- a/clang/lib/Sema/TreeTransform.h +++ b/clang/lib/Sema/TreeTransform.h @@ -8311,7 +8311,23 @@ template StmtResult TreeTransform::TransformAttributedStmt(AttributedStmt *S, StmtDiscardKind SDK) { + // P3589R2: Push profile suppressions BEFORE transforming the sub-statement + // so that profile violation checks during template instantiation see them. + unsigned ProfileSuppressCount = 0; + if (getSema().getLangOpts().Profiles) { + for (const auto *A : S->getAttrs()) { + if (const auto *PSA = dyn_cast(A)) { + getSema().pushProfileSuppression(PSA->getProfileName(), + PSA->getRule()); + ++ProfileSuppressCount; + } + } + } + StmtResult SubStmt = getDerived().TransformStmt(S->getSubStmt(), SDK); + + getSema().popProfileSuppressions(ProfileSuppressCount); + if (SubStmt.isInvalid()) return StmtError(); @@ -8649,7 +8665,20 @@ TreeTransform::TransformDeclStmt(DeclStmt *S) { SmallVector Decls; LambdaScopeInfo *LSI = getSema().getCurLambda(); for (auto *D : S->decls()) { + // P3589R2: Push profile suppressions from decl attrs so that + // initializer checks during template instantiation see them. + unsigned ProfileSuppressCount = 0; + if (getSema().getLangOpts().Profiles) { + for (const auto *A : D->specific_attrs()) { + getSema().pushProfileSuppression(A->getProfileName(), A->getRule()); + ++ProfileSuppressCount; + } + } + Decl *Transformed = getDerived().TransformDefinition(D->getLocation(), D); + + getSema().popProfileSuppressions(ProfileSuppressCount); + if (!Transformed) return StmtError(); diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 1d85d0b8ead86..c839c5a7aa967 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -186,3 +186,35 @@ void test_selective_suppress() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } } + +// Suppress on compound statement inside template: suppression must be +// effective during instantiation, not just during parsing. +template +void template_suppress_stmt(T x) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto *p = reinterpret_cast(x); + } +} +void instantiate_suppress_stmt() { template_suppress_stmt(0); } + +// Suppress on variable declaration inside template. +template +void template_suppress_var(T x) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] auto *p = reinterpret_cast(x); +} +void instantiate_suppress_var() { template_suppress_var(0); } + +// Suppress ends at statement boundary inside template. +template +void template_suppress_boundary(T x) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto *p = reinterpret_cast(x); + } + auto *q = reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} +void instantiate_suppress_boundary() { + template_suppress_boundary(0); // expected-note {{in instantiation of function template specialization 'template_suppress_boundary' requested here}} +} From b9735d81883c66067c6eaa63cc3a25ef8ddece21 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 18:48:25 -0400 Subject: [PATCH 024/289] Fix suppress inheritence; refactor ParseNonCommaBalancedToken --- clang/lib/Parse/ParseDeclCXX.cpp | 39 +++++++++---------- clang/lib/Sema/SemaDecl.cpp | 2 +- clang/test/Parser/cxx-profiles-framework.cpp | 3 ++ .../test/SemaCXX/safety-profile-type-cast.cpp | 9 +++++ 4 files changed, 31 insertions(+), 22 deletions(-) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 80b52a7293e93..80f7fa6340eaa 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5069,31 +5069,28 @@ bool Parser::ParseProfileName(std::string &Name) { bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { if (Tok.isOneOf(tok::l_paren, tok::l_square, tok::l_brace)) { - tok::TokenKind Open = Tok.getKind(); - tok::TokenKind Close = Open == tok::l_paren ? tok::r_paren - : Open == tok::l_square ? tok::r_square - : tok::r_brace; + tok::TokenKind Close = Tok.is(tok::l_paren) ? tok::r_paren + : Tok.is(tok::l_square) ? tok::r_square + : tok::r_brace; Spelling = PP.getSpelling(Tok); - ConsumeAnyToken(); - unsigned Depth = 1; - while (Depth > 0 && !Tok.is(tok::eof)) { - if (Tok.is(Open)) - ++Depth; - else if (Tok.is(Close)) - --Depth; - if (Depth > 0) { - Spelling += " "; - Spelling += PP.getSpelling(Tok); - ConsumeAnyToken(); - } - } - if (Tok.is(tok::eof)) { + if (Tok.is(tok::l_paren)) + ConsumeParen(); + else if (Tok.is(tok::l_square)) + ConsumeBracket(); + else + ConsumeBrace(); + + CachedTokens Toks; + if (!ConsumeAndStoreUntil(Close, Toks, /*StopAtSemi=*/false, + /*ConsumeFinalToken=*/true)) { Diag(Tok, diag::err_expected) << Close; return true; } - Spelling += " "; - Spelling += PP.getSpelling(Tok); - ConsumeAnyToken(); + + for (const auto &T : Toks) { + Spelling += " "; + Spelling += PP.getSpelling(T); + } return false; } diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 054b664ca0a8b..84ea75caa4e49 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -2974,7 +2974,7 @@ static bool mergeDeclAttribute(Sema &S, NamedDecl *D, NewAttr = S.HLSL().mergeVkConstantIdAttr(D, *CI, CI->getId()); else if (const auto *SA = dyn_cast(Attr)) NewAttr = S.HLSL().mergeShaderAttr(D, *SA, SA->getType()); - else if (isa(Attr)) + else if (isa(Attr) || isa(Attr)) // Do nothing. Each redeclaration should be suppressed separately. NewAttr = nullptr; else if (const auto *RD = dyn_cast(Attr)) diff --git a/clang/test/Parser/cxx-profiles-framework.cpp b/clang/test/Parser/cxx-profiles-framework.cpp index aef559513c14f..a799da89e0514 100644 --- a/clang/test/Parser/cxx-profiles-framework.cpp +++ b/clang/test/Parser/cxx-profiles-framework.cpp @@ -16,6 +16,9 @@ // Nested balanced groups in arguments [[profiles::enforce(nested(config: (a b)))]]; +// Mixed bracket types in balanced groups +[[profiles::enforce(mixed(config: (a [b] c)))]]; + // Bare non-operator-non-punctuator token arguments [[profiles::enforce(bare1(3))]]; diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index c839c5a7aa967..3b4a3e779d465 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -218,3 +218,12 @@ void template_suppress_boundary(T x) { void instantiate_suppress_boundary() { template_suppress_boundary(0); // expected-note {{in instantiation of function template specialization 'template_suppress_boundary' requested here}} } + +// Suppress on forward declaration does not propagate to definition. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] +void suppress_fwd_only(); + +void suppress_fwd_only() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} From a7008b1a1f121b64796b30d638777368574fc36e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 18:59:14 -0400 Subject: [PATCH 025/289] Handle suppress on FieldDecl in template --- clang/lib/Sema/SemaTemplateInstantiate.cpp | 12 ++++++ .../test/SemaCXX/safety-profile-type-cast.cpp | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index 34ed5dffa11b4..a70b968bb90f9 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -3842,8 +3842,20 @@ bool Sema::InstantiateInClassInitializer( ActOnStartCXXInClassMemberInitializer(); CXXThisScopeRAII ThisScope(*this, Instantiation->getParent(), Qualifiers()); + // P3589R2: Push profile suppressions from the pattern field so that + // profile checks during NSDMI instantiation see them. + unsigned ProfileSuppressCount = 0; + if (getLangOpts().Profiles) { + for (const auto *A : Pattern->specific_attrs()) { + pushProfileSuppression(A->getProfileName(), A->getRule()); + ++ProfileSuppressCount; + } + } + ExprResult NewInit = SubstInitializer(OldInit, TemplateArgs, /*CXXDirectInit=*/false); + + popProfileSuppressions(ProfileSuppressCount); Expr *Init = NewInit.get(); assert((!Init || !isa(Init)) && "call-style init in class"); ActOnFinishCXXInClassMemberInitializer( diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 3b4a3e779d465..9034bdd5247c7 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -227,3 +227,45 @@ void suppress_fwd_only(); void suppress_fwd_only() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } + +// Suppress on field NSDMI in class template: suppression must carry through +// when the in-class initializer is instantiated. +template +struct FieldSuppressTemplate { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] T *p = reinterpret_cast(0); +}; +FieldSuppressTemplate field_suppress_inst; + +// Without suppress on field, the NSDMI violation fires during instantiation. +template +struct FieldNoSuppressTemplate { // #FieldNoSuppressTemplate + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FieldNoSuppressTemplate {{in instantiation of default member initializer 'FieldNoSuppressTemplate::p' requested here}} +}; +FieldNoSuppressTemplate field_no_suppress_inst; // expected-note {{in evaluation of exception specification for 'FieldNoSuppressTemplate::FieldNoSuppressTemplate' needed here}} + +// Profile violations fire in constexpr functions. Use a guarded path so the +// function can still produce a constant expression (avoiding the unrelated +// "constexpr function never produces a constant expression" error). +constexpr int *constexpr_cast(bool b) { + if (b) + return reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + return nullptr; +} + +// Profile violations fire in consteval functions. +consteval int *consteval_cast(bool b) { + if (b) + return reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + return nullptr; +} + +// Suppress works inside constexpr functions. +constexpr int *constexpr_suppress_cast(bool b) { + if (b) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] return reinterpret_cast(0); + } + return nullptr; +} From fd2ead17cc8ee7306c2861a39705d50ca881d8b9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Apr 2026 19:12:11 -0400 Subject: [PATCH 026/289] Fix suppress attribute on non-templated FieldDecl --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 13 +++++++++++++ clang/test/SemaCXX/safety-profile-type-cast.cpp | 7 +++++++ 2 files changed, 20 insertions(+) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index bc18881e89110..0747f126af012 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -17,6 +17,7 @@ #include "clang/Sema/DeclSpec.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/Sema.h" #include "llvm/ADT/ScopeExit.h" using namespace clang; @@ -685,9 +686,21 @@ void Parser::ParseLexedMemberInitializer(LateParsedMemberInitializer &MI) { EnterExpressionEvaluationContext Eval( Actions, Sema::ExpressionEvaluationContext::PotentiallyEvaluatedIfUsed); + // P3589R2: Push profile suppressions from the field so that + // profile checks during late-parsed NSDMI see them. + unsigned ProfileSuppressCount = 0; + if (Actions.getLangOpts().Profiles) { + for (const auto *A : MI.Field->specific_attrs()) { + Actions.pushProfileSuppression(A->getProfileName(), A->getRule()); + ++ProfileSuppressCount; + } + } + ExprResult Init = ParseCXXMemberInitializer(MI.Field, /*IsFunction=*/false, EqualLoc); + Actions.popProfileSuppressions(ProfileSuppressCount); + Actions.ActOnFinishCXXInClassMemberInitializer(MI.Field, EqualLoc, Init); // The next token should be our artificial terminating EOF token. diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 9034bdd5247c7..c232f396689b2 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -228,6 +228,13 @@ void suppress_fwd_only() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } +// Suppress on field NSDMI in non-template class: suppression must be +// effective during late-parsing of the in-class initializer. +struct FieldSuppressNonTemplate { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] int *p = reinterpret_cast(0); +}; + // Suppress on field NSDMI in class template: suppression must carry through // when the in-class initializer is instantiated. template From 1f7a50897b50d6322ce50db1209c07e4009ddeaf Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Apr 2026 13:46:25 -0400 Subject: [PATCH 027/289] Add weekly profiles release CI workflows --- .../workflows/schedule-profiles-release.yml | 17 +++ .github/workflows/weekly-profiles-release.yml | 128 ++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 .github/workflows/schedule-profiles-release.yml create mode 100644 .github/workflows/weekly-profiles-release.yml diff --git a/.github/workflows/schedule-profiles-release.yml b/.github/workflows/schedule-profiles-release.yml new file mode 100644 index 0000000000000..19409cb107ad4 --- /dev/null +++ b/.github/workflows/schedule-profiles-release.yml @@ -0,0 +1,17 @@ +name: Schedule Profiles Build + +on: + schedule: + - cron: '0 12 * * 5' + +permissions: + actions: write + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Trigger build on profiles-framework + env: + GH_TOKEN: ${{ github.token }} + run: gh workflow run weekly-profiles-release.yml --repo ${{ github.repository }} --ref profiles-framework diff --git a/.github/workflows/weekly-profiles-release.yml b/.github/workflows/weekly-profiles-release.yml new file mode 100644 index 0000000000000..2b7c98f77d666 --- /dev/null +++ b/.github/workflows/weekly-profiles-release.yml @@ -0,0 +1,128 @@ +name: Weekly Profiles Release + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + build-linux: + name: Build Linux x86_64 + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: sudo apt-get install -y ninja-build ccache + + - name: Restore ccache + uses: actions/cache@v4 + with: + path: ~/.ccache + key: ccache-linux-${{ github.sha }} + restore-keys: ccache-linux- + + - name: Configure + run: | + cmake -G Ninja -S llvm -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=gcc \ + -DCMAKE_CXX_COMPILER=g++ \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \ + -DLLVM_ENABLE_PROJECTS=clang \ + -DLLVM_TARGETS_TO_BUILD="X86;AArch64" \ + -DLLVM_INSTALL_TOOLCHAIN_ONLY=ON \ + -DCMAKE_INSTALL_PREFIX=build/install + + - name: Build and install + run: ninja -C build install-clang install-clang-resource-headers + + - name: Package + run: tar czf clang-profiles-linux-x86_64.tar.gz -C build/install . + + - uses: actions/upload-artifact@v4 + with: + name: clang-profiles-linux-x86_64 + path: clang-profiles-linux-x86_64.tar.gz + + build-windows: + name: Build Windows x86_64 + runs-on: windows-2022 + steps: + - uses: actions/checkout@v4 + + - uses: ilammy/msvc-dev-cmd@v1 + + - name: Restore sccache + uses: actions/cache@v4 + with: + path: ~\AppData\Local\Mozilla\sccache + key: sccache-windows-${{ github.sha }} + restore-keys: sccache-windows- + + - name: Install sccache + run: choco install sccache -y + + - name: Configure + run: > + cmake -G Ninja -S llvm -B build + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_COMPILER=cl + -DCMAKE_CXX_COMPILER=cl + -DCMAKE_C_COMPILER_LAUNCHER=sccache + -DCMAKE_CXX_COMPILER_LAUNCHER=sccache + -DLLVM_ENABLE_PROJECTS=clang + -DLLVM_TARGETS_TO_BUILD="X86;AArch64" + -DLLVM_INSTALL_TOOLCHAIN_ONLY=ON + -DCMAKE_INSTALL_PREFIX=build/install + + - name: Build and install + run: ninja -C build install-clang install-clang-resource-headers + + - name: Package + run: Compress-Archive -Path build\install\* -DestinationPath clang-profiles-windows-x86_64.zip + + - uses: actions/upload-artifact@v4 + with: + name: clang-profiles-windows-x86_64 + path: clang-profiles-windows-x86_64.zip + + create-release: + name: Create GitHub Release + needs: [build-linux, build-windows] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: clang-profiles-linux-x86_64 + path: artifacts + + - uses: actions/download-artifact@v4 + with: + name: clang-profiles-windows-x86_64 + path: artifacts + + - name: Compute tag + id: tag + run: | + date_tag="profiles-$(date -u +%Y-%m-%d)" + echo "tag=${date_tag}-${{ github.run_number }}" >> "$GITHUB_OUTPUT" + echo "name=Profiles Build $(date -u +%Y-%m-%d) #${{ github.run_number }}" >> "$GITHUB_OUTPUT" + + - uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.tag.outputs.tag }} + name: ${{ steps.tag.outputs.name }} + target_commitish: ${{ github.sha }} + files: artifacts/* + body: | + Automated weekly build of Clang with safety profiles support. + + **Branch:** `${{ github.ref_name }}` + **Commit:** `${{ github.sha }}` + + ### Artifacts + - `clang-profiles-linux-x86_64.tar.gz` — Linux x86_64 (Ubuntu 22.04, gcc) + - `clang-profiles-windows-x86_64.zip` — Windows x86_64 (MSVC 2022) From 236510df8e376771a0705caba00f2f567c24fc84 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Apr 2026 17:16:18 -0400 Subject: [PATCH 028/289] Remove sccache from Windows CI build --- .github/workflows/weekly-profiles-release.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.github/workflows/weekly-profiles-release.yml b/.github/workflows/weekly-profiles-release.yml index 2b7c98f77d666..af801aeda60bb 100644 --- a/.github/workflows/weekly-profiles-release.yml +++ b/.github/workflows/weekly-profiles-release.yml @@ -55,24 +55,12 @@ jobs: - uses: ilammy/msvc-dev-cmd@v1 - - name: Restore sccache - uses: actions/cache@v4 - with: - path: ~\AppData\Local\Mozilla\sccache - key: sccache-windows-${{ github.sha }} - restore-keys: sccache-windows- - - - name: Install sccache - run: choco install sccache -y - - name: Configure run: > cmake -G Ninja -S llvm -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=cl -DCMAKE_CXX_COMPILER=cl - -DCMAKE_C_COMPILER_LAUNCHER=sccache - -DCMAKE_CXX_COMPILER_LAUNCHER=sccache -DLLVM_ENABLE_PROJECTS=clang -DLLVM_TARGETS_TO_BUILD="X86;AArch64" -DLLVM_INSTALL_TOOLCHAIN_ONLY=ON From 8827272875eaf86482e912d530efcff65dfb3731 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 10 Apr 2026 10:19:00 -0400 Subject: [PATCH 029/289] Refactor argument access --- clang/include/clang/Sema/Sema.h | 8 +++++++ clang/lib/Sema/Sema.cpp | 38 ++++++++++++++++----------------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 548677d68585b..d5a7770a121f7 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1058,6 +1058,14 @@ class Sema final : public SemaBase { SmallVectorImpl *NewNames, SmallVectorImpl *NewDesignators); + struct ParsedProfileSuppressInfo { + StringRef ProfileName; + StringRef Justification; + StringRef Rule; + }; + std::optional + getProfileSuppressInfo(const ParsedAttr &AL); + ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); void pushProfileSuppression(StringRef ProfileName, StringRef RuleName); diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 970ddaf7ddad5..3d949695457f7 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3040,16 +3040,22 @@ bool Sema::processProfilesEnforceAttr( return true; } -ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { - StringRef ProfileName; - if (!checkStringLiteralArgumentAttr(AL, 0, ProfileName)) - return nullptr; - - StringRef Justification, Rule; +std::optional +Sema::getProfileSuppressInfo(const ParsedAttr &AL) { + ParsedProfileSuppressInfo Info; + if (!checkStringLiteralArgumentAttr(AL, 0, Info.ProfileName)) + return std::nullopt; if (AL.getNumArgs() >= 2) - checkStringLiteralArgumentAttr(AL, 1, Justification); + checkStringLiteralArgumentAttr(AL, 1, Info.Justification); if (AL.getNumArgs() >= 3) - checkStringLiteralArgumentAttr(AL, 2, Rule); + checkStringLiteralArgumentAttr(AL, 2, Info.Rule); + return Info; +} + +ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { + auto Info = getProfileSuppressInfo(AL); + if (!Info) + return nullptr; SmallVector RawArgs; for (unsigned I = 3; I < AL.getNumArgs(); ++I) { @@ -3059,8 +3065,8 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { } return ::new (Context) ProfilesSuppressAttr( - Context, AL, ProfileName, Justification, Rule, RawArgs.data(), - RawArgs.size()); + Context, AL, Info->ProfileName, Info->Justification, Info->Rule, + RawArgs.data(), RawArgs.size()); } void Sema::pushProfileSuppression(StringRef ProfileName, StringRef RuleName) { @@ -3132,16 +3138,8 @@ Sema::ProfileSuppressRAII::ProfileSuppressRAII( for (const auto &AL : Attrs) { if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) continue; - if (AL.getNumArgs() < 1) - continue; - if (const auto *SL = - dyn_cast(AL.getArgAsExpr(0))) { - StringRef Rule; - if (AL.getNumArgs() >= 3) - if (const auto *RuleSL = - dyn_cast(AL.getArgAsExpr(2))) - Rule = RuleSL->getString(); - S.pushProfileSuppression(SL->getString(), Rule); + if (auto Info = S.getProfileSuppressInfo(AL)) { + S.pushProfileSuppression(Info->ProfileName, Info->Rule); ++Count; } } From 2e580afa909e93f5ced6cec768fe2fd2d943b494 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 10 Apr 2026 13:34:45 -0400 Subject: [PATCH 030/289] Diagnose enforce on empty-declaration at class scope --- clang/lib/Sema/SemaDecl.cpp | 3 +++ clang/test/SemaCXX/safety-profile-framework.cpp | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 84ea75caa4e49..1de6f8639cefb 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -5409,6 +5409,9 @@ Decl *Sema::ParsedFreeStandingDeclSpec(Scope *S, AccessSpecifier AS, ? diag::err_no_declarators : diag::ext_no_declarators) << DS.getSourceRange(); + for (const ParsedAttr &AL : DeclAttrs) + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) + Diag(AL.getLoc(), diag::err_profiles_enforce_not_at_tu_scope); return TagD; } diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index 63f06229a540a..066ec0752d501 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -38,7 +38,8 @@ void enforced_func(); // Enforce inside class scope: error // =================================================================== struct EnforceInClass { - [[profiles::enforce(test::type_cast)]]; // expected-warning {{declaration does not declare anything}} + [[profiles::enforce(test::type_cast)]]; // expected-warning {{declaration does not declare anything}} \ + // expected-error {{'profiles::enforce' attribute on empty-declaration must be at translation unit scope}} }; // =================================================================== From e4ff2edcad92cd4e644ed6ef849b47240a31b76b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 18:55:59 -0400 Subject: [PATCH 031/289] Do not restrict subjects of suppress --- clang/include/clang/Basic/Attr.td | 3 --- clang/test/SemaCXX/safety-profile-framework.cpp | 7 +++++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 5b3b56f2d2975..d016ec835fbe2 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -3479,9 +3479,6 @@ def ProfilesSuppress : DeclOrStmtAttr { ]; let LangOpts = [Profiles]; let HasCustomParsing = 1; - let Subjects = SubjectList<[ - Stmt, Var, Field, Function, Record, Namespace, Empty - ], ErrorDiag, "variables, functions, structs, and namespaces">; let Documentation = [ProfilesSuppressDocs]; } diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index 066ec0752d501..40501786b1b17 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -91,3 +91,10 @@ void test_block_scope() { void test_enforced_profile_errors() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } + +// =================================================================== +// Suppress on additional declaration kinds +// =================================================================== +enum [[profiles::suppress(test::type_cast)]] SuppressedEnum { SE_A, SE_B }; + +using SuppressedAlias [[profiles::suppress(test::type_cast)]] = int; From 719eb71248933e646d69ced6769c9bcc0210f2e5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 19:02:08 -0400 Subject: [PATCH 032/289] Fix suppress attribute in declarator --- clang/lib/Parse/ParseDecl.cpp | 11 +++++++++++ clang/test/SemaCXX/safety-profile-type-cast.cpp | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index fe64589d69060..0281a8f87b996 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -2585,6 +2585,15 @@ Decl *Parser::ParseDeclarationAfterDeclaratorAndAttributes( SemaCUDA::CUDATargetContextRAII X(Actions.CUDA(), SemaCUDA::CTCK_InitGlobalVar, ThisDecl); + + unsigned ProfileSuppressCount = 0; + if (Actions.getLangOpts().Profiles && ThisDecl) { + for (const auto *A : ThisDecl->specific_attrs()) { + Actions.pushProfileSuppression(A->getProfileName(), A->getRule()); + ++ProfileSuppressCount; + } + } + switch (TheInitKind) { // Parse declarator '=' initializer. case InitKind::Equal: { @@ -2722,6 +2731,8 @@ Decl *Parser::ParseDeclarationAfterDeclaratorAndAttributes( } } + Actions.popProfileSuppressions(ProfileSuppressCount); + Actions.FinalizeDeclaration(ThisDecl); return OuterDecl ? OuterDecl : ThisDecl; } diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index c232f396689b2..0be8e1a4dafc9 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -276,3 +276,14 @@ constexpr int *constexpr_suppress_cast(bool b) { } return nullptr; } + +// Trailing declarator-position suppress on TU-scope variable initializer. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +int *trailing_tu_var [[profiles::suppress(test::type_cast)]] = reinterpret_cast(0); + +// Trailing declarator-position suppress on block-scope variable initializer. +void test_trailing_suppress_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + int *p [[profiles::suppress(test::type_cast)]] = reinterpret_cast(0); + int *q = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} From c658c13c148eaddb489d4a795118968773780032 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 19:07:00 -0400 Subject: [PATCH 033/289] Fix enforce in global module fragment --- clang/lib/Sema/Sema.cpp | 8 ++++++-- .../safety-profile-framework-modules.cppm | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 3d949695457f7..bb81c321c6aaa 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3028,9 +3028,13 @@ bool Sema::processProfilesEnforceAttr( if (!addProfileEnforcement(Name, Desig, AL.getLoc())) continue; + if (Mod && !llvm::any_of(Mod->EnforcedProfileDesignators, + [&](const Module::EnforcedProfile &EP) { + return EP.ProfileName == Name; + })) + Mod->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); + if (IsNew) { - if (Mod) - Mod->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); if (NewNames) NewNames->push_back(Name); if (NewDesignators) diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 7ec7e0683e3f5..551ec589daaf1 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -8,6 +8,8 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_mismatch.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_repeated.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/impl_propagation.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_enforce.cppm -o %t/mod_gmf_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_ok.cpp -fmodule-file=GmfMod=%t/mod_gmf_enforce.pcm -verify // =================================================================== // Module with enforced profiles @@ -56,3 +58,19 @@ module TestMod; void impl_func() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } + +// =================================================================== +// Module with GMF enforce preceding module-declaration enforce: +// require must still find the profile on the module. +// =================================================================== +//--- mod_gmf_enforce.cppm +// expected-no-diagnostics +module; +[[profiles::enforce(test::type_cast)]]; +export module GmfMod [[profiles::enforce(test::type_cast)]]; + +export void gmf_func(); + +//--- require_gmf_ok.cpp +// expected-no-diagnostics +import GmfMod [[profiles::require(test::type_cast)]]; From 25f3e02aff8e4d19397fb49466e71be11892883c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 20:00:04 -0400 Subject: [PATCH 034/289] Refactor argument access --- clang/include/clang/Parse/Parser.h | 6 +- clang/include/clang/Sema/ParsedAttr.h | 52 +++++++++++++ clang/include/clang/Sema/Sema.h | 8 -- clang/lib/Parse/ParseDeclCXX.cpp | 102 ++++++++++---------------- clang/lib/Sema/Sema.cpp | 50 +++++-------- clang/lib/Sema/SemaDeclAttr.cpp | 3 - clang/lib/Sema/SemaModule.cpp | 8 +- clang/lib/Sema/SemaStmtAttr.cpp | 3 - 8 files changed, 114 insertions(+), 118 deletions(-) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index 8e47d81c3c591..5ed477b045eba 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2281,9 +2281,9 @@ class Parser : public CodeCompletionHandler { IdentifierInfo *ScopeName, SourceLocation ScopeLoc); bool ParseProfileName(std::string &Name); - bool ParseProfileDesignator(std::string &Name, std::string &Designator); - bool ParseProfileDesignatorList(SmallVectorImpl &Names, - SmallVectorImpl &Designators); + bool ParseProfileDesignator(detail::ProfileDesignator &Designator); + bool ParseProfileDesignatorList( + SmallVectorImpl &Designators); bool ParseProfileArgumentList(SmallVectorImpl &Args); bool ParseNonCommaBalancedToken(std::string &Spelling); bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling); diff --git a/clang/include/clang/Sema/ParsedAttr.h b/clang/include/clang/Sema/ParsedAttr.h index 5387f9fad6cd2..ed971127a9f4a 100644 --- a/clang/include/clang/Sema/ParsedAttr.h +++ b/clang/include/clang/Sema/ParsedAttr.h @@ -28,6 +28,7 @@ #include #include #include +#include #include namespace clang { @@ -96,6 +97,26 @@ struct PropertyData { : GetterId(getterId), SetterId(setterId) {} }; +struct ProfileDesignator { + std::string Name; + std::string Spelling; +}; + +struct ProfileEnforceArgs { + SmallVector Designators; +}; + +struct ProfileSuppressArgs { + std::string ProfileName; + std::string Justification; + std::string Rule; + SmallVector RawArguments; +}; + +struct ProfileRequireArgs { + ProfileDesignator Designator; +}; + } // namespace detail /// A union of the various pointer types that can be passed to an @@ -184,6 +205,8 @@ class ParsedAttr final const Expr *MessageExpr; + void *CustomData = nullptr; + const ParsedAttrInfo &Info; ArgsUnion *getArgsBuffer() { return getTrailingObjects(); } @@ -473,6 +496,30 @@ class ParsedAttr final return getPropertyDataBuffer().SetterId; } + void setCustomData(void *Data) { CustomData = Data; } + + template T &getCustomData() { + assert(CustomData && "No custom data set"); + return *static_cast(CustomData); + } + + template const T &getCustomData() const { + assert(CustomData && "No custom data set"); + return *static_cast(CustomData); + } + + const detail::ProfileEnforceArgs &getProfileEnforceArgs() const { + return getCustomData(); + } + + const detail::ProfileSuppressArgs &getProfileSuppressArgs() const { + return getCustomData(); + } + + const detail::ProfileRequireArgs &getProfileRequireArgs() const { + return getCustomData(); + } + /// Set the macro identifier info object that this parsed attribute was /// declared in if it was declared in a macro. Also set the expansion location /// of the macro. @@ -717,6 +764,11 @@ class AttributePool { AttributeFactory &getFactory() const { return Factory; } + template T *make(Args &&...args) { + void *Mem = Factory.Alloc.Allocate(sizeof(T), alignof(T)); + return ::new (Mem) T(std::forward(args)...); + } + void clear() { Factory.reclaimPool(*this); Attrs.clear(); diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index d5a7770a121f7..548677d68585b 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1058,14 +1058,6 @@ class Sema final : public SemaBase { SmallVectorImpl *NewNames, SmallVectorImpl *NewDesignators); - struct ParsedProfileSuppressInfo { - StringRef ProfileName; - StringRef Justification; - StringRef Rule; - }; - std::optional - getProfileSuppressInfo(const ParsedAttr &AL); - ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); void pushProfileSuppression(StringRef ProfileName, StringRef RuleName); diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 80f7fa6340eaa..32b43be7855ef 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5036,16 +5036,6 @@ void Parser::ParseMicrosoftIfExistsClassDeclaration( // C++ Profiles framework (P3589R2) //===----------------------------------------------------------------------===// -// Profile names may contain '::' (e.g. "std::type"), so IdentifierLoc is not -// suitable. We synthesize StringLiteral nodes to pass them through ArgsUnion. -static Expr *MakeProfileStringLiteral(Parser &P, StringRef Str, - SourceLocation Loc) { - ASTContext &Ctx = P.getActions().Context; - QualType StrTy = Ctx.getStringLiteralArrayType(Ctx.CharTy, Str.size()); - return StringLiteral::Create(Ctx, Str, StringLiteralKind::Ordinary, false, - StrTy, {Loc}); -} - bool Parser::ParseProfileName(std::string &Name) { if (!Tok.is(tok::identifier)) { Diag(Tok, diag::err_profiles_expected_profile_name); @@ -5141,17 +5131,16 @@ bool Parser::ParseProfileArgumentList(SmallVectorImpl &Args) { return false; } -bool Parser::ParseProfileDesignator(std::string &Name, - std::string &Designator) { - if (ParseProfileName(Name)) +bool Parser::ParseProfileDesignator(detail::ProfileDesignator &D) { + if (ParseProfileName(D.Name)) return true; - Designator = Name; + D.Spelling = D.Name; if (!Tok.is(tok::l_paren)) return false; - Designator += "("; + D.Spelling += "("; ConsumeParen(); SmallVector Args; @@ -5160,28 +5149,26 @@ bool Parser::ParseProfileDesignator(std::string &Name, for (unsigned I = 0; I < Args.size(); ++I) { if (I > 0) - Designator += ", "; - Designator += Args[I]; + D.Spelling += ", "; + D.Spelling += Args[I]; } if (!Tok.is(tok::r_paren)) { Diag(Tok, diag::err_expected) << tok::r_paren; return true; } - Designator += ")"; + D.Spelling += ")"; ConsumeParen(); return false; } bool Parser::ParseProfileDesignatorList( - SmallVectorImpl &Names, - SmallVectorImpl &Designators) { + SmallVectorImpl &Designators) { while (true) { - std::string Name, Designator; - if (ParseProfileDesignator(Name, Designator)) + detail::ProfileDesignator D; + if (ParseProfileDesignator(D)) return true; - Names.push_back(std::move(Name)); - Designators.push_back(std::move(Designator)); + Designators.push_back(std::move(D)); if (!TryConsumeToken(tok::comma)) break; @@ -5197,6 +5184,8 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, SourceLocation ScopeLoc) { assert(ScopeName && ScopeName->isStr("profiles")); + AttributePool &Pool = Attrs.getPool(); + auto SkipToRParen = [&]() { SkipUntil(tok::r_paren, StopAtSemi | StopBeforeMatch); if (Tok.is(tok::r_paren)) @@ -5206,8 +5195,8 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, if (AttrName->isStr("enforce")) { ConsumeParen(); - SmallVector Names, Designators; - if (ParseProfileDesignatorList(Names, Designators)) { + auto *Args = Pool.make(); + if (ParseProfileDesignatorList(Args->Designators)) { SkipToRParen(); return true; } @@ -5222,30 +5211,23 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, if (EndLoc) *EndLoc = RParen; - SmallVector ArgExprs; - for (unsigned I = 0; I < Names.size(); ++I) { - ArgExprs.push_back(MakeProfileStringLiteral(*this, Names[I], AttrNameLoc)); - ArgExprs.push_back( - MakeProfileStringLiteral(*this, Designators[I], AttrNameLoc)); - } - Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), - AttributeScopeInfo(ScopeName, ScopeLoc), ArgExprs.data(), - ArgExprs.size(), ParsedAttr::Form::CXX11()); + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(Args); return true; } if (AttrName->isStr("suppress")) { ConsumeParen(); - std::string ProfileName; - if (ParseProfileName(ProfileName)) { + auto *Args = Pool.make(); + if (ParseProfileName(Args->ProfileName)) { SkipToRParen(); return true; } - std::string Justification, Rule; - SmallVector RawArgs; - auto ParseStringLiteralValue = [&](std::string &Out) -> bool { SmallVector StringToks; do { @@ -5271,18 +5253,18 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, SkipToRParen(); return true; } - if (ParseStringLiteralValue(Justification)) { + if (ParseStringLiteralValue(Args->Justification)) { SkipToRParen(); return true; } } else if (KeyII->isStr("rule")) { if (tok::isStringLiteral(Tok.getKind())) { - if (ParseStringLiteralValue(Rule)) { + if (ParseStringLiteralValue(Args->Rule)) { SkipToRParen(); return true; } } else { - if (ParseNonCommaBalancedToken(Rule)) { + if (ParseNonCommaBalancedToken(Args->Rule)) { SkipToRParen(); return true; } @@ -5293,7 +5275,7 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, SkipToRParen(); return true; } - RawArgs.push_back(KeyII->getName().str() + " : " + Value); + Args->RawArguments.push_back(KeyII->getName().str() + " : " + Value); } } else { std::string Spelling; @@ -5301,7 +5283,7 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, SkipToRParen(); return true; } - RawArgs.push_back(std::move(Spelling)); + Args->RawArguments.push_back(std::move(Spelling)); } } @@ -5315,26 +5297,19 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, if (EndLoc) *EndLoc = RParen; - SmallVector ArgExprs; - ArgExprs.push_back( - MakeProfileStringLiteral(*this, ProfileName, AttrNameLoc)); - ArgExprs.push_back( - MakeProfileStringLiteral(*this, Justification, AttrNameLoc)); - ArgExprs.push_back(MakeProfileStringLiteral(*this, Rule, AttrNameLoc)); - for (const auto &Arg : RawArgs) - ArgExprs.push_back(MakeProfileStringLiteral(*this, Arg, AttrNameLoc)); - - Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), - AttributeScopeInfo(ScopeName, ScopeLoc), ArgExprs.data(), - ArgExprs.size(), ParsedAttr::Form::CXX11()); + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(Args); return true; } if (AttrName->isStr("require")) { ConsumeParen(); - std::string Name, Designator; - if (ParseProfileDesignator(Name, Designator)) { + auto *Args = Pool.make(); + if (ParseProfileDesignator(Args->Designator)) { SkipToRParen(); return true; } @@ -5349,10 +5324,11 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, if (EndLoc) *EndLoc = RParen; - ArgsUnion Arg = MakeProfileStringLiteral(*this, Designator, AttrNameLoc); - Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), - AttributeScopeInfo(ScopeName, ScopeLoc), &Arg, 1, - ParsedAttr::Form::CXX11()); + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(Args); return true; } diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index bb81c321c6aaa..129f43a480780 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3014,62 +3014,47 @@ bool Sema::processProfilesEnforceAttr( const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, SmallVectorImpl *NewDesignators) { - if (!AL.checkAtLeastNumArgs(*this, 2)) + const auto &Args = AL.getProfileEnforceArgs(); + if (Args.Designators.empty()) { + Diag(AL.getLoc(), diag::err_attribute_too_few_arguments) << AL << 1; return false; + } - unsigned NumDesignators = AL.getNumArgs() / 2; - for (unsigned I = 0; I < NumDesignators; ++I) { - StringRef Name, Desig; - if (!checkStringLiteralArgumentAttr(AL, I * 2, Name) || - !checkStringLiteralArgumentAttr(AL, I * 2 + 1, Desig)) - continue; + for (const auto &D : Args.Designators) { + StringRef Name = D.Name; + StringRef Spelling = D.Spelling; bool IsNew = !isProfileEnforced(Name); - if (!addProfileEnforcement(Name, Desig, AL.getLoc())) + if (!addProfileEnforcement(Name, Spelling, AL.getLoc())) continue; if (Mod && !llvm::any_of(Mod->EnforcedProfileDesignators, [&](const Module::EnforcedProfile &EP) { return EP.ProfileName == Name; })) - Mod->EnforcedProfileDesignators.push_back({Name.str(), Desig.str()}); + Mod->EnforcedProfileDesignators.push_back({Name.str(), Spelling.str()}); if (IsNew) { if (NewNames) NewNames->push_back(Name); if (NewDesignators) - NewDesignators->push_back(Desig); + NewDesignators->push_back(Spelling); } } return true; } -std::optional -Sema::getProfileSuppressInfo(const ParsedAttr &AL) { - ParsedProfileSuppressInfo Info; - if (!checkStringLiteralArgumentAttr(AL, 0, Info.ProfileName)) - return std::nullopt; - if (AL.getNumArgs() >= 2) - checkStringLiteralArgumentAttr(AL, 1, Info.Justification); - if (AL.getNumArgs() >= 3) - checkStringLiteralArgumentAttr(AL, 2, Info.Rule); - return Info; -} - ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { - auto Info = getProfileSuppressInfo(AL); - if (!Info) + const auto &Args = AL.getProfileSuppressArgs(); + if (Args.ProfileName.empty()) return nullptr; SmallVector RawArgs; - for (unsigned I = 3; I < AL.getNumArgs(); ++I) { - StringRef Arg; - if (checkStringLiteralArgumentAttr(AL, I, Arg)) - RawArgs.push_back(Arg); - } + for (const auto &Arg : Args.RawArguments) + RawArgs.push_back(Arg); return ::new (Context) ProfilesSuppressAttr( - Context, AL, Info->ProfileName, Info->Justification, Info->Rule, + Context, AL, Args.ProfileName, Args.Justification, Args.Rule, RawArgs.data(), RawArgs.size()); } @@ -3142,8 +3127,9 @@ Sema::ProfileSuppressRAII::ProfileSuppressRAII( for (const auto &AL : Attrs) { if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) continue; - if (auto Info = S.getProfileSuppressInfo(AL)) { - S.pushProfileSuppression(Info->ProfileName, Info->Rule); + const auto &Args = AL.getProfileSuppressArgs(); + if (!Args.ProfileName.empty()) { + S.pushProfileSuppression(Args.ProfileName, Args.Rule); ++Count; } } diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index ffd3a0d74149d..c351ec88d8f8f 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5493,9 +5493,6 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, const ParsedAttr &AL) { - if (!AL.checkAtLeastNumArgs(S, 1)) - return; - if (auto *A = S.makeProfilesSuppressAttr(AL)) D->addAttr(A); } diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index e1556162f5fb6..0347a7ba08521 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -1633,17 +1633,13 @@ void Sema::ActOnModuleImportAttrs(Decl *D, for (const auto &AL : Attrs) { if (AL.getKind() == ParsedAttr::AT_ProfilesRequire) { - if (!AL.checkAtLeastNumArgs(*this, 1)) - continue; - if (!ImportedMod) { Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); continue; } - StringRef Desig; - if (!checkStringLiteralArgumentAttr(AL, 0, Desig)) - continue; + const auto &Args = AL.getProfileRequireArgs(); + StringRef Desig = Args.Designator.Spelling; bool Found = llvm::any_of( ImportedMod->EnforcedProfileDesignators, diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp index a404309ead620..620b06c4c102a 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -74,9 +74,6 @@ static Attr *handleSuppressAttr(Sema &S, Stmt *St, const ParsedAttr &A, static Attr *handleProfilesSuppressStmtAttr(Sema &S, Stmt *St, const ParsedAttr &A, SourceRange Range) { - if (!A.checkAtLeastNumArgs(S, 1)) - return nullptr; - return S.makeProfilesSuppressAttr(A); } From 32c7773c70a61abaee74edc413b3928cee1172ec Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 20:19:48 -0400 Subject: [PATCH 035/289] Refactor profile suppression --- clang/include/clang/Sema/Sema.h | 17 ++---- clang/lib/Parse/ParseCXXInlineMethods.cpp | 12 +--- clang/lib/Parse/ParseDecl.cpp | 12 +--- clang/lib/Parse/ParseStmt.cpp | 2 +- clang/lib/Sema/Sema.cpp | 64 ++++++++++++---------- clang/lib/Sema/SemaTemplateInstantiate.cpp | 12 +--- clang/lib/Sema/TreeTransform.h | 27 +-------- 7 files changed, 49 insertions(+), 97 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 548677d68585b..3f5dbfd14de45 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1060,25 +1060,20 @@ class Sema final : public SemaBase { ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); - void pushProfileSuppression(StringRef ProfileName, StringRef RuleName); - void popProfileSuppressions(unsigned Count); - - bool isProfileSuppressedByStmt(StringRef ProfileName, - StringRef RuleName = "") const; - bool isProfileSuppressedByDeclAttr(StringRef ProfileName, - StringRef RuleName = "") const; bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName = "") const; bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); - class ProfileSuppressRAII { + class ProfileSuppressScope { Sema &S; - unsigned Count; + unsigned Count = 0; public: - ProfileSuppressRAII(Sema &S, const ParsedAttributesView &Attrs); - ~ProfileSuppressRAII(); + ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); + ProfileSuppressScope(Sema &S, const Decl *D); + ProfileSuppressScope(Sema &S, ArrayRef Attrs); + ~ProfileSuppressScope(); }; /// Determines the active Scope associated with the given declaration diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index 0747f126af012..5b135010f813a 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -686,21 +686,11 @@ void Parser::ParseLexedMemberInitializer(LateParsedMemberInitializer &MI) { EnterExpressionEvaluationContext Eval( Actions, Sema::ExpressionEvaluationContext::PotentiallyEvaluatedIfUsed); - // P3589R2: Push profile suppressions from the field so that - // profile checks during late-parsed NSDMI see them. - unsigned ProfileSuppressCount = 0; - if (Actions.getLangOpts().Profiles) { - for (const auto *A : MI.Field->specific_attrs()) { - Actions.pushProfileSuppression(A->getProfileName(), A->getRule()); - ++ProfileSuppressCount; - } - } + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, MI.Field); ExprResult Init = ParseCXXMemberInitializer(MI.Field, /*IsFunction=*/false, EqualLoc); - Actions.popProfileSuppressions(ProfileSuppressCount); - Actions.ActOnFinishCXXInClassMemberInitializer(MI.Field, EqualLoc, Init); // The next token should be our artificial terminating EOF token. diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index 0281a8f87b996..11615c39c2c68 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -2137,7 +2137,7 @@ Parser::DeclGroupPtrTy Parser::ParseDeclGroup(ParsingDeclSpec &DS, ParsedAttributes LocalAttrs(AttrFactory); LocalAttrs.takeAllPrependingFrom(Attrs); - Sema::ProfileSuppressRAII ProfileSuppressGuard(Actions, LocalAttrs); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, LocalAttrs); ParsingDeclarator D(*this, DS, LocalAttrs, Context); if (TemplateInfo.TemplateParams) @@ -2586,13 +2586,7 @@ Decl *Parser::ParseDeclarationAfterDeclaratorAndAttributes( SemaCUDA::CUDATargetContextRAII X(Actions.CUDA(), SemaCUDA::CTCK_InitGlobalVar, ThisDecl); - unsigned ProfileSuppressCount = 0; - if (Actions.getLangOpts().Profiles && ThisDecl) { - for (const auto *A : ThisDecl->specific_attrs()) { - Actions.pushProfileSuppression(A->getProfileName(), A->getRule()); - ++ProfileSuppressCount; - } - } + Sema::ProfileSuppressScope ProfileSuppressForInit(Actions, ThisDecl); switch (TheInitKind) { // Parse declarator '=' initializer. @@ -2731,8 +2725,6 @@ Decl *Parser::ParseDeclarationAfterDeclaratorAndAttributes( } } - Actions.popProfileSuppressions(ProfileSuppressCount); - Actions.FinalizeDeclaration(ThisDecl); return OuterDecl ? OuterDecl : ThisDecl; } diff --git a/clang/lib/Parse/ParseStmt.cpp b/clang/lib/Parse/ParseStmt.cpp index e148891813efe..3c3d3a91ed88a 100644 --- a/clang/lib/Parse/ParseStmt.cpp +++ b/clang/lib/Parse/ParseStmt.cpp @@ -76,7 +76,7 @@ StmtResult Parser::ParseStatementOrDeclaration(StmtVector &Stmts, if (getLangOpts().HLSL) MaybeParseMicrosoftAttributes(GNUOrMSAttrs); - Sema::ProfileSuppressRAII ProfileSuppressGuard(Actions, CXX11Attrs); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, CXX11Attrs); StmtResult Res = ParseStatementOrDeclarationAfterAttributes( Stmts, StmtCtx, TrailingElseLoc, CXX11Attrs, GNUOrMSAttrs, diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 129f43a480780..c2585f8bc29cd 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3058,29 +3058,14 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { RawArgs.data(), RawArgs.size()); } -void Sema::pushProfileSuppression(StringRef ProfileName, StringRef RuleName) { - ProfileSuppressStack.push_back( - {ProfileName.str(), RuleName.str()}); -} - -void Sema::popProfileSuppressions(unsigned Count) { - assert(ProfileSuppressStack.size() >= Count); - ProfileSuppressStack.pop_back_n(Count); -} - -bool Sema::isProfileSuppressedByStmt(StringRef ProfileName, - StringRef RuleName) const { +bool Sema::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName) const { for (const auto &E : ProfileSuppressStack) { if (E.ProfileName != ProfileName) continue; if (E.RuleName.empty() || E.RuleName == RuleName) return true; } - return false; -} - -bool Sema::isProfileSuppressedByDeclAttr(StringRef ProfileName, - StringRef RuleName) const { for (const DeclContext *DC = CurContext; DC; DC = DC->getParent()) { if (const auto *D = dyn_cast(DC)) { for (const auto *A : D->specific_attrs()) { @@ -3094,12 +3079,6 @@ bool Sema::isProfileSuppressedByDeclAttr(StringRef ProfileName, return false; } -bool Sema::isProfileSuppressed(StringRef ProfileName, - StringRef RuleName) const { - return isProfileSuppressedByStmt(ProfileName, RuleName) || - isProfileSuppressedByDeclAttr(ProfileName, RuleName); -} - bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID) { if (!isProfileEnforced(ProfileName)) @@ -3121,20 +3100,49 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, return true; } -Sema::ProfileSuppressRAII::ProfileSuppressRAII( +Sema::ProfileSuppressScope::ProfileSuppressScope( Sema &S, const ParsedAttributesView &Attrs) - : S(S), Count(0) { + : S(S) { + if (!S.getLangOpts().Profiles) + return; for (const auto &AL : Attrs) { if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) continue; const auto &Args = AL.getProfileSuppressArgs(); if (!Args.ProfileName.empty()) { - S.pushProfileSuppression(Args.ProfileName, Args.Rule); + S.ProfileSuppressStack.push_back( + {Args.ProfileName, Args.Rule}); + ++Count; + } + } +} + +Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D) + : S(S) { + if (!S.getLangOpts().Profiles || !D) + return; + for (const auto *A : D->specific_attrs()) { + S.ProfileSuppressStack.push_back( + {A->getProfileName().str(), A->getRule().str()}); + ++Count; + } +} + +Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, + ArrayRef Attrs) + : S(S) { + if (!S.getLangOpts().Profiles) + return; + for (const auto *A : Attrs) { + if (const auto *PSA = dyn_cast(A)) { + S.ProfileSuppressStack.push_back( + {PSA->getProfileName().str(), PSA->getRule().str()}); ++Count; } } } -Sema::ProfileSuppressRAII::~ProfileSuppressRAII() { - S.popProfileSuppressions(Count); +Sema::ProfileSuppressScope::~ProfileSuppressScope() { + assert(S.ProfileSuppressStack.size() >= Count); + S.ProfileSuppressStack.pop_back_n(Count); } diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index a70b968bb90f9..06ccce4517f16 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -3842,20 +3842,10 @@ bool Sema::InstantiateInClassInitializer( ActOnStartCXXInClassMemberInitializer(); CXXThisScopeRAII ThisScope(*this, Instantiation->getParent(), Qualifiers()); - // P3589R2: Push profile suppressions from the pattern field so that - // profile checks during NSDMI instantiation see them. - unsigned ProfileSuppressCount = 0; - if (getLangOpts().Profiles) { - for (const auto *A : Pattern->specific_attrs()) { - pushProfileSuppression(A->getProfileName(), A->getRule()); - ++ProfileSuppressCount; - } - } + ProfileSuppressScope ProfileSuppressGuard(*this, Pattern); ExprResult NewInit = SubstInitializer(OldInit, TemplateArgs, /*CXXDirectInit=*/false); - - popProfileSuppressions(ProfileSuppressCount); Expr *Init = NewInit.get(); assert((!Init || !isa(Init)) && "call-style init in class"); ActOnFinishCXXInClassMemberInitializer( diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h index 90923ab2fb36d..0f7f20e460a0b 100644 --- a/clang/lib/Sema/TreeTransform.h +++ b/clang/lib/Sema/TreeTransform.h @@ -8311,23 +8311,10 @@ template StmtResult TreeTransform::TransformAttributedStmt(AttributedStmt *S, StmtDiscardKind SDK) { - // P3589R2: Push profile suppressions BEFORE transforming the sub-statement - // so that profile violation checks during template instantiation see them. - unsigned ProfileSuppressCount = 0; - if (getSema().getLangOpts().Profiles) { - for (const auto *A : S->getAttrs()) { - if (const auto *PSA = dyn_cast(A)) { - getSema().pushProfileSuppression(PSA->getProfileName(), - PSA->getRule()); - ++ProfileSuppressCount; - } - } - } + Sema::ProfileSuppressScope ProfileSuppressGuard(getSema(), S->getAttrs()); StmtResult SubStmt = getDerived().TransformStmt(S->getSubStmt(), SDK); - getSema().popProfileSuppressions(ProfileSuppressCount); - if (SubStmt.isInvalid()) return StmtError(); @@ -8665,20 +8652,10 @@ TreeTransform::TransformDeclStmt(DeclStmt *S) { SmallVector Decls; LambdaScopeInfo *LSI = getSema().getCurLambda(); for (auto *D : S->decls()) { - // P3589R2: Push profile suppressions from decl attrs so that - // initializer checks during template instantiation see them. - unsigned ProfileSuppressCount = 0; - if (getSema().getLangOpts().Profiles) { - for (const auto *A : D->specific_attrs()) { - getSema().pushProfileSuppression(A->getProfileName(), A->getRule()); - ++ProfileSuppressCount; - } - } + Sema::ProfileSuppressScope ProfileSuppressGuard(getSema(), D); Decl *Transformed = getDerived().TransformDefinition(D->getLocation(), D); - getSema().popProfileSuppressions(ProfileSuppressCount); - if (!Transformed) return StmtError(); From cc73506e26661e4a41498368ec313ab01ab8d548 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 20:28:16 -0400 Subject: [PATCH 036/289] Fix profile supppression in static data members and variable templates --- clang/lib/Sema/SemaTemplateInstantiateDecl.cpp | 1 + .../test/SemaCXX/safety-profile-type-cast.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index cc24e03e77c07..d746fa36a0d4b 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -6226,6 +6226,7 @@ void Sema::InstantiateVariableInitializer( Var->setImplicitlyInline(); ContextRAII SwitchContext(*this, Var->getDeclContext()); + ProfileSuppressScope ProfileSuppressGuard(*this, OldVar); EnterExpressionEvaluationContext Evaluated( *this, Sema::ExpressionEvaluationContext::PotentiallyEvaluated, Var, diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 0be8e1a4dafc9..b6b09f2f83347 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -287,3 +287,21 @@ void test_trailing_suppress_block() { int *p [[profiles::suppress(test::type_cast)]] = reinterpret_cast(0); int *q = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } + +// Suppress on variable template: suppression must carry through instantiation. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] T *var_tmpl_suppress = reinterpret_cast(0); +template int *var_tmpl_suppress; + +// Suppress on static data member template: suppression must carry through +// instantiation even when the suppress is only on the variable, not the class. +template +struct StaticMemberSuppressTemplate { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] static T *p; +}; +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] T *StaticMemberSuppressTemplate::p = reinterpret_cast(0); +template struct StaticMemberSuppressTemplate; From 241123cdef8a09ff0fe00a14b4d81c480b134c19 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 20:59:43 -0400 Subject: [PATCH 037/289] Refactor profile argument parsing --- clang/include/clang/Parse/Parser.h | 1 + clang/lib/Parse/ParseDeclCXX.cpp | 240 ++++++++++++----------------- 2 files changed, 98 insertions(+), 143 deletions(-) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index 5ed477b045eba..2572cfffbf187 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2285,6 +2285,7 @@ class Parser : public CodeCompletionHandler { bool ParseProfileDesignatorList( SmallVectorImpl &Designators); bool ParseProfileArgumentList(SmallVectorImpl &Args); + bool ParseProfileSuppressBody(detail::ProfileSuppressArgs &Args); bool ParseNonCommaBalancedToken(std::string &Spelling); bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling); diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 32b43be7855ef..35ca1634fae3d 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5059,16 +5059,13 @@ bool Parser::ParseProfileName(std::string &Name) { bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { if (Tok.isOneOf(tok::l_paren, tok::l_square, tok::l_brace)) { - tok::TokenKind Close = Tok.is(tok::l_paren) ? tok::r_paren - : Tok.is(tok::l_square) ? tok::r_square - : tok::r_brace; + tok::TokenKind Close; Spelling = PP.getSpelling(Tok); - if (Tok.is(tok::l_paren)) - ConsumeParen(); - else if (Tok.is(tok::l_square)) - ConsumeBracket(); - else - ConsumeBrace(); + switch (Tok.getKind()) { + case tok::l_paren: Close = tok::r_paren; ConsumeParen(); break; + case tok::l_square: Close = tok::r_square; ConsumeBracket(); break; + default: Close = tok::r_brace; ConsumeBrace(); break; + } CachedTokens Toks; if (!ConsumeAndStoreUntil(Close, Toks, /*StopAtSemi=*/false, @@ -5176,6 +5173,64 @@ bool Parser::ParseProfileDesignatorList( return false; } +bool Parser::ParseProfileSuppressBody(detail::ProfileSuppressArgs &Args) { + if (ParseProfileName(Args.ProfileName)) + return true; + + auto ParseStringLiteralValue = [&](std::string &Out) -> bool { + SmallVector StringToks; + do { + StringToks.push_back(Tok); + ConsumeStringToken(); + } while (tok::isStringLiteral(Tok.getKind())); + StringLiteralParser Literal(StringToks, PP); + if (Literal.hadError) + return true; + Out = Literal.GetString().str(); + return false; + }; + + while (TryConsumeToken(tok::comma)) { + if (!Tok.is(tok::identifier) || !NextToken().is(tok::colon)) { + std::string Spelling; + if (ParseNonOperatorNonPunctuatorToken(Spelling)) + return true; + Args.RawArguments.push_back(std::move(Spelling)); + continue; + } + + IdentifierInfo *KeyII = Tok.getIdentifierInfo(); + ConsumeToken(); + ConsumeToken(); + + if (KeyII->isStr("justification")) { + if (!tok::isStringLiteral(Tok.getKind())) { + Diag(Tok, diag::err_profiles_suppress_justification_not_string); + return true; + } + if (ParseStringLiteralValue(Args.Justification)) + return true; + continue; + } + + if (KeyII->isStr("rule")) { + bool Failed = tok::isStringLiteral(Tok.getKind()) + ? ParseStringLiteralValue(Args.Rule) + : ParseNonCommaBalancedToken(Args.Rule); + if (Failed) + return true; + continue; + } + + std::string Value; + if (ParseNonCommaBalancedToken(Value)) + return true; + Args.RawArguments.push_back(KeyII->getName().str() + " : " + Value); + } + + return false; +} + bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, SourceLocation AttrNameLoc, ParsedAttributes &Attrs, @@ -5184,153 +5239,52 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, SourceLocation ScopeLoc) { assert(ScopeName && ScopeName->isStr("profiles")); + if (!AttrName->isStr("enforce") && !AttrName->isStr("suppress") && + !AttrName->isStr("require")) + return false; + + ConsumeParen(); + AttributePool &Pool = Attrs.getPool(); auto SkipToRParen = [&]() { SkipUntil(tok::r_paren, StopAtSemi | StopBeforeMatch); if (Tok.is(tok::r_paren)) ConsumeParen(); + return true; }; + void *CustomData; if (AttrName->isStr("enforce")) { - ConsumeParen(); - auto *Args = Pool.make(); - if (ParseProfileDesignatorList(Args->Designators)) { - SkipToRParen(); - return true; - } - - if (!Tok.is(tok::r_paren)) { - Diag(Tok, diag::err_profiles_expected_rparen) << "enforce"; - SkipToRParen(); - return true; - } - SourceLocation RParen = Tok.getLocation(); - ConsumeParen(); - if (EndLoc) - *EndLoc = RParen; - - ParsedAttr *PA = - Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), - AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, - ParsedAttr::Form::CXX11()); - PA->setCustomData(Args); - return true; - } - - if (AttrName->isStr("suppress")) { - ConsumeParen(); - + if (ParseProfileDesignatorList(Args->Designators)) + return SkipToRParen(); + CustomData = Args; + } else if (AttrName->isStr("suppress")) { auto *Args = Pool.make(); - if (ParseProfileName(Args->ProfileName)) { - SkipToRParen(); - return true; - } - - auto ParseStringLiteralValue = [&](std::string &Out) -> bool { - SmallVector StringToks; - do { - StringToks.push_back(Tok); - ConsumeStringToken(); - } while (tok::isStringLiteral(Tok.getKind())); - StringLiteralParser Literal(StringToks, PP); - if (Literal.hadError) - return true; - Out = Literal.GetString().str(); - return false; - }; - - while (TryConsumeToken(tok::comma)) { - if (Tok.is(tok::identifier) && NextToken().is(tok::colon)) { - IdentifierInfo *KeyII = Tok.getIdentifierInfo(); - ConsumeToken(); - ConsumeToken(); - - if (KeyII->isStr("justification")) { - if (!tok::isStringLiteral(Tok.getKind())) { - Diag(Tok, diag::err_profiles_suppress_justification_not_string); - SkipToRParen(); - return true; - } - if (ParseStringLiteralValue(Args->Justification)) { - SkipToRParen(); - return true; - } - } else if (KeyII->isStr("rule")) { - if (tok::isStringLiteral(Tok.getKind())) { - if (ParseStringLiteralValue(Args->Rule)) { - SkipToRParen(); - return true; - } - } else { - if (ParseNonCommaBalancedToken(Args->Rule)) { - SkipToRParen(); - return true; - } - } - } else { - std::string Value; - if (ParseNonCommaBalancedToken(Value)) { - SkipToRParen(); - return true; - } - Args->RawArguments.push_back(KeyII->getName().str() + " : " + Value); - } - } else { - std::string Spelling; - if (ParseNonOperatorNonPunctuatorToken(Spelling)) { - SkipToRParen(); - return true; - } - Args->RawArguments.push_back(std::move(Spelling)); - } - } - - if (!Tok.is(tok::r_paren)) { - Diag(Tok, diag::err_profiles_expected_rparen) << "suppress"; - SkipToRParen(); - return true; - } - SourceLocation RParen = Tok.getLocation(); - ConsumeParen(); - if (EndLoc) - *EndLoc = RParen; - - ParsedAttr *PA = - Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), - AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, - ParsedAttr::Form::CXX11()); - PA->setCustomData(Args); - return true; - } - - if (AttrName->isStr("require")) { - ConsumeParen(); - + if (ParseProfileSuppressBody(*Args)) + return SkipToRParen(); + CustomData = Args; + } else { auto *Args = Pool.make(); - if (ParseProfileDesignator(Args->Designator)) { - SkipToRParen(); - return true; - } + if (ParseProfileDesignator(Args->Designator)) + return SkipToRParen(); + CustomData = Args; + } - if (!Tok.is(tok::r_paren)) { - Diag(Tok, diag::err_profiles_expected_rparen) << "require"; - SkipToRParen(); - return true; - } - SourceLocation RParen = Tok.getLocation(); - ConsumeParen(); - if (EndLoc) - *EndLoc = RParen; - - ParsedAttr *PA = - Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), - AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, - ParsedAttr::Form::CXX11()); - PA->setCustomData(Args); - return true; + if (!Tok.is(tok::r_paren)) { + Diag(Tok, diag::err_profiles_expected_rparen) << AttrName; + return SkipToRParen(); } + SourceLocation RParen = Tok.getLocation(); + ConsumeParen(); + if (EndLoc) + *EndLoc = RParen; - return false; + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, RParen), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(CustomData); + return true; } From d28bd5e7a9a0f34dc6b40aa5435f6a94ac419a0d Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 21:00:00 -0400 Subject: [PATCH 038/289] Remove unused diagnostic --- clang/include/clang/Basic/DiagnosticParseKinds.td | 2 -- 1 file changed, 2 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index d1e9551d3fd69..fe50086f6743a 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1893,8 +1893,6 @@ def err_hlsl_rootsig_non_zero_flag : Error<"flag value is neither a literal 0 no // C++ Profiles framework (P3589R2) def err_profiles_expected_profile_name : Error< "expected profile name">; -def err_profiles_expected_lparen : Error< - "expected '(' in '%0' attribute">; def err_profiles_expected_rparen : Error< "expected ')' in '%0' attribute">; def err_profiles_invalid_argument_token : Error< From bb3e275e5d9524ef3cfb2b506534357f14840a3f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 21:16:15 -0400 Subject: [PATCH 039/289] Fix incorrect test --- clang/test/SemaCXX/safety-profile-framework.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index 40501786b1b17..68dc80e42dc34 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -67,8 +67,9 @@ void suppressed_func(); // Suppress on statements // =================================================================== void test_stmt_suppress() { - [[profiles::suppress(test::type_cast)]] int x = 0; - [[profiles::suppress(test::type_cast)]] { int y = 0; } + [[profiles::suppress(test::type_cast)]] int *x = reinterpret_cast(0); + [[profiles::suppress(test::type_cast)]] { int *y = reinterpret_cast(0); } + int *z = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } // =================================================================== From fd3b77e2435539befe4dd1162a6032d0113ac79a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 21:58:55 -0400 Subject: [PATCH 040/289] Use non-owning types in profile attribute arguments to avoid leaking std::string --- clang/include/clang/Parse/Parser.h | 17 ++++++++-- clang/include/clang/Sema/ParsedAttr.h | 33 ++++++++++++++----- clang/include/clang/Sema/Sema.h | 4 +-- clang/lib/Parse/ParseDeclCXX.cpp | 46 ++++++++++++++++++++------- clang/lib/Sema/Sema.cpp | 13 ++++---- 5 files changed, 82 insertions(+), 31 deletions(-) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index 2572cfffbf187..dfdadc2d04c0c 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2280,12 +2280,23 @@ class Parser : public CodeCompletionHandler { SourceLocation *EndLoc, IdentifierInfo *ScopeName, SourceLocation ScopeLoc); + struct ParsedProfileDesignator { + std::string Name; + std::string Spelling; + }; + struct ParsedProfileSuppressArgs { + std::string Name; + std::string Justification; + std::string Rule; + SmallVector RawArguments; + }; + bool ParseProfileName(std::string &Name); - bool ParseProfileDesignator(detail::ProfileDesignator &Designator); + bool ParseProfileDesignator(ParsedProfileDesignator &Designator); bool ParseProfileDesignatorList( - SmallVectorImpl &Designators); + SmallVectorImpl &Designators); bool ParseProfileArgumentList(SmallVectorImpl &Args); - bool ParseProfileSuppressBody(detail::ProfileSuppressArgs &Args); + bool ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args); bool ParseNonCommaBalancedToken(std::string &Spelling); bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling); diff --git a/clang/include/clang/Sema/ParsedAttr.h b/clang/include/clang/Sema/ParsedAttr.h index ed971127a9f4a..91f186f420d6f 100644 --- a/clang/include/clang/Sema/ParsedAttr.h +++ b/clang/include/clang/Sema/ParsedAttr.h @@ -28,7 +28,6 @@ #include #include #include -#include #include namespace clang { @@ -98,19 +97,19 @@ struct PropertyData { }; struct ProfileDesignator { - std::string Name; - std::string Spelling; + StringRef Name; + StringRef Spelling; }; struct ProfileEnforceArgs { - SmallVector Designators; + ArrayRef Designators; }; struct ProfileSuppressArgs { - std::string ProfileName; - std::string Justification; - std::string Rule; - SmallVector RawArguments; + StringRef Name; + StringRef Justification; + StringRef Rule; + ArrayRef RawArguments; }; struct ProfileRequireArgs { @@ -769,6 +768,24 @@ class AttributePool { return ::new (Mem) T(std::forward(args)...); } + StringRef copyString(StringRef S) { + if (S.empty()) + return {}; + char *Data = static_cast(Factory.Alloc.Allocate(S.size(), 1)); + std::memcpy(Data, S.data(), S.size()); + return {Data, S.size()}; + } + + template MutableArrayRef allocateArray(unsigned N) { + if (N == 0) + return {}; + auto *Arr = + static_cast(Factory.Alloc.Allocate(sizeof(T) * N, alignof(T))); + for (unsigned I = 0; I < N; ++I) + ::new (&Arr[I]) T(); + return {Arr, N}; + } + void clear() { Factory.reclaimPool(*this); Attrs.clear(); diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 3f5dbfd14de45..f62b5d0bf64b2 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1045,8 +1045,8 @@ class Sema final : public SemaBase { SmallVector EnforcedProfiles; struct ProfileSuppressEntry { - std::string ProfileName; - std::string RuleName; + StringRef ProfileName; + StringRef RuleName; }; SmallVector ProfileSuppressStack; diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 35ca1634fae3d..6c1acb0e0e19f 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5128,7 +5128,7 @@ bool Parser::ParseProfileArgumentList(SmallVectorImpl &Args) { return false; } -bool Parser::ParseProfileDesignator(detail::ProfileDesignator &D) { +bool Parser::ParseProfileDesignator(ParsedProfileDesignator &D) { if (ParseProfileName(D.Name)) return true; @@ -5160,9 +5160,9 @@ bool Parser::ParseProfileDesignator(detail::ProfileDesignator &D) { } bool Parser::ParseProfileDesignatorList( - SmallVectorImpl &Designators) { + SmallVectorImpl &Designators) { while (true) { - detail::ProfileDesignator D; + ParsedProfileDesignator D; if (ParseProfileDesignator(D)) return true; Designators.push_back(std::move(D)); @@ -5173,8 +5173,8 @@ bool Parser::ParseProfileDesignatorList( return false; } -bool Parser::ParseProfileSuppressBody(detail::ProfileSuppressArgs &Args) { - if (ParseProfileName(Args.ProfileName)) +bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { + if (ParseProfileName(Args.Name)) return true; auto ParseStringLiteralValue = [&](std::string &Out) -> bool { @@ -5256,19 +5256,43 @@ bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, void *CustomData; if (AttrName->isStr("enforce")) { - auto *Args = Pool.make(); - if (ParseProfileDesignatorList(Args->Designators)) + SmallVector Parsed; + if (ParseProfileDesignatorList(Parsed)) return SkipToRParen(); + + auto Desigs = Pool.allocateArray(Parsed.size()); + for (unsigned I = 0; I < Parsed.size(); ++I) { + Desigs[I].Name = Pool.copyString(Parsed[I].Name); + Desigs[I].Spelling = Pool.copyString(Parsed[I].Spelling); + } + + auto *Args = Pool.make(); + Args->Designators = Desigs; CustomData = Args; } else if (AttrName->isStr("suppress")) { - auto *Args = Pool.make(); - if (ParseProfileSuppressBody(*Args)) + ParsedProfileSuppressArgs Parsed; + if (ParseProfileSuppressBody(Parsed)) return SkipToRParen(); + + auto *Args = Pool.make(); + Args->Name = Pool.copyString(Parsed.Name); + Args->Justification = Pool.copyString(Parsed.Justification); + Args->Rule = Pool.copyString(Parsed.Rule); + if (!Parsed.RawArguments.empty()) { + auto RawBuf = Pool.allocateArray(Parsed.RawArguments.size()); + for (unsigned I = 0; I < Parsed.RawArguments.size(); ++I) + RawBuf[I] = Pool.copyString(Parsed.RawArguments[I]); + Args->RawArguments = RawBuf; + } CustomData = Args; } else { - auto *Args = Pool.make(); - if (ParseProfileDesignator(Args->Designator)) + ParsedProfileDesignator Parsed; + if (ParseProfileDesignator(Parsed)) return SkipToRParen(); + + auto *Args = Pool.make(); + Args->Designator.Name = Pool.copyString(Parsed.Name); + Args->Designator.Spelling = Pool.copyString(Parsed.Spelling); CustomData = Args; } diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index c2585f8bc29cd..12fff632fddc8 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3046,7 +3046,7 @@ bool Sema::processProfilesEnforceAttr( ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { const auto &Args = AL.getProfileSuppressArgs(); - if (Args.ProfileName.empty()) + if (Args.Name.empty()) return nullptr; SmallVector RawArgs; @@ -3054,7 +3054,7 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { RawArgs.push_back(Arg); return ::new (Context) ProfilesSuppressAttr( - Context, AL, Args.ProfileName, Args.Justification, Args.Rule, + Context, AL, Args.Name, Args.Justification, Args.Rule, RawArgs.data(), RawArgs.size()); } @@ -3109,9 +3109,8 @@ Sema::ProfileSuppressScope::ProfileSuppressScope( if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) continue; const auto &Args = AL.getProfileSuppressArgs(); - if (!Args.ProfileName.empty()) { - S.ProfileSuppressStack.push_back( - {Args.ProfileName, Args.Rule}); + if (!Args.Name.empty()) { + S.ProfileSuppressStack.push_back({Args.Name, Args.Rule}); ++Count; } } @@ -3123,7 +3122,7 @@ Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D) return; for (const auto *A : D->specific_attrs()) { S.ProfileSuppressStack.push_back( - {A->getProfileName().str(), A->getRule().str()}); + {A->getProfileName(), A->getRule()}); ++Count; } } @@ -3136,7 +3135,7 @@ Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, for (const auto *A : Attrs) { if (const auto *PSA = dyn_cast(A)) { S.ProfileSuppressStack.push_back( - {PSA->getProfileName().str(), PSA->getRule().str()}); + {PSA->getProfileName(), PSA->getRule()}); ++Count; } } From 7c9df3272efe750400492ee10035dc65ba8b4e7e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 22:04:49 -0400 Subject: [PATCH 041/289] Fix interface-to-implementation designator mismatch going undiagnosed --- clang/lib/Sema/SemaModule.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 0347a7ba08521..7815fc26375fa 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -483,11 +483,8 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, // P3589R2 [decl.attr.enforce]p4: propagate interface's enforced profiles to // implementation unit. if (Interface) { - for (const auto &EP : Interface->EnforcedProfileDesignators) { - if (!isProfileEnforced(EP.ProfileName)) - EnforcedProfiles.push_back( - {EP.ProfileName, EP.Designator, ModuleLoc}); - } + for (const auto &EP : Interface->EnforcedProfileDesignators) + addProfileEnforcement(EP.ProfileName, EP.Designator, ModuleLoc); } // We already potentially made an implicit import (in the case of a module From 74bfb1c6e8eefc43ee955b0f58b5c12f894a1289 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 22:08:57 -0400 Subject: [PATCH 042/289] Don't check enforced profiles when profiles are disabled --- clang/lib/Sema/Sema.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 12fff632fddc8..d11f454f48e7a 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -2985,6 +2985,8 @@ Attr *Sema::CreateAnnotationAttr(const ParsedAttr &AL) { //===----------------------------------------------------------------------===// bool Sema::isProfileEnforced(StringRef ProfileName) const { + if (!getLangOpts().Profiles) + return false; return getProfileEnforcement(ProfileName) != nullptr; } From 329f8caf606268aa8dab60e847840475c2c32af0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 22:33:44 -0400 Subject: [PATCH 043/289] Add class template and modules tests --- .../safety-profile-framework-modules.cppm | 18 +++++++++++++++ .../test/SemaCXX/safety-profile-type-cast.cpp | 23 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 551ec589daaf1..98741c8af21e8 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -10,6 +10,8 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/impl_propagation.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_enforce.cppm -o %t/mod_gmf_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_ok.cpp -fmodule-file=GmfMod=%t/mod_gmf_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_only_enforce.cppm -o %t/mod_gmf_only_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_only_fail.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify // =================================================================== // Module with enforced profiles @@ -74,3 +76,19 @@ export void gmf_func(); //--- require_gmf_ok.cpp // expected-no-diagnostics import GmfMod [[profiles::require(test::type_cast)]]; + +// =================================================================== +// GMF-only enforce (no enforce on module-declaration): the profile is +// enforced in the TU but NOT exported via the module. A require on +// import must fail per P3589R2 [decl.attr.require]p2. +// =================================================================== +//--- mod_gmf_only_enforce.cppm +// expected-no-diagnostics +module; +[[profiles::enforce(test::type_cast)]]; +export module GmfOnlyMod; + +export void gmf_only_func(); + +//--- require_gmf_only_fail.cpp +import GmfOnlyMod [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index b6b09f2f83347..5402907403864 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -288,6 +288,29 @@ void test_trailing_suppress_block() { int *q = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } +// Suppress on class template definition: member functions should be suppressed +// during instantiation via DeclContext walk. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedClassTemplate { + void f() { + T *p = reinterpret_cast(0); + } +}; +SuppressedClassTemplate suppressed_class_tmpl_inst; + +// Without suppress on class template: member violation fires during instantiation. +template +struct UnsuppressedClassTemplate { + void f() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +}; +void instantiate_unsuppressed_class_tmpl() { + UnsuppressedClassTemplate x; + x.f(); // expected-note {{in instantiation of member function 'UnsuppressedClassTemplate::f' requested here}} +} + // Suppress on variable template: suppression must carry through instantiation. template // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} From 6876919033ad1ec377d12f0642d1962a3f4286b4 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 22:47:06 -0400 Subject: [PATCH 044/289] Add PCH support --- .../include/clang/Serialization/ASTBitCodes.h | 3 +++ clang/include/clang/Serialization/ASTReader.h | 7 +++++++ clang/include/clang/Serialization/ASTWriter.h | 1 + clang/lib/Serialization/ASTReader.cpp | 14 +++++++++++++ clang/lib/Serialization/ASTWriter.cpp | 20 +++++++++++++++++++ clang/test/PCH/cxx-profiles-enforce.cpp | 19 ++++++++++++++++++ 6 files changed, 64 insertions(+) create mode 100644 clang/test/PCH/cxx-profiles-enforce.cpp diff --git a/clang/include/clang/Serialization/ASTBitCodes.h b/clang/include/clang/Serialization/ASTBitCodes.h index 30321ebcf4b37..1181e55252185 100644 --- a/clang/include/clang/Serialization/ASTBitCodes.h +++ b/clang/include/clang/Serialization/ASTBitCodes.h @@ -748,6 +748,9 @@ enum ASTRecordTypes { /// Record code for #pragma clang riscv intrinsic vector. RISCV_VECTOR_INTRINSICS_PRAGMA = 78, + + /// Record code for enforced profile designators (P3589R2). + ENFORCED_PROFILES = 79, }; /// Record types used within a source manager block. diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h index 14f7d8a69a1b3..b6198262e3df6 100644 --- a/clang/include/clang/Serialization/ASTReader.h +++ b/clang/include/clang/Serialization/ASTReader.h @@ -1007,6 +1007,13 @@ class ASTReader /// The floating point pragma option settings. SmallVector FPPragmaOptions; + /// Enforced profile designators from PCH (P3589R2). + struct EnforcedProfileEntry { + std::string ProfileName; + std::string Designator; + }; + SmallVector SerializedEnforcedProfiles; + /// The pragma clang optimize location (if the pragma state is "off"). SourceLocation OptimizeOffPragmaLocation; diff --git a/clang/include/clang/Serialization/ASTWriter.h b/clang/include/clang/Serialization/ASTWriter.h index 0f3993ad01693..0638212d12313 100644 --- a/clang/include/clang/Serialization/ASTWriter.h +++ b/clang/include/clang/Serialization/ASTWriter.h @@ -641,6 +641,7 @@ class ASTWriter : public ASTDeserializationListener, void WriteModuleFileExtension(Sema &SemaRef, ModuleFileExtensionWriter &Writer); void WriteRISCVIntrinsicPragmas(Sema &SemaRef); + void WriteEnforcedProfiles(Sema &SemaRef); unsigned DeclParmVarAbbrev = 0; unsigned DeclContextLexicalAbbrev = 0; diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 5f9a51f1aba33..6e126f72d241e 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -4363,6 +4363,14 @@ llvm::Error ASTReader::ReadASTBlock(ModuleFile &F, FPPragmaOptions.swap(Record); break; + case ENFORCED_PROFILES: { + unsigned NameLen = Record[0]; + std::string Name = Blob.substr(0, NameLen).str(); + std::string Desig = Blob.substr(NameLen).str(); + SerializedEnforcedProfiles.push_back({std::move(Name), std::move(Desig)}); + break; + } + case DECLS_WITH_EFFECTS_TO_VERIFY: for (unsigned I = 0, N = Record.size(); I != N; /*in loop*/) DeclsWithEffectsToVerify.push_back(ReadDeclID(F, Record, I)); @@ -9282,6 +9290,12 @@ void ASTReader::UpdateSema() { } } + // Restore enforced profile designators (P3589R2). + for (const auto &EP : SerializedEnforcedProfiles) + SemaObj->addProfileEnforcement(EP.ProfileName, EP.Designator, + SourceLocation()); + SerializedEnforcedProfiles.clear(); + // For non-modular AST files, restore visiblity of modules. for (auto &Import : PendingImportedModulesSema) { if (Import.ImportLoc.isInvalid()) diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 2217260130407..145653b14368d 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -977,6 +977,7 @@ void ASTWriter::WriteBlockInfoBlock() { RECORD(PP_UNSAFE_BUFFER_USAGE); RECORD(VTABLES_TO_EMIT); RECORD(RISCV_VECTOR_INTRINSICS_PRAGMA); + RECORD(ENFORCED_PROFILES); // SourceManager Block. BLOCK(SOURCE_MANAGER_BLOCK); @@ -5301,6 +5302,24 @@ void ASTWriter::WriteRISCVIntrinsicPragmas(Sema &SemaRef) { Stream.EmitRecord(RISCV_VECTOR_INTRINSICS_PRAGMA, Record); } +void ASTWriter::WriteEnforcedProfiles(Sema &SemaRef) { + if (SemaRef.EnforcedProfiles.empty()) + return; + + auto Abbrev = std::make_shared(); + Abbrev->Add(llvm::BitCodeAbbrevOp(ENFORCED_PROFILES)); + Abbrev->Add(llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::VBR, 6)); + Abbrev->Add(llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::Blob)); + unsigned AbbrevID = Stream.EmitAbbrev(std::move(Abbrev)); + + for (const auto &EP : SemaRef.EnforcedProfiles) { + RecordData::value_type Record[] = {ENFORCED_PROFILES, + EP.ProfileName.size()}; + Stream.EmitRecordWithBlob(AbbrevID, Record, + EP.ProfileName + EP.CanonicalDesignator); + } +} + //===----------------------------------------------------------------------===// // General Serialization Routines //===----------------------------------------------------------------------===// @@ -6204,6 +6223,7 @@ ASTFileSignature ASTWriter::WriteASTCore(Sema *SemaPtr, StringRef isysroot, WriteOpenCLExtensions(*SemaPtr); WriteCUDAPragmas(*SemaPtr); WriteRISCVIntrinsicPragmas(*SemaPtr); + WriteEnforcedProfiles(*SemaPtr); } // If we're emitting a module, write out the submodule information. diff --git a/clang/test/PCH/cxx-profiles-enforce.cpp b/clang/test/PCH/cxx-profiles-enforce.cpp new file mode 100644 index 0000000000000..73cd3eb14236c --- /dev/null +++ b/clang/test/PCH/cxx-profiles-enforce.cpp @@ -0,0 +1,19 @@ +// Test this without pch. +// RUN: %clang_cc1 %s -fprofiles -std=c++20 -fsyntax-only -include %s -verify + +// Test with pch. +// RUN: %clang_cc1 %s -fprofiles -std=c++20 -emit-pch -o %t +// RUN: %clang_cc1 %s -fprofiles -std=c++20 -fsyntax-only -include-pch %t -verify + +#ifndef HEADER +#define HEADER + +[[profiles::enforce(test::type_cast)]]; + +#else + +void test() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +#endif From dca8f0eb5625f3f3f8413812440c9de4a2e61416 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 23:11:49 -0400 Subject: [PATCH 045/289] Add driver level -fprofiles flag --- clang/include/clang/Options/Options.td | 10 +++++++--- clang/lib/Driver/ToolChains/Clang.cpp | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index 3ebff8eb1d887..d5c9714cb2f59 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -1695,6 +1695,13 @@ defm coroutines : BoolFOption<"coroutines", "Enable support for the C++ Coroutines">, NegFlag>; +defm profiles : BoolFOption<"profiles", + LangOpts<"Profiles">, DefaultFalse, + PosFlag, + NegFlag>, + ShouldParseIf; + defm coro_aligned_allocation : BoolFOption<"coro-aligned-allocation", LangOpts<"CoroAlignedAllocation">, DefaultFalse, PosFlag, let Visibility = [CC1Option] in { -def fprofiles : Flag<["-"], "fprofiles">, - HelpText<"Enable C++ profiles framework (P3589R2)">, - MarshallingInfoFlag>; def fblocks_runtime_optional : Flag<["-"], "fblocks-runtime-optional">, HelpText<"Weakly link in the blocks runtime">, MarshallingInfoFlag>; diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index 36ba3d35ed012..b35d23415b18b 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -6409,6 +6409,9 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA, Args.addOptInFlag(CmdArgs, options::OPT_ffixed_point, options::OPT_fno_fixed_point); + Args.addOptInFlag(CmdArgs, options::OPT_fprofiles, + options::OPT_fno_profiles); + Args.addOptInFlag(CmdArgs, options::OPT_fexperimental_overflow_behavior_types, options::OPT_fno_experimental_overflow_behavior_types); From 3a2b9817250d25e5f0bdd07ad9af20d630e3bd1c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 14 Apr 2026 23:33:04 -0400 Subject: [PATCH 046/289] Add tests for module interfaces/implementations --- .../safety-profile-framework-modules.cppm | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 98741c8af21e8..0c2be4d3f84ae 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -12,6 +12,11 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_ok.cpp -fmodule-file=GmfMod=%t/mod_gmf_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_only_enforce.cppm -o %t/mod_gmf_only_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_only_fail.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/part_iface.cppm -o %t/part_iface.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_ok.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_fail.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_iface_violation.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_enforce.cppm -verify // =================================================================== // Module with enforced profiles @@ -92,3 +97,49 @@ export void gmf_only_func(); //--- require_gmf_only_fail.cpp import GmfOnlyMod [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} + +// =================================================================== +// Partition interface with enforce: the profile is exported via the +// partition module, so require on partition import succeeds, and +// enforcement fires locally within the partition. +// =================================================================== +//--- part_iface.cppm +// expected-no-diagnostics +export module PartMod:part [[profiles::enforce(test::type_cast)]]; + +export void part_func(); + +// =================================================================== +// Partition interface: enforcement fires locally within the partition. +// =================================================================== +//--- part_iface_violation.cppm +export module PartViol:part [[profiles::enforce(test::type_cast)]]; + +export void part_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +//--- part_primary_require_ok.cppm +// expected-no-diagnostics +export module PartMod; +import :part [[profiles::require(test::type_cast)]]; + +// =================================================================== +// Partition interface: require fails for a profile the partition does +// not enforce. +// =================================================================== +//--- part_primary_require_fail.cppm +export module PartMod; +import :part [[profiles::require(test::not_enforced)]]; // expected-error {{required profile 'test::not_enforced' is not enforced by imported module}} + +// =================================================================== +// Partition implementation with enforce: enforcement is active locally +// but the profile is NOT exported on the module (ExportMod is null +// for PartitionImplementation). +// =================================================================== +//--- part_impl_enforce.cppm +module PartImpl:impl [[profiles::enforce(test::type_cast)]]; + +void impl_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} From 152aea2e29d5e6189e8862eb209ec809be40fe05 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 17:06:36 -0400 Subject: [PATCH 047/289] Use ProfileSuppressScope instead of DeclContext walk --- clang/include/clang/Sema/Sema.h | 1 + clang/lib/Parse/ParseDeclCXX.cpp | 6 ++ clang/lib/Sema/Sema.cpp | 26 +++--- .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 6 ++ .../test/SemaCXX/safety-profile-type-cast.cpp | 85 +++++++++++++++++++ 5 files changed, 114 insertions(+), 10 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index f62b5d0bf64b2..f4f4cbd95ed94 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1073,6 +1073,7 @@ class Sema final : public SemaBase { ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); ProfileSuppressScope(Sema &S, const Decl *D); ProfileSuppressScope(Sema &S, ArrayRef Attrs); + ProfileSuppressScope(Sema &S, const DeclContext *LexicalChain); ~ProfileSuppressScope(); }; diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 6c1acb0e0e19f..ed882c4a63f4d 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -207,6 +207,8 @@ Parser::DeclGroupPtrTy Parser::ParseNamespace(DeclaratorContext Context, getCurScope(), InlineLoc, NamespaceLoc, IdentLoc, Ident, T.getOpenLocation(), attrs, ImplicitUsingDirectiveDecl, false); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); + PrettyDeclStackTraceEntry CrashInfo(Actions.Context, NamespcDecl, NamespaceLoc, "parsing namespace"); @@ -256,6 +258,8 @@ void Parser::ParseInnerNamespace(const InnerNamespaceInfoList &InnerNSs, assert(!ImplicitUsingDirectiveDecl && "nested namespace definition cannot define anonymous namespace"); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); + ParseInnerNamespace(InnerNSs, ++index, InlineLoc, attrs, Tracker); NamespaceScope.Exit(); @@ -3658,6 +3662,8 @@ void Parser::ParseCXXMemberSpecification(SourceLocation RecordLoc, IsFinalSpelledSealed, IsAbstract, T.getOpenLocation()); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, TagDecl); + // C++ 11p3: Members of a class defined with the keyword class are private // by default. Members of a class defined with the keywords struct or union // are public by default. diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index d11f454f48e7a..2699879abf717 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3068,16 +3068,6 @@ bool Sema::isProfileSuppressed(StringRef ProfileName, if (E.RuleName.empty() || E.RuleName == RuleName) return true; } - for (const DeclContext *DC = CurContext; DC; DC = DC->getParent()) { - if (const auto *D = dyn_cast(DC)) { - for (const auto *A : D->specific_attrs()) { - if (A->getProfileName() != ProfileName) - continue; - if (A->getRule().empty() || A->getRule() == RuleName) - return true; - } - } - } return false; } @@ -3143,6 +3133,22 @@ Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, } } +Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, + const DeclContext *DC) + : S(S) { + if (!S.getLangOpts().Profiles) + return; + for (; DC; DC = DC->getLexicalParent()) { + if (const auto *D = dyn_cast(DC)) { + for (const auto *A : D->specific_attrs()) { + S.ProfileSuppressStack.push_back( + {A->getProfileName(), A->getRule()}); + ++Count; + } + } + } +} + Sema::ProfileSuppressScope::~ProfileSuppressScope() { assert(S.ProfileSuppressStack.size() >= Count); S.ProfileSuppressStack.pop_back_n(Count); diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index d746fa36a0d4b..ef9ed4c366557 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -5931,6 +5931,12 @@ void Sema::InstantiateFunctionDefinition(SourceLocation PointOfInstantiation, // PushDeclContext because we don't have a scope. Sema::ContextRAII savedContext(*this, Function); + // P3589R2 [decl.attr.suppress]p3: push profile suppressions from the + // pattern's lexical parent chain so that inline members of a suppressed + // class/namespace retain suppression during instantiation. + ProfileSuppressScope ProfileSuppressGuard( + *this, PatternDecl->getLexicalDeclContext()); + FPFeaturesStateRAII SavedFPFeatures(*this); CurFPFeatures = FPOptions(getLangOpts()); FpPragmaStack.CurrentValue = FPOptionsOverride(); diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 5402907403864..16f5b7568d596 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -328,3 +328,88 @@ template // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast)]] T *StaticMemberSuppressTemplate::p = reinterpret_cast(0); template struct StaticMemberSuppressTemplate; + +// Out-of-line member function of a suppressed class: suppression does NOT +// extend past the class body. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] OutOfLineSuppressedClass { + void inline_ok() { + int *p = reinterpret_cast(0); + } + void out_of_line(); + static int *s; +}; + +void OutOfLineSuppressedClass::out_of_line() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *OutOfLineSuppressedClass::s = reinterpret_cast(0); + +// Out-of-line function in a formerly-suppressed namespace: suppression does +// NOT extend past the namespace body. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] OutOfLineSuppressedNS { + void inline_ok() { + int *p = reinterpret_cast(0); + } + void out_of_line(); +} + +void OutOfLineSuppressedNS::out_of_line() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// Out-of-line member of a suppressed class template. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] OutOfLineSuppressedClassTemplate { + void inline_ok() { + T *p = reinterpret_cast(0); + } + void out_of_line(); +}; + +template +void OutOfLineSuppressedClassTemplate::out_of_line() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +template struct OutOfLineSuppressedClassTemplate; // expected-note {{in instantiation of member function 'OutOfLineSuppressedClassTemplate::out_of_line' requested here}} + +// Nested suppress: class template inside a suppressed outer class. +// Suppression on Outer must propagate to inline members of Inner during +// instantiation via the lexical parent chain. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] NestedSuppressOuter { + template + struct Inner { + void f() { T *p = reinterpret_cast(0); } + void out_of_line(); + }; +}; + +template +void NestedSuppressOuter::Inner::out_of_line() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +template struct NestedSuppressOuter::Inner; // expected-note {{in instantiation of member function 'NestedSuppressOuter::Inner::out_of_line' requested here}} + +// Nested suppress: class template inside a suppressed namespace. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] NestedSuppressNS { + template + struct Inner { + void f() { T *p = reinterpret_cast(0); } + void out_of_line(); + }; +} + +template +void NestedSuppressNS::Inner::out_of_line() { + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +template struct NestedSuppressNS::Inner; // expected-note {{in instantiation of member function 'NestedSuppressNS::Inner::out_of_line' requested here}} From 7c93bce1cb58d48815e86408ef4948190c548ba2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 17:24:39 -0400 Subject: [PATCH 048/289] Fix suppress for out-of-line data members --- clang/lib/Sema/SemaTemplateInstantiate.cpp | 2 ++ clang/lib/Sema/SemaTemplateInstantiateDecl.cpp | 2 ++ clang/test/SemaCXX/safety-profile-type-cast.cpp | 17 +++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index 06ccce4517f16..9ce39932ebd35 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -3843,6 +3843,8 @@ bool Sema::InstantiateInClassInitializer( CXXThisScopeRAII ThisScope(*this, Instantiation->getParent(), Qualifiers()); ProfileSuppressScope ProfileSuppressGuard(*this, Pattern); + ProfileSuppressScope ProfileSuppressLexicalGuard( + *this, Pattern->getLexicalDeclContext()); ExprResult NewInit = SubstInitializer(OldInit, TemplateArgs, /*CXXDirectInit=*/false); diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index ef9ed4c366557..4de8f099b161a 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -6233,6 +6233,8 @@ void Sema::InstantiateVariableInitializer( ContextRAII SwitchContext(*this, Var->getDeclContext()); ProfileSuppressScope ProfileSuppressGuard(*this, OldVar); + ProfileSuppressScope ProfileSuppressLexicalGuard( + *this, OldVar->getLexicalDeclContext()); EnterExpressionEvaluationContext Evaluated( *this, Sema::ExpressionEvaluationContext::PotentiallyEvaluated, Var, diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 16f5b7568d596..0eccbbae2fb5c 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -413,3 +413,20 @@ void NestedSuppressNS::Inner::out_of_line() { } template struct NestedSuppressNS::Inner; // expected-note {{in instantiation of member function 'NestedSuppressNS::Inner::out_of_line' requested here}} + +// NSDMI in a suppressed class template: suppression applies via the lexical +// parent chain during default member initializer instantiation. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedNSDMI { + T *p = reinterpret_cast(0); +}; +SuppressedNSDMI suppressed_nsdmi_inst; + +// Inline static data member in a suppressed class template. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] SuppressedInlineStatic { + static inline T *s = reinterpret_cast(0); +}; +template struct SuppressedInlineStatic; From 26775103c96c65ab7bfbd41f24fc569feae0e68c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 17:34:34 -0400 Subject: [PATCH 049/289] Fix suppress for out-of-line functions --- clang/lib/Sema/SemaTemplateInstantiateDecl.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index 4de8f099b161a..ab05a37906b68 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -5931,10 +5931,9 @@ void Sema::InstantiateFunctionDefinition(SourceLocation PointOfInstantiation, // PushDeclContext because we don't have a scope. Sema::ContextRAII savedContext(*this, Function); - // P3589R2 [decl.attr.suppress]p3: push profile suppressions from the - // pattern's lexical parent chain so that inline members of a suppressed - // class/namespace retain suppression during instantiation. ProfileSuppressScope ProfileSuppressGuard( + *this, static_cast(PatternDecl)); + ProfileSuppressScope ProfileSuppressLexicalGuard( *this, PatternDecl->getLexicalDeclContext()); FPFeaturesStateRAII SavedFPFeatures(*this); From 402a9babf33ee7861f8ceb31bdaa982fccdfb8d8 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 17:42:34 -0400 Subject: [PATCH 050/289] Combine ProfileSuppressScope constructors --- clang/include/clang/Sema/Sema.h | 6 ++-- clang/lib/Sema/Sema.cpp | 35 ++++++++----------- clang/lib/Sema/SemaTemplateInstantiate.cpp | 5 ++- .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 9 ++--- 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index f4f4cbd95ed94..b108a5e47ab31 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1069,11 +1069,13 @@ class Sema final : public SemaBase { Sema &S; unsigned Count = 0; + void addFromDecl(const Decl *D); + public: ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); - ProfileSuppressScope(Sema &S, const Decl *D); + ProfileSuppressScope(Sema &S, const Decl *D, + bool WalkLexicalParents = false); ProfileSuppressScope(Sema &S, ArrayRef Attrs); - ProfileSuppressScope(Sema &S, const DeclContext *LexicalChain); ~ProfileSuppressScope(); }; diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 2699879abf717..e2d076abbd86f 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3108,10 +3108,7 @@ Sema::ProfileSuppressScope::ProfileSuppressScope( } } -Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D) - : S(S) { - if (!S.getLangOpts().Profiles || !D) - return; +void Sema::ProfileSuppressScope::addFromDecl(const Decl *D) { for (const auto *A : D->specific_attrs()) { S.ProfileSuppressStack.push_back( {A->getProfileName(), A->getRule()}); @@ -3119,6 +3116,20 @@ Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D) } } +Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, + bool WalkLexicalParents) + : S(S) { + if (!S.getLangOpts().Profiles || !D) + return; + addFromDecl(D); + if (WalkLexicalParents) { + for (const DeclContext *DC = D->getLexicalDeclContext(); DC; + DC = DC->getLexicalParent()) + if (const auto *Parent = dyn_cast(DC)) + addFromDecl(Parent); + } +} + Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, ArrayRef Attrs) : S(S) { @@ -3133,22 +3144,6 @@ Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, } } -Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, - const DeclContext *DC) - : S(S) { - if (!S.getLangOpts().Profiles) - return; - for (; DC; DC = DC->getLexicalParent()) { - if (const auto *D = dyn_cast(DC)) { - for (const auto *A : D->specific_attrs()) { - S.ProfileSuppressStack.push_back( - {A->getProfileName(), A->getRule()}); - ++Count; - } - } - } -} - Sema::ProfileSuppressScope::~ProfileSuppressScope() { assert(S.ProfileSuppressStack.size() >= Count); S.ProfileSuppressStack.pop_back_n(Count); diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index 9ce39932ebd35..e658288a1271a 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -3842,9 +3842,8 @@ bool Sema::InstantiateInClassInitializer( ActOnStartCXXInClassMemberInitializer(); CXXThisScopeRAII ThisScope(*this, Instantiation->getParent(), Qualifiers()); - ProfileSuppressScope ProfileSuppressGuard(*this, Pattern); - ProfileSuppressScope ProfileSuppressLexicalGuard( - *this, Pattern->getLexicalDeclContext()); + ProfileSuppressScope ProfileSuppressGuard( + *this, Pattern, /*WalkLexicalParents=*/true); ExprResult NewInit = SubstInitializer(OldInit, TemplateArgs, /*CXXDirectInit=*/false); diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index ab05a37906b68..2c5b96c24e1e8 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -5932,9 +5932,7 @@ void Sema::InstantiateFunctionDefinition(SourceLocation PointOfInstantiation, Sema::ContextRAII savedContext(*this, Function); ProfileSuppressScope ProfileSuppressGuard( - *this, static_cast(PatternDecl)); - ProfileSuppressScope ProfileSuppressLexicalGuard( - *this, PatternDecl->getLexicalDeclContext()); + *this, PatternDecl, /*WalkLexicalParents=*/true); FPFeaturesStateRAII SavedFPFeatures(*this); CurFPFeatures = FPOptions(getLangOpts()); @@ -6231,9 +6229,8 @@ void Sema::InstantiateVariableInitializer( Var->setImplicitlyInline(); ContextRAII SwitchContext(*this, Var->getDeclContext()); - ProfileSuppressScope ProfileSuppressGuard(*this, OldVar); - ProfileSuppressScope ProfileSuppressLexicalGuard( - *this, OldVar->getLexicalDeclContext()); + ProfileSuppressScope ProfileSuppressGuard( + *this, OldVar, /*WalkLexicalParents=*/true); EnterExpressionEvaluationContext Evaluated( *this, Sema::ExpressionEvaluationContext::PotentiallyEvaluated, Var, From b78f923efd80ffa30c6d7c7e88649317e4460c4d Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 17:58:36 -0400 Subject: [PATCH 051/289] Don't serialize TU-level enforced profiles state in module PCMs --- clang/lib/Serialization/ASTWriter.cpp | 2 + .../safety-profile-framework-modules.cppm | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 145653b14368d..867c92f2f4629 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -5303,6 +5303,8 @@ void ASTWriter::WriteRISCVIntrinsicPragmas(Sema &SemaRef) { } void ASTWriter::WriteEnforcedProfiles(Sema &SemaRef) { + if (WritingModule) + return; if (SemaRef.EnforcedProfiles.empty()) return; diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 0c2be4d3f84ae..9bcb818e75959 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -17,6 +17,10 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_fail.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_iface_violation.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_enforce.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_no_local_enforce.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_gmf_only_no_leak.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_different_desig.cppm -o %t/mod_different_desig.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_two_modules.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -fmodule-file=DiffDesigMod=%t/mod_different_desig.pcm -verify // =================================================================== // Module with enforced profiles @@ -143,3 +147,45 @@ module PartImpl:impl [[profiles::enforce(test::type_cast)]]; void impl_func() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } + +// =================================================================== +// Importing a module with enforce must NOT leak enforcement into the +// importer's own TU. Without a local enforce, reinterpret_cast is OK. +// =================================================================== +//--- import_no_local_enforce.cpp +// expected-no-diagnostics +import TestMod; + +void importer_func() { + int *p = reinterpret_cast(0); +} + +// =================================================================== +// GMF-only enforce must NOT leak into importers. +// =================================================================== +//--- import_gmf_only_no_leak.cpp +// expected-no-diagnostics +import GmfOnlyMod; + +void gmf_importer_func() { + int *p = reinterpret_cast(0); +} + +// =================================================================== +// Second module enforcing test::type_cast with a different designator. +// =================================================================== +//--- mod_different_desig.cppm +// expected-no-diagnostics +export module DiffDesigMod [[profiles::enforce(test::type_cast(strict: true))]]; + +export void diff_func(); + +// =================================================================== +// Importing two modules that enforce the same profile name with +// different designators must NOT produce err_profiles_enforce_mismatch +// in the importer. +// =================================================================== +//--- import_two_modules.cpp +// expected-no-diagnostics +import TestMod; +import DiffDesigMod; From 5372c8a28747f326e1a2d3003020f9445d783ea4 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 18:24:56 -0400 Subject: [PATCH 052/289] Fix suppress on generic lambda --- clang/lib/Sema/SemaLambda.cpp | 11 ++++ .../test/SemaCXX/safety-profile-type-cast.cpp | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index 8572e3a742a6c..271aaacc8f4a4 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -16,6 +16,7 @@ #include "clang/AST/ExprCXX.h" #include "clang/AST/MangleNumberingContext.h" #include "clang/Basic/TargetInfo.h" +#include "clang/Sema/Attr.h" #include "clang/Sema/DeclSpec.h" #include "clang/Sema/Initialization.h" #include "clang/Sema/Lookup.h" @@ -1502,6 +1503,16 @@ void Sema::ActOnStartOfLambdaDefinition(LambdaIntroducer &Intro, // Attributes on the lambda apply to the method. ProcessDeclAttributes(CurScope, Method, ParamInfo); + // P3589R2: Propagate active profile suppressions to the call operator so + // that generic lambda instantiation (which walks lexical Decl parents, not + // the enclosing stmt tree) can recover them. + if (getLangOpts().Profiles) { + for (const auto &E : ProfileSuppressStack) + Method->addAttr(ProfilesSuppressAttr::CreateImplicit( + Context, E.ProfileName, /*Justification=*/"", E.RuleName, + /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0)); + } + if (Context.getTargetInfo().getTriple().isAArch64()) ARM().CheckSMEFunctionDefAttributes(Method); diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 0eccbbae2fb5c..bc566e42d2172 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -430,3 +430,68 @@ struct [[profiles::suppress(test::type_cast)]] SuppressedInlineStatic { static inline T *s = reinterpret_cast(0); }; template struct SuppressedInlineStatic; + +// Generic lambda defined inside a suppress block, returned, and instantiated +// outside -- the suppress must carry through instantiation. +auto get_suppressed_generic_lambda() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto l = [](auto x) { return reinterpret_cast(x); }; + return l; + } +} +void test_generic_lambda_suppress_propagation() { + auto l = get_suppressed_generic_lambda(); + l(0); +} + +// Generic lambda inside a suppress with rule restriction: only the matching +// rule is suppressed. +auto get_rule_restricted_lambda() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] { + auto l = [](auto x) { return reinterpret_cast(x); }; + return l; + } +} +void test_generic_lambda_rule_suppress() { + auto l = get_rule_restricted_lambda(); + l(0); +} + +// Generic lambda without suppress: the violation still fires during +// instantiation. +auto get_unsuppressed_generic_lambda() { + auto l = [](auto x) { + return reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + return l; +} +void test_generic_lambda_no_suppress() { + auto l = get_unsuppressed_generic_lambda(); + l(0); // expected-note {{in instantiation of function template specialization 'get_unsuppressed_generic_lambda()::(lambda)::operator()' requested here}} +} + +// Non-generic lambda inside suppress: still works (regression check). +void test_nongeneric_lambda_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + auto l = []() { return reinterpret_cast(0); }; + l(); + } +} + +// Suppress of a different profile does not propagate to the generic lambda. +auto get_wrong_profile_lambda() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] { + auto l = [](auto x) { + return reinterpret_cast(x); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + return l; + } +} +void test_wrong_profile_lambda() { + auto l = get_wrong_profile_lambda(); + l(0); // expected-note {{in instantiation of function template specialization 'get_wrong_profile_lambda()::(lambda)::operator()' requested here}} +} From 045954815b61d015c8b8c513a4e91ae37191640f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Apr 2026 20:29:52 -0400 Subject: [PATCH 053/289] Add docs --- clang/docs/ProfilesFramework.rst | 159 +++++++++++++++++++++++++++++++ clang/docs/index.rst | 1 + 2 files changed, 160 insertions(+) create mode 100644 clang/docs/ProfilesFramework.rst diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst new file mode 100644 index 0000000000000..4dd38c133c53d --- /dev/null +++ b/clang/docs/ProfilesFramework.rst @@ -0,0 +1,159 @@ +=========================== +C++ Profiles Framework +=========================== + +.. contents:: + :depth: 3 + :local: + + +Introduction +============ + +The C++ Profiles framework (`P3589R2 +`_) allows a +translation unit to opt into additional language restrictions called *profiles*. +A profile is a named set of rules enforced by the compiler. A translation unit +requests enforcement with ``[[profiles::enforce(...)]]``; individual +declarations or statements can suppress enforcement with +``[[profiles::suppress(...)]]``; and module imports can require that an imported +module enforces a profile with ``[[profiles::require(...)]]``. + +Profiles do not change the meaning of well-formed programs with no undefined +behavior. Their static semantic effects are conceptually applied only after +translation phase 7: a profile cannot change the outcome of overload resolution +or template instantiation, and it is not possible to SFINAE on a profile +violation. + +The framework is profile-agnostic. It handles attribute parsing, enforcement +tracking, suppression scoping, module integration, and serialization. +Individual profiles only need to call a single API +(``Sema::checkProfileViolation``) at the appropriate semantic check sites. +Everything else -- suppression, template instantiation, SFINAE exclusion, +module propagation, and PCH/BMI serialization -- is handled by the framework +automatically. + +The entire framework is gated on the ``-fprofiles`` command-line flag +(``LangOpts.Profiles``). + + +Implementing a New Profile +========================== + +Adding a new profile requires no changes to the framework itself. A profile is +defined entirely by: + +1. A **profile name** (a ``::``-separated identifier sequence such as + ``vendor::safety`` or ``std::type``). +2. One or more **rule names** (string identifiers such as + ``"reinterpret_cast"``). +3. **Diagnostics** emitted when a rule is violated. +4. **Check sites** in the compiler where ``checkProfileViolation`` is called. + +There is no central registry of profiles. The framework treats profile names as +opaque strings; a profile is considered "enforced" simply because the user wrote +``[[profiles::enforce(name)]]`` in their source. Each rule within a profile is +likewise just a string identifier; users can suppress individual rules with +``[[profiles::suppress(profile_name, rule: "rule_name")]]``. + +Define Diagnostics +------------------ + +Add diagnostics to ``clang/include/clang/Basic/DiagnosticSemaKinds.td`` in the +``// C++ Profiles framework (P3589R2)`` group. Each diagnostic should accept +``%0`` for the profile name, since ``checkProfileViolation`` passes the profile +name as the first diagnostic argument. + +For example, the ``test::type_cast`` profile defines: + +.. code-block:: text + + def err_profile_type_cast_reinterpret : Error< + "'reinterpret_cast' is unsafe under profile '%0'">; + +Add ``checkProfileViolation`` Calls +------------------------------------ + +At each semantic site where a rule can be violated, call +``Sema::checkProfileViolation``: + +.. code-block:: c++ + + checkProfileViolation("my::profile", "my_rule", Loc, + diag::err_my_profile_rule); + +This function checks whether the profile is enforced and not suppressed, +skips SFINAE / unevaluated / discarded-statement contexts (so that profiles +cannot affect overload resolution or template instantiation), and emits the +diagnostic if appropriate. The profile name is passed as ``%0``. + + +Framework Internals Reference +============================= + +This section describes the framework mechanisms that profile implementers +benefit from understanding, even though they do not interact with them directly. + +``ProfileSuppressScope`` +------------------------ + +An RAII guard that pushes suppression entries onto ``Sema::ProfileSuppressStack`` +and pops them on destruction. It is used by the parser and template +instantiation machinery to make ``[[profiles::suppress]]`` attributes active +during the appropriate region. ``checkProfileViolation`` consults +``ProfileSuppressStack`` directly, so profile implementers never need to create +``ProfileSuppressScope`` objects. + +Template Instantiation +---------------------- + +During template instantiation, the framework ensures that +``[[profiles::suppress]]`` on the template pattern and its lexical parents +applies to instantiated code. This is done via ``ProfileSuppressScope`` with +``WalkLexicalParents=true`` at several sites: + +- ``SemaTemplateInstantiateDecl.cpp`` -- function and variable template + instantiation. +- ``SemaTemplateInstantiate.cpp`` -- default member initializer instantiation. +- ``TreeTransform.h`` -- ``TransformAttributedStmt`` (suppress on statements) + and ``TransformDeclStmt`` (suppress on declarations within a ``DeclStmt``). + +Module Enforcement +------------------ + +``[[profiles::enforce(...)]]`` on a module interface declaration records the +enforced profile designators on ``Module::EnforcedProfileDesignators``. Module +implementation units automatically inherit the interface's enforcements. +``[[profiles::require(...)]]`` on an import-declaration validates that the +imported module's ``EnforcedProfileDesignators`` contains a matching designator. + +Importing a module that enforces a profile does **not** enforce that profile in +the importing translation unit. Enforcement is always explicit and local. + +Serialization +------------- + +The framework serializes enforcement state automatically. Profile implementers +do not need to add any serialization code. + +- **PCH**: ``Sema::EnforcedProfiles`` is written as ``ENFORCED_PROFILES`` + records in the AST bitstream and restored when the PCH is loaded. +- **Module BMI**: ``Module::EnforcedProfileDesignators`` is written as + ``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. + +The ``test::type_cast`` Profile +=============================== + +The ``test::type_cast`` profile is a minimal, test-only profile included in the +tree. It demonstrates everything needed to implement a profile. + +The profile has a single rule, ``reinterpret_cast``, which diagnoses uses of +``reinterpret_cast``. In ``clang/lib/Sema/SemaCast.cpp``, inside the +``reinterpret_cast`` handling of ``Sema::BuildCXXNamedCast``: + +.. code-block:: c++ + + checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, + diag::err_profile_type_cast_reinterpret); + +That single call is the entire profile implementation. diff --git a/clang/docs/index.rst b/clang/docs/index.rst index c2974a4b2f9ea..1fd752fe08bc1 100644 --- a/clang/docs/index.rst +++ b/clang/docs/index.rst @@ -31,6 +31,7 @@ Using Clang as a Compiler ScalableStaticAnalysisFramework/index DataFlowAnalysisIntro FunctionEffectAnalysis + ProfilesFramework AddressSanitizer ThreadSanitizer MemorySanitizer From d71e3d0fac4a530e01a3d7e14723ba1ec110eea2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Apr 2026 12:18:04 -0400 Subject: [PATCH 054/289] Fix suppress on inline member functions --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 6 ++ .../test/SemaCXX/safety-profile-type-cast.cpp | 83 +++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index 5b135010f813a..70a7bb67a02b4 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -376,6 +376,9 @@ void Parser::ParseLexedMethodDeclaration(LateParsedMethodDeclaration &LM) { // Start the delayed C++ method declaration Actions.ActOnStartDelayedCXXMethodDeclaration(getCurScope(), LM.Method); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, + LM.Method->getAsFunction()); + // Introduce the parameters into scope and parse their default // arguments. InFunctionTemplateScope.Scopes.Enter(Scope::FunctionPrototypeScope | @@ -604,6 +607,9 @@ void Parser::ParseLexedMethodDef(LexedMethod &LM) { Actions.ActOnStartOfFunctionDef(getCurScope(), LM.D); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, + LM.D->getAsFunction()); + llvm::scope_exit _([&]() { while (Tok.isNot(tok::eof)) ConsumeAnyToken(); diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index bc566e42d2172..15f99610f76a4 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -495,3 +495,86 @@ void test_wrong_profile_lambda() { auto l = get_wrong_profile_lambda(); l(0); // expected-note {{in instantiation of function template specialization 'get_wrong_profile_lambda()::(lambda)::operator()' requested here}} } + +// Suppress on an inline member function definition applies to the body. +struct InlineMethodSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + void f() { + int *p = reinterpret_cast(0); + } +}; + +// Suppress on an inline member function with a non-matching profile does not +// suppress the violation. +struct InlineMethodWrongProfile { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] + void f() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +}; + +// Suppress on an inline constructor applies to the member initializer list. +struct InlineCtorMemInitSuppress { + int *p; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + InlineCtorMemInitSuppress() : p(reinterpret_cast(0)) {} +}; + +// Suppress on an inline destructor applies to the destructor body. +struct InlineDtorSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + ~InlineDtorSuppress() { + int *p = reinterpret_cast(0); + } +}; + +// Suppress on an inline conversion operator applies to its body. +struct InlineConversionSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + operator int *() { + return reinterpret_cast(0); + } +}; + +// Suppress on an inline operator overload applies to its body. +struct InlineOperatorSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + int *operator+() { + return reinterpret_cast(0); + } +}; + +// Suppress on a late-parsed default argument of an inline member function. +struct InlineDefaultArgSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + void f(int *p = reinterpret_cast(0)); +}; + +// Without per-method suppress, the violation still fires in an inline body. +struct InlineMethodNoSuppress { + void f() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } +}; + +// Per-method suppress on a member function template with a non-dependent +// violation in the body: exercises getAsFunction() on a FunctionTemplateDecl. +struct InlineMemberTemplateSuppress { + template + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + void f() { + int *p = reinterpret_cast(0); + } +}; +void instantiate_inline_member_template_suppress() { + InlineMemberTemplateSuppress s; + s.f(); +} From 7c41e8c5da46066676d91a231eac7bab28ae4911 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Apr 2026 12:27:56 -0400 Subject: [PATCH 055/289] Fix suppress on data member initializers --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 2 -- clang/lib/Parse/ParseDeclCXX.cpp | 2 ++ .../test/SemaCXX/safety-profile-type-cast.cpp | 20 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index 70a7bb67a02b4..c037ec4d09d6c 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -692,8 +692,6 @@ void Parser::ParseLexedMemberInitializer(LateParsedMemberInitializer &MI) { EnterExpressionEvaluationContext Eval( Actions, Sema::ExpressionEvaluationContext::PotentiallyEvaluatedIfUsed); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, MI.Field); - ExprResult Init = ParseCXXMemberInitializer(MI.Field, /*IsFunction=*/false, EqualLoc); diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index ed882c4a63f4d..3e683f7b7f687 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -3275,6 +3275,8 @@ ExprResult Parser::ParseCXXMemberInitializer(Decl *D, bool IsFunction, : Sema::ExpressionEvaluationContext::PotentiallyEvaluated, D); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, D); + // CWG2760 // Default member initializers used to initialize a base or member subobject // [...] are considered to be part of the function body diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 15f99610f76a4..098556df4f49e 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -578,3 +578,23 @@ void instantiate_inline_member_template_suppress() { InlineMemberTemplateSuppress s; s.f(); } + +// Suppress on a static inline data member applies to its in-class initializer. +struct StaticInlineDataMemberSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] + static inline int *p = reinterpret_cast(0); +}; + +// Without per-member suppress, a static inline data member initializer fires. +struct StaticInlineDataMemberNoSuppress { + static inline int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +}; + +// Suppress on a static data member initializer with a non-matching profile +// does not suppress the violation. +struct StaticInlineDataMemberWrongProfile { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] + static inline int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +}; From bd6cff67142f11e57abf65e11a80d8330a69fcfa Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Apr 2026 12:42:10 -0400 Subject: [PATCH 056/289] Correctly warn unused when profiles are disabled --- clang/lib/Sema/SemaModule.cpp | 36 ++++++++++--------- .../safety-profile-framework-modules.cppm | 30 ++++++++++++++++ 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index 7815fc26375fa..edb13336fb5b6 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -467,7 +467,8 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, ? Mod : nullptr; for (const auto &AL : Attrs) - if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) + if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce && + AL.diagnoseLangOpts(*this)) processProfilesEnforceAttr(AL, ExportMod, nullptr, nullptr); } @@ -1629,23 +1630,26 @@ void Sema::ActOnModuleImportAttrs(Decl *D, Module *ImportedMod = ID ? ID->getImportedModule() : nullptr; for (const auto &AL : Attrs) { - if (AL.getKind() == ParsedAttr::AT_ProfilesRequire) { - if (!ImportedMod) { - Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); - continue; - } + if (AL.getKind() != ParsedAttr::AT_ProfilesRequire) + continue; + if (!AL.diagnoseLangOpts(*this)) + continue; - const auto &Args = AL.getProfileRequireArgs(); - StringRef Desig = Args.Designator.Spelling; + if (!ImportedMod) { + Diag(AL.getLoc(), diag::err_profiles_require_not_on_import); + continue; + } - bool Found = llvm::any_of( - ImportedMod->EnforcedProfileDesignators, - [&](const Module::EnforcedProfile &EP) { - return EP.Designator == Desig; - }); + const auto &Args = AL.getProfileRequireArgs(); + StringRef Desig = Args.Designator.Spelling; - if (!Found) - Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; - } + bool Found = llvm::any_of( + ImportedMod->EnforcedProfileDesignators, + [&](const Module::EnforcedProfile &EP) { + return EP.Designator == Desig; + }); + + if (!Found) + Diag(AL.getLoc(), diag::err_profiles_require_not_enforced) << Desig; } } diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 9bcb818e75959..f3daa012c6fae 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -21,6 +21,9 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_gmf_only_no_leak.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_different_desig.cppm -o %t/mod_different_desig.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_two_modules.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -fmodule-file=DiffDesigMod=%t/mod_different_desig.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/mod_noflag_enforce.cppm -verify +// RUN: %clang_cc1 -std=c++20 -emit-module-interface %t/mod_bare.cppm -o %t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_noflag_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify // =================================================================== // Module with enforced profiles @@ -189,3 +192,30 @@ export void diff_func(); // expected-no-diagnostics import TestMod; import DiffDesigMod; + +// =================================================================== +// Without -fprofiles, [[profiles::enforce]] on a module-declaration +// must emit warn_attribute_ignored instead of being silently accepted. +// =================================================================== +//--- mod_noflag_enforce.cppm +export module NoFlagMod [[profiles::enforce(test::type_cast)]]; // expected-warning {{'profiles::enforce' attribute ignored}} + +export void f(); + +// =================================================================== +// A plain module with no profile attrs, built without -fprofiles, to +// serve as an import target for the require-without-flag test below. +// =================================================================== +//--- mod_bare.cppm +// expected-no-diagnostics +export module BareMod; + +export void bare_fn(); + +// =================================================================== +// Without -fprofiles, [[profiles::require]] on an import must emit +// warn_attribute_ignored and must NOT produce the spurious +// err_profiles_require_not_enforced diagnostic. +// =================================================================== +//--- import_noflag_require.cpp +import BareMod [[profiles::require(test::type_cast)]]; // expected-warning {{'profiles::require' attribute ignored}} From 335bbbb1d0db3ac5086f88c7f64bea8c81a9c699 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Apr 2026 13:05:15 -0400 Subject: [PATCH 057/289] Fix crash when missing left parenthesis --- .../clang/Basic/DiagnosticParseKinds.td | 2 ++ clang/include/clang/Parse/Parser.h | 12 +++---- clang/lib/Parse/ParseDeclCXX.cpp | 32 +++++++++++-------- clang/test/Parser/cxx-profiles-framework.cpp | 23 +++++++++++++ .../safety-profile-framework-modules.cppm | 18 +++++++++++ 5 files changed, 68 insertions(+), 19 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index fe50086f6743a..879ebcf6da564 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1893,6 +1893,8 @@ def err_hlsl_rootsig_non_zero_flag : Error<"flag value is neither a literal 0 no // C++ Profiles framework (P3589R2) def err_profiles_expected_profile_name : Error< "expected profile name">; +def err_profiles_expected_lparen : Error< + "%0 attribute requires an argument clause">; def err_profiles_expected_rparen : Error< "expected ')' in '%0' attribute">; def err_profiles_invalid_argument_token : Error< diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index dfdadc2d04c0c..ef96e2d040386 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2274,12 +2274,12 @@ class Parser : public CodeCompletionHandler { IdentifierInfo *ScopeName, SourceLocation ScopeLoc, ParsedAttr::Form Form); - bool ParseProfilesAttributeArgs(IdentifierInfo *AttrName, - SourceLocation AttrNameLoc, - ParsedAttributes &Attrs, - SourceLocation *EndLoc, - IdentifierInfo *ScopeName, - SourceLocation ScopeLoc); + bool TryParseProfilesAttribute(IdentifierInfo *AttrName, + SourceLocation AttrNameLoc, + ParsedAttributes &Attrs, + SourceLocation *EndLoc, + IdentifierInfo *ScopeName, + SourceLocation ScopeLoc); struct ParsedProfileDesignator { std::string Name; std::string Spelling; diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 3e683f7b7f687..8078e42082642 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -4502,12 +4502,6 @@ bool Parser::ParseCXX11AttributeArgs( return true; } - if (ScopeName && ScopeName->isStr("profiles")) { - if (ParseProfilesAttributeArgs(AttrName, AttrNameLoc, Attrs, EndLoc, - ScopeName, ScopeLoc)) - return true; - } - unsigned NumArgs; // Some Clang-scoped attributes have some special parsing behavior. if (ScopeName && (ScopeName->isStr("clang") || ScopeName->isStr("_Clang"))) @@ -4674,8 +4668,15 @@ void Parser::ParseCXX11AttributeSpecifierInternal(ParsedAttributes &Attrs, } } + // P3589R2 profile attributes need custom parsing for both the + // well-formed and the missing argument clause forms. + if (ScopeName && ScopeName->isStr("profiles") && + TryParseProfilesAttribute(AttrName, AttrLoc, Attrs, EndLoc, ScopeName, + ScopeLoc)) + AttrParsed = true; + // Parse attribute arguments - if (Tok.is(tok::l_paren)) + if (!AttrParsed && Tok.is(tok::l_paren)) AttrParsed = ParseCXX11AttributeArgs(AttrName, AttrLoc, Attrs, EndLoc, ScopeName, ScopeLoc, OpenMPTokens); @@ -5239,18 +5240,23 @@ bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { return false; } -bool Parser::ParseProfilesAttributeArgs(IdentifierInfo *AttrName, - SourceLocation AttrNameLoc, - ParsedAttributes &Attrs, - SourceLocation *EndLoc, - IdentifierInfo *ScopeName, - SourceLocation ScopeLoc) { +bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, + SourceLocation AttrNameLoc, + ParsedAttributes &Attrs, + SourceLocation *EndLoc, + IdentifierInfo *ScopeName, + SourceLocation ScopeLoc) { assert(ScopeName && ScopeName->isStr("profiles")); if (!AttrName->isStr("enforce") && !AttrName->isStr("suppress") && !AttrName->isStr("require")) return false; + if (Tok.isNot(tok::l_paren)) { + Diag(AttrNameLoc, diag::err_profiles_expected_lparen) << AttrName; + return true; + } + ConsumeParen(); AttributePool &Pool = Attrs.getPool(); diff --git a/clang/test/Parser/cxx-profiles-framework.cpp b/clang/test/Parser/cxx-profiles-framework.cpp index a799da89e0514..d1a4eb052680a 100644 --- a/clang/test/Parser/cxx-profiles-framework.cpp +++ b/clang/test/Parser/cxx-profiles-framework.cpp @@ -79,3 +79,26 @@ void suppress_bare_operator(); // Bare argument cannot be a balanced group [[profiles::suppress(std::type, (a b))]] // expected-error {{invalid token in profile argument}} void suppress_bare_group(); + +// =================================================================== +// Missing argument clause: must diagnose, not crash +// =================================================================== + +// enforce with no argument clause at TU scope +[[profiles::enforce]]; // expected-error {{'enforce' attribute requires an argument clause}} + +// suppress with no argument clause on a declaration +[[profiles::suppress]] void suppress_no_args_decl(); // expected-error {{'suppress' attribute requires an argument clause}} + +// require appearing on an empty-declaration with no argument clause: +// the no-l_paren diagnostic fires first; require-not-on-import is not +// reached. +[[profiles::require]]; // expected-error {{'require' attribute requires an argument clause}} + +// The [[using profiles: ...]] syntax must be gated identically. +[[using profiles: enforce]]; // expected-error {{'enforce' attribute requires an argument clause}} + +// Regression: an unknown profiles-scoped attribute without parens must +// continue to fall through to the generic "unknown attribute" warning +// and must not trigger the new "requires an argument clause" error. +[[profiles::bogus]]; // expected-warning {{unknown attribute 'profiles::bogus' ignored}} diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index f3daa012c6fae..0f689f55592b6 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -24,6 +24,8 @@ // RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/mod_noflag_enforce.cppm -verify // RUN: %clang_cc1 -std=c++20 -emit-module-interface %t/mod_bare.cppm -o %t/mod_bare.pcm -verify // RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_noflag_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/mod_enforce_no_args.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_require_no_args.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // =================================================================== // Module with enforced profiles @@ -219,3 +221,19 @@ export void bare_fn(); // =================================================================== //--- import_noflag_require.cpp import BareMod [[profiles::require(test::type_cast)]]; // expected-warning {{'profiles::require' attribute ignored}} + +// =================================================================== +// [[profiles::enforce]] on a module-declaration with no argument +// clause must be diagnosed, not crash. +// =================================================================== +//--- mod_enforce_no_args.cppm +export module NoArgsMod [[profiles::enforce]]; // expected-error {{'enforce' attribute requires an argument clause}} + +export void f(); + +// =================================================================== +// [[profiles::require]] on an import-declaration with no argument +// clause must be diagnosed, not crash. +// =================================================================== +//--- import_require_no_args.cpp +import TestMod [[profiles::require]]; // expected-error {{'require' attribute requires an argument clause}} From 1b74f2ce7b2ef00463540ac37c6fa532fb1e1b70 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Apr 2026 16:15:23 -0400 Subject: [PATCH 058/289] Late parsed members require walk of lexical parents --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 8 +-- clang/lib/Parse/ParseDeclCXX.cpp | 3 +- .../test/SemaCXX/safety-profile-type-cast.cpp | 66 +++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index c037ec4d09d6c..c6a028100cd81 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -376,8 +376,8 @@ void Parser::ParseLexedMethodDeclaration(LateParsedMethodDeclaration &LM) { // Start the delayed C++ method declaration Actions.ActOnStartDelayedCXXMethodDeclaration(getCurScope(), LM.Method); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, - LM.Method->getAsFunction()); + Sema::ProfileSuppressScope ProfileSuppressGuard( + Actions, LM.Method->getAsFunction(), /*WalkLexicalParents=*/true); // Introduce the parameters into scope and parse their default // arguments. @@ -607,8 +607,8 @@ void Parser::ParseLexedMethodDef(LexedMethod &LM) { Actions.ActOnStartOfFunctionDef(getCurScope(), LM.D); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, - LM.D->getAsFunction()); + Sema::ProfileSuppressScope ProfileSuppressGuard( + Actions, LM.D->getAsFunction(), /*WalkLexicalParents=*/true); llvm::scope_exit _([&]() { while (Tok.isNot(tok::eof)) diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 8078e42082642..5f8fd5beaf6b9 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -3275,7 +3275,8 @@ ExprResult Parser::ParseCXXMemberInitializer(Decl *D, bool IsFunction, : Sema::ExpressionEvaluationContext::PotentiallyEvaluated, D); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, D); + Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, D, + /*WalkLexicalParents=*/true); // CWG2760 // Default member initializers used to initialize a base or member subobject diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 098556df4f49e..b5d35fed72bd3 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -598,3 +598,69 @@ struct StaticInlineDataMemberWrongProfile { [[profiles::suppress(test::other)]] static inline int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} }; + +// Suppress on a nested (non-template) class must reach its late-parsed inline +// method bodies, NSDMIs, and default arguments even though the nested class's +// body is parsed before the outer class's members are late-parsed. +struct NestedSuppressInnerOuter { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + struct [[profiles::suppress(test::type_cast)]] Inner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; +}; + +// Non-matching profile on the nested class does not suppress the violation. +struct NestedSuppressWrongProfile { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + struct [[profiles::suppress(test::other)]] Inner { + void f() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + } + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + void g(int *q = reinterpret_cast(0)); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; +}; + +// Suppress on the outer class extends to inline bodies / NSDMIs / default +// args of a nested non-template class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] OuterSuppressNested { + struct Inner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; +}; + +// Suppress on an enclosing namespace reaches a nested non-template class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::type_cast)]] nested_suppress_ns { + struct Inner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; +} + +// Deeply nested: suppress on the middle class reaches the innermost class's +// inline body. +struct DeepOuter { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + struct [[profiles::suppress(test::type_cast)]] DeepMiddle { + struct DeepInner { + void f() { + int *p = reinterpret_cast(0); + } + int *p = reinterpret_cast(0); + void g(int *q = reinterpret_cast(0)); + }; + }; +}; From 8374ac9a2fbb80b52e20bfc9befaa7c235025853 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Apr 2026 17:01:32 -0400 Subject: [PATCH 059/289] Fix suppress on non-generic lambda --- clang/lib/Parse/ParseExprCXX.cpp | 7 ++- .../test/SemaCXX/safety-profile-type-cast.cpp | 45 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp index b3d50daf66b10..b52ca3a18092e 100644 --- a/clang/lib/Parse/ParseExprCXX.cpp +++ b/clang/lib/Parse/ParseExprCXX.cpp @@ -1482,7 +1482,12 @@ ExprResult Parser::ParseLambdaExpressionAfterIntroducer( return ExprError(); } - StmtResult Stmt(ParseCompoundStatementBody()); + StmtResult Stmt; + { + Sema::ProfileSuppressScope ProfileSuppressGuard( + Actions, Actions.getCurLambda()->CallOperator); + Stmt = ParseCompoundStatementBody(); + } BodyScope.Exit(); TemplateParamScope.Exit(); LambdaScope.Exit(); diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index b5d35fed72bd3..f10701bba6ce1 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -1,5 +1,5 @@ -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++20 %s -// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++20 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::type_cast)]]; // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} @@ -496,6 +496,47 @@ void test_wrong_profile_lambda() { l(0); // expected-note {{in instantiation of function template specialization 'get_wrong_profile_lambda()::(lambda)::operator()' requested here}} } +// Direct suppress on a non-generic lambda's declarator applies to its body. +// The attribute precedes the parameter list so it appertains to the call +// operator declaration (P2173; C++23 standard). +void test_nongeneric_lambda_direct_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::type_cast)]] () { + int *p = reinterpret_cast(0); + }; + l(); +} + +// Direct suppress of a non-matching profile on a non-generic lambda does not +// suppress the violation. +void test_nongeneric_lambda_direct_suppress_mismatch() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::other)]] () { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + l(); +} + +// Rule-based direct suppress on a non-generic lambda applies when the rule +// matches. +void test_nongeneric_lambda_direct_suppress_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::type_cast, rule: "reinterpret_cast")]] () { + int *p = reinterpret_cast(0); + }; + l(); +} + +// Rule-based direct suppress on a non-generic lambda does not suppress a +// non-matching rule. +void test_nongeneric_lambda_direct_suppress_wrong_rule() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + auto l = [] [[profiles::suppress(test::type_cast, rule: "static_cast")]] () { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + }; + l(); +} + // Suppress on an inline member function definition applies to the body. struct InlineMethodSuppress { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} From 15ea59f2b2f1b57a28848eae20d63808dda73ea6 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 24 Apr 2026 20:21:45 -0400 Subject: [PATCH 060/289] Introduce igsuppressed-on-SFINAE diagnostic category for profiles --- clang/docs/ProfilesFramework.rst | 10 ++++++---- clang/include/clang/Basic/DiagnosticSemaKinds.td | 6 +++++- clang/lib/Sema/Sema.cpp | 2 -- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 4dd38c133c53d..ae6189bf9bde6 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -82,10 +82,12 @@ At each semantic site where a rule can be violated, call checkProfileViolation("my::profile", "my_rule", Loc, diag::err_my_profile_rule); -This function checks whether the profile is enforced and not suppressed, -skips SFINAE / unevaluated / discarded-statement contexts (so that profiles -cannot affect overload resolution or template instantiation), and emits the -diagnostic if appropriate. The profile name is passed as ``%0``. +This function checks whether the profile is enforced and not suppressed. During +template argument deduction, profile rule diagnostics are suppressed by Clang's +normal SFINAE machinery so they cannot affect overload resolution or template +instantiation; selected specializations replay suppressed diagnostics when used. +Unevaluated and discarded-statement contexts are skipped. The profile name is +passed as ``%0``. Framework Internals Reference diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index dfd373b174bf0..08773c76be66a 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14116,6 +14116,10 @@ def err_cuda_device_kernel_launch_require_rdc : Error<"kernel launch from __device__ or __global__ function requires " "relocatable device code (i.e. requires -fgpu-rdc)">; // C++ Profiles framework (P3589R2) +class ProfileRuleError : Error { + let SFINAE = SFINAE_Suppress; +} + def err_profiles_enforce_not_empty_decl : Error< "'profiles::enforce' attribute only allowed on empty-declarations and module-declarations">; def err_profiles_enforce_not_at_tu_scope : Error< @@ -14130,6 +14134,6 @@ def err_profiles_require_not_enforced : Error< "required profile '%0' is not enforced by imported module">; def err_profiles_suppress_justification_not_string : Error< "'justification' argument of 'profiles::suppress' must be a string literal">; -def err_profile_type_cast_reinterpret : Error< +def err_profile_type_cast_reinterpret : ProfileRuleError< "'reinterpret_cast' is unsafe under profile '%0'">; } // end of sema component. diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index e2d076abbd86f..e9709d595a3a6 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3082,8 +3082,6 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, // outcome of overload resolution or template instantiation, nor is it // possible to 'SFINAE out' failure of a program to satisfy a profile // requirement." - if (isSFINAEContext()) - return false; if (isUnevaluatedContext()) return false; if (currentEvaluationContext().isDiscardedStatementContext()) From cac2e1a21b150b3c6644a8dda48ec3c54ef3171e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 24 Apr 2026 21:03:05 -0400 Subject: [PATCH 061/289] Refactor argument storage (again) --- clang/include/clang/AST/Attr.h | 1 + clang/include/clang/Basic/Attr.td | 18 +++++- clang/include/clang/Basic/Profiles.h | 41 +++++++++++++ clang/include/clang/Parse/Parser.h | 23 ++++++- clang/include/clang/Sema/ParsedAttr.h | 3 + clang/include/clang/Sema/Sema.h | 10 ++- clang/lib/Parse/ParseDeclCXX.cpp | 87 +++++++++++++++++++++------ clang/lib/Sema/Sema.cpp | 43 +++++++++++-- clang/lib/Sema/SemaDeclAttr.cpp | 14 ++++- clang/lib/Sema/SemaLambda.cpp | 5 +- 10 files changed, 212 insertions(+), 33 deletions(-) create mode 100644 clang/include/clang/Basic/Profiles.h diff --git a/clang/include/clang/AST/Attr.h b/clang/include/clang/AST/Attr.h index 0f9fc01391c30..52e294326abe4 100644 --- a/clang/include/clang/AST/Attr.h +++ b/clang/include/clang/AST/Attr.h @@ -23,6 +23,7 @@ #include "clang/Basic/LLVM.h" #include "clang/Basic/LangOptions.h" #include "clang/Basic/OpenMPKinds.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/Sanitizers.h" #include "clang/Basic/SourceLocation.h" #include "clang/Support/Compiler.h" diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index d016ec835fbe2..901f4b7f5f9ad 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -3462,7 +3462,11 @@ def ProfilesEnforce : Attr { let Spellings = [CXX11<"profiles", "enforce">]; let Args = [ VariadicStringArgument<"ProfileNames">, - VariadicStringArgument<"ProfileDesignators"> + VariadicStringArgument<"ProfileDesignators">, + VariadicUnsignedArgument<"ProfileArgumentCounts">, + VariadicStringArgument<"ProfileArgumentKeys">, + VariadicStringArgument<"ProfileArgumentValues">, + VariadicUnsignedArgument<"ProfileArgumentKinds"> ]; let LangOpts = [Profiles]; let HasCustomParsing = 1; @@ -3475,7 +3479,10 @@ def ProfilesSuppress : DeclOrStmtAttr { StringArgument<"ProfileName">, StringArgument<"Justification", 1>, StringArgument<"Rule", 1>, - VariadicStringArgument<"RawArguments"> + VariadicStringArgument<"RawArguments">, + VariadicStringArgument<"RawArgumentKeys">, + VariadicStringArgument<"RawArgumentValues">, + VariadicUnsignedArgument<"RawArgumentKinds"> ]; let LangOpts = [Profiles]; let HasCustomParsing = 1; @@ -3484,7 +3491,12 @@ def ProfilesSuppress : DeclOrStmtAttr { def ProfilesRequire : Attr { let Spellings = [CXX11<"profiles", "require">]; - let Args = [StringArgument<"Designator">]; + let Args = [ + StringArgument<"Designator">, + VariadicStringArgument<"DesignatorArgumentKeys">, + VariadicStringArgument<"DesignatorArgumentValues">, + VariadicUnsignedArgument<"DesignatorArgumentKinds"> + ]; let LangOpts = [Profiles]; let HasCustomParsing = 1; let Documentation = [ProfilesRequireDocs]; diff --git a/clang/include/clang/Basic/Profiles.h b/clang/include/clang/Basic/Profiles.h new file mode 100644 index 0000000000000..1d7a317760f1a --- /dev/null +++ b/clang/include/clang/Basic/Profiles.h @@ -0,0 +1,41 @@ +//===--- Profiles.h - C++ profiles framework helpers -----------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_BASIC_PROFILES_H +#define LLVM_CLANG_BASIC_PROFILES_H + +#include "clang/Basic/SourceLocation.h" +#include "llvm/ADT/StringRef.h" +#include + +namespace clang::profiles { + +enum class ProfileArgumentKind : unsigned { + Positional = 0, + Named = 1, +}; + +struct ProfileArgument { + llvm::StringRef Key; + llvm::StringRef Value; + ProfileArgumentKind Kind = ProfileArgumentKind::Positional; + SourceRange Range; + + bool isNamed() const { return Kind == ProfileArgumentKind::Named; } +}; + +inline std::string getCanonicalProfileArgumentSpelling( + const ProfileArgument &Argument) { + if (!Argument.isNamed()) + return Argument.Value.str(); + return (Argument.Key + " : " + Argument.Value).str(); +} + +} // namespace clang::profiles + +#endif // LLVM_CLANG_BASIC_PROFILES_H diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index ef96e2d040386..bfaa108a72115 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -15,6 +15,7 @@ #include "clang/Basic/OpenACCKinds.h" #include "clang/Basic/OperatorPrecedence.h" +#include "clang/Basic/Profiles.h" #include "clang/Lex/CodeCompletionHandler.h" #include "clang/Lex/Preprocessor.h" #include "clang/Sema/Sema.h" @@ -2283,22 +2284,38 @@ class Parser : public CodeCompletionHandler { struct ParsedProfileDesignator { std::string Name; std::string Spelling; + struct Argument { + std::string Key; + std::string Value; + profiles::ProfileArgumentKind Kind = + profiles::ProfileArgumentKind::Positional; + SourceRange Range; + + bool isNamed() const { + return Kind == profiles::ProfileArgumentKind::Named; + } + }; + SmallVector Arguments; }; struct ParsedProfileSuppressArgs { std::string Name; std::string Justification; std::string Rule; SmallVector RawArguments; + SmallVector Arguments; }; bool ParseProfileName(std::string &Name); bool ParseProfileDesignator(ParsedProfileDesignator &Designator); bool ParseProfileDesignatorList( SmallVectorImpl &Designators); - bool ParseProfileArgumentList(SmallVectorImpl &Args); + bool ParseProfileArgumentList( + SmallVectorImpl &Args); bool ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args); - bool ParseNonCommaBalancedToken(std::string &Spelling); - bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling); + bool ParseNonCommaBalancedToken(std::string &Spelling, + SourceRange *Range = nullptr); + bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling, + SourceRange *Range = nullptr); void MaybeParseCXX11Attributes(Declarator &D) { if (isAllowedCXX11AttributeSpecifier()) { diff --git a/clang/include/clang/Sema/ParsedAttr.h b/clang/include/clang/Sema/ParsedAttr.h index 91f186f420d6f..9ed3f26c4d774 100644 --- a/clang/include/clang/Sema/ParsedAttr.h +++ b/clang/include/clang/Sema/ParsedAttr.h @@ -18,6 +18,7 @@ #include "clang/Basic/AttributeCommonInfo.h" #include "clang/Basic/Diagnostic.h" #include "clang/Basic/ParsedAttrInfo.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Sema/Ownership.h" #include "llvm/ADT/PointerUnion.h" @@ -99,6 +100,7 @@ struct PropertyData { struct ProfileDesignator { StringRef Name; StringRef Spelling; + ArrayRef Arguments; }; struct ProfileEnforceArgs { @@ -110,6 +112,7 @@ struct ProfileSuppressArgs { StringRef Justification; StringRef Rule; ArrayRef RawArguments; + ArrayRef Arguments; }; struct ProfileRequireArgs { diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index b108a5e47ab31..cda3d1b0e71bc 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1056,7 +1056,15 @@ class Sema final : public SemaBase { SourceLocation Loc); bool processProfilesEnforceAttr(const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, - SmallVectorImpl *NewDesignators); + SmallVectorImpl *NewDesignators, + SmallVectorImpl *NewArgumentCounts = + nullptr, + SmallVectorImpl *NewArgumentKeys = + nullptr, + SmallVectorImpl *NewArgumentValues = + nullptr, + SmallVectorImpl *NewArgumentKinds = + nullptr); ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 5f8fd5beaf6b9..b1f0b64e38f5e 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5067,7 +5067,33 @@ bool Parser::ParseProfileName(std::string &Name) { return false; } -bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { +template +static std::string +getCanonicalProfileArgumentSpelling(const ProfileArgument &Argument) { + if (!Argument.isNamed()) + return Argument.Value; + return Argument.Key + " : " + Argument.Value; +} + +template +static ArrayRef copyProfileArguments( + AttributePool &Pool, const ProfileArguments &Parsed) { + if (Parsed.empty()) + return {}; + + auto Args = Pool.allocateArray(Parsed.size()); + for (unsigned I = 0; I < Parsed.size(); ++I) { + Args[I].Key = Pool.copyString(Parsed[I].Key); + Args[I].Value = Pool.copyString(Parsed[I].Value); + Args[I].Kind = Parsed[I].Kind; + Args[I].Range = Parsed[I].Range; + } + return Args; +} + +bool Parser::ParseNonCommaBalancedToken(std::string &Spelling, + SourceRange *Range) { + SourceLocation StartLoc = Tok.getLocation(); if (Tok.isOneOf(tok::l_paren, tok::l_square, tok::l_brace)) { tok::TokenKind Close; Spelling = PP.getSpelling(Tok); @@ -5088,6 +5114,8 @@ bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { Spelling += " "; Spelling += PP.getSpelling(T); } + if (Range) + *Range = SourceRange(StartLoc, Toks.back().getLocation()); return false; } @@ -5098,39 +5126,48 @@ bool Parser::ParseNonCommaBalancedToken(std::string &Spelling) { } Spelling = PP.getSpelling(Tok); + if (Range) + *Range = SourceRange(StartLoc, Tok.getLocation()); ConsumeAnyToken(); return false; } -bool Parser::ParseNonOperatorNonPunctuatorToken(std::string &Spelling) { +bool Parser::ParseNonOperatorNonPunctuatorToken(std::string &Spelling, + SourceRange *Range) { // P3589R2 [dcl.attr.profiles]: A bare profile-argument is a // non-operator-non-punctuator-token. if (tok::getPunctuatorSpelling(Tok.getKind()) || Tok.is(tok::eof)) { Diag(Tok, diag::err_profiles_invalid_argument_token); return true; } + SourceLocation Loc = Tok.getLocation(); Spelling = PP.getSpelling(Tok); + if (Range) + *Range = SourceRange(Loc, Loc); ConsumeAnyToken(); return false; } -bool Parser::ParseProfileArgumentList(SmallVectorImpl &Args) { +bool Parser::ParseProfileArgumentList( + SmallVectorImpl &Args) { while (true) { + ParsedProfileDesignator::Argument Arg; if (Tok.is(tok::identifier) && NextToken().is(tok::colon)) { - std::string Key = Tok.getIdentifierInfo()->getName().str(); + SourceLocation KeyLoc = Tok.getLocation(); + Arg.Key = Tok.getIdentifierInfo()->getName().str(); + Arg.Kind = profiles::ProfileArgumentKind::Named; ConsumeToken(); ConsumeToken(); - std::string Value; - if (ParseNonCommaBalancedToken(Value)) + SourceRange ValueRange; + if (ParseNonCommaBalancedToken(Arg.Value, &ValueRange)) return true; - Args.push_back(Key + " : " + Value); + Arg.Range = SourceRange(KeyLoc, ValueRange.getEnd()); } else { - std::string Spelling; - if (ParseNonOperatorNonPunctuatorToken(Spelling)) + if (ParseNonOperatorNonPunctuatorToken(Arg.Value, &Arg.Range)) return true; - Args.push_back(std::move(Spelling)); } + Args.push_back(std::move(Arg)); if (!TryConsumeToken(tok::comma)) break; @@ -5150,14 +5187,13 @@ bool Parser::ParseProfileDesignator(ParsedProfileDesignator &D) { D.Spelling += "("; ConsumeParen(); - SmallVector Args; - if (ParseProfileArgumentList(Args)) + if (ParseProfileArgumentList(D.Arguments)) return true; - for (unsigned I = 0; I < Args.size(); ++I) { + for (unsigned I = 0; I < D.Arguments.size(); ++I) { if (I > 0) D.Spelling += ", "; - D.Spelling += Args[I]; + D.Spelling += getCanonicalProfileArgumentSpelling(D.Arguments[I]); } if (!Tok.is(tok::r_paren)) { @@ -5202,14 +5238,16 @@ bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { while (TryConsumeToken(tok::comma)) { if (!Tok.is(tok::identifier) || !NextToken().is(tok::colon)) { - std::string Spelling; - if (ParseNonOperatorNonPunctuatorToken(Spelling)) + ParsedProfileDesignator::Argument Arg; + if (ParseNonOperatorNonPunctuatorToken(Arg.Value, &Arg.Range)) return true; - Args.RawArguments.push_back(std::move(Spelling)); + Args.RawArguments.push_back(getCanonicalProfileArgumentSpelling(Arg)); + Args.Arguments.push_back(std::move(Arg)); continue; } IdentifierInfo *KeyII = Tok.getIdentifierInfo(); + SourceLocation KeyLoc = Tok.getLocation(); ConsumeToken(); ConsumeToken(); @@ -5233,9 +5271,16 @@ bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { } std::string Value; - if (ParseNonCommaBalancedToken(Value)) + SourceRange ValueRange; + if (ParseNonCommaBalancedToken(Value, &ValueRange)) return true; - Args.RawArguments.push_back(KeyII->getName().str() + " : " + Value); + ParsedProfileDesignator::Argument Arg; + Arg.Key = KeyII->getName().str(); + Arg.Value = std::move(Value); + Arg.Kind = profiles::ProfileArgumentKind::Named; + Arg.Range = SourceRange(KeyLoc, ValueRange.getEnd()); + Args.RawArguments.push_back(getCanonicalProfileArgumentSpelling(Arg)); + Args.Arguments.push_back(std::move(Arg)); } return false; @@ -5279,6 +5324,7 @@ bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, for (unsigned I = 0; I < Parsed.size(); ++I) { Desigs[I].Name = Pool.copyString(Parsed[I].Name); Desigs[I].Spelling = Pool.copyString(Parsed[I].Spelling); + Desigs[I].Arguments = copyProfileArguments(Pool, Parsed[I].Arguments); } auto *Args = Pool.make(); @@ -5299,6 +5345,7 @@ bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, RawBuf[I] = Pool.copyString(Parsed.RawArguments[I]); Args->RawArguments = RawBuf; } + Args->Arguments = copyProfileArguments(Pool, Parsed.Arguments); CustomData = Args; } else { ParsedProfileDesignator Parsed; @@ -5308,6 +5355,8 @@ bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, auto *Args = Pool.make(); Args->Designator.Name = Pool.copyString(Parsed.Name); Args->Designator.Spelling = Pool.copyString(Parsed.Spelling); + Args->Designator.Arguments = + copyProfileArguments(Pool, Parsed.Arguments); CustomData = Args; } diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index e9709d595a3a6..3fd220f2479ff 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3012,10 +3012,31 @@ bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, return true; } +static void appendProfileArgumentData( + ArrayRef Arguments, + SmallVectorImpl *ArgumentCounts, + SmallVectorImpl *ArgumentKeys, + SmallVectorImpl *ArgumentValues, + SmallVectorImpl *ArgumentKinds) { + if (!ArgumentCounts) + return; + + assert(ArgumentKeys && ArgumentValues && ArgumentKinds); + ArgumentCounts->push_back(Arguments.size()); + for (const auto &Arg : Arguments) { + ArgumentKeys->push_back(Arg.Key); + ArgumentValues->push_back(Arg.Value); + ArgumentKinds->push_back(static_cast(Arg.Kind)); + } +} + bool Sema::processProfilesEnforceAttr( - const ParsedAttr &AL, Module *Mod, - SmallVectorImpl *NewNames, - SmallVectorImpl *NewDesignators) { + const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators, + SmallVectorImpl *NewArgumentCounts, + SmallVectorImpl *NewArgumentKeys, + SmallVectorImpl *NewArgumentValues, + SmallVectorImpl *NewArgumentKinds) { const auto &Args = AL.getProfileEnforceArgs(); if (Args.Designators.empty()) { Diag(AL.getLoc(), diag::err_attribute_too_few_arguments) << AL << 1; @@ -3041,6 +3062,9 @@ bool Sema::processProfilesEnforceAttr( NewNames->push_back(Name); if (NewDesignators) NewDesignators->push_back(Spelling); + appendProfileArgumentData(D.Arguments, NewArgumentCounts, + NewArgumentKeys, NewArgumentValues, + NewArgumentKinds); } } return true; @@ -3054,10 +3078,21 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { SmallVector RawArgs; for (const auto &Arg : Args.RawArguments) RawArgs.push_back(Arg); + SmallVector RawArgumentKeys; + SmallVector RawArgumentValues; + SmallVector RawArgumentKinds; + for (const auto &Arg : Args.Arguments) { + RawArgumentKeys.push_back(Arg.Key); + RawArgumentValues.push_back(Arg.Value); + RawArgumentKinds.push_back(static_cast(Arg.Kind)); + } return ::new (Context) ProfilesSuppressAttr( Context, AL, Args.Name, Args.Justification, Args.Rule, - RawArgs.data(), RawArgs.size()); + RawArgs.data(), RawArgs.size(), RawArgumentKeys.data(), + RawArgumentKeys.size(), RawArgumentValues.data(), + RawArgumentValues.size(), RawArgumentKinds.data(), + RawArgumentKinds.size()); } bool Sema::isProfileSuppressed(StringRef ProfileName, diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index c351ec88d8f8f..158f0d2d2b144 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5483,12 +5483,22 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { Mod = M; SmallVector Names, Designators; - if (!S.processProfilesEnforceAttr(AL, Mod, &Names, &Designators)) + SmallVector ArgumentCounts, ArgumentKinds; + SmallVector ArgumentKeys, ArgumentValues; + if (!S.processProfilesEnforceAttr(AL, Mod, &Names, &Designators, + &ArgumentCounts, &ArgumentKeys, + &ArgumentValues, &ArgumentKinds)) return; D->addAttr(::new (S.Context) ProfilesEnforceAttr(S.Context, AL, Names.data(), Names.size(), - Designators.data(), Designators.size())); + Designators.data(), Designators.size(), + ArgumentCounts.data(), + ArgumentCounts.size(), ArgumentKeys.data(), + ArgumentKeys.size(), ArgumentValues.data(), + ArgumentValues.size(), + ArgumentKinds.data(), + ArgumentKinds.size())); } static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index 271aaacc8f4a4..2791dce4273a3 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -1510,7 +1510,10 @@ void Sema::ActOnStartOfLambdaDefinition(LambdaIntroducer &Intro, for (const auto &E : ProfileSuppressStack) Method->addAttr(ProfilesSuppressAttr::CreateImplicit( Context, E.ProfileName, /*Justification=*/"", E.RuleName, - /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0)); + /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0, + /*RawArgumentKeys=*/nullptr, /*RawArgumentKeysSize=*/0, + /*RawArgumentValues=*/nullptr, /*RawArgumentValuesSize=*/0, + /*RawArgumentKinds=*/nullptr, /*RawArgumentKindsSize=*/0)); } if (Context.getTargetInfo().getTriple().isAArch64()) From 9073d19af96f6900abc304fe74071db1804cb35c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 29 Apr 2026 19:08:03 -0400 Subject: [PATCH 062/289] Document intentional gaps in implementation --- clang/docs/ProfilesFramework.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index ae6189bf9bde6..a9c41c6615f34 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -143,6 +143,22 @@ do not need to add any serialization code. - **Module BMI**: ``Module::EnforcedProfileDesignators`` is written as ``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. +Intentional Omissions +===================== + +The following parts of P3589R2 are deliberately not implemented: + +- ``[[profiles::exempt(...)]]`` (P3589R2 section 1.1.6), which would exempt + named included source files from profile enforcement. Implementing it + requires bookkeeping that connects the original spelling of an ``#include`` + to the source locations of constructs in the included file, and the feature + is not needed to exercise or validate the rest of the framework. +- The redeclaration consistency rule from P3589R2 section 2.2 paragraph 5 + (every redeclaration of a declaration in the dominion of a profile must + itself appear in the dominion of a compatible profile). Profile attributes + on redeclarations are parsed and recorded, but no cross-redeclaration + compatibility check is performed. + The ``test::type_cast`` Profile =============================== From 3f2de5890e9727354ea39a3980e5c6a94b7e8743 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 4 May 2026 14:51:29 -0400 Subject: [PATCH 063/289] Support suppression of analysis based warnings --- clang/docs/ProfilesFramework.rst | 15 ++ .../clang/Basic/DiagnosticSemaKinds.td | 2 + clang/include/clang/Sema/Sema.h | 12 ++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 16 +- clang/lib/Sema/Sema.cpp | 83 ++++++++++- .../SemaCXX/safety-profile-uninit-read.cpp | 141 ++++++++++++++++++ 6 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-uninit-read.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index a9c41c6615f34..6f29c5993db87 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -90,6 +90,21 @@ Unevaluated and discarded-statement contexts are skipped. The profile name is passed as ``%0``. +Suppression Dominion is Token-Based +=================================== + +A ``[[profiles::suppress(P)]]`` attribute suppresses profile ``P`` in the +token range of the declaration or statement it appertains to -- nothing more. +For a variable declaration that range covers the initializer expression (so +``[[profiles::suppress(P)]] T x = init();`` silences violations inside +``init()``), but it does *not* tag the variable as permitted-uninitialized for +subsequent uses; those uses appear in different declarations or statements +and are checked normally at their own source location. Profiles that need +per-object "opt-out of this check everywhere this value is used" semantics +(for example, the proposed ``[[uninitialized]]`` attribute of the +initialization profile) must introduce their own, separate, decl-scoped +marker. + Framework Internals Reference ============================= diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 08773c76be66a..b6a77d406fafb 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14136,4 +14136,6 @@ def err_profiles_suppress_justification_not_string : Error< "'justification' argument of 'profiles::suppress' must be a string literal">; def err_profile_type_cast_reinterpret : ProfileRuleError< "'reinterpret_cast' is unsafe under profile '%0'">; +def err_profile_uninit_read : ProfileRuleError< + "variable %1 is read before initialization under profile '%0'">; } // end of sema component. diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index cda3d1b0e71bc..d87d8a87d9d44 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -127,6 +127,7 @@ class ASTWriter; class CXXBasePath; class CXXBasePaths; class CXXFieldCollector; +class AnalysisDeclContext; class CodeCompleteConsumer; enum class ComparisonCategoryType : unsigned char; class ConstraintSatisfaction; @@ -1070,9 +1071,20 @@ class Sema final : public SemaBase { bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName = "") const; + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Stmt *S, AnalysisDeclContext &AC) const; + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc); + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + const Stmt *UseStmt, + AnalysisDeclContext &AC) const; bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); + bool anyProfileRequestsCFGUninitAnalysis() const; + bool tryEmitCFGUninitAnalysisDiagnostic(const VarDecl *VD, const Expr *UseExpr, + AnalysisDeclContext &AC); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 7ed9b43b76d9d..6fed7d1b399e2 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1688,6 +1688,7 @@ struct SortDiagBySourceLocation { namespace { class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; + AnalysisDeclContext &AC; typedef SmallVector UsesVec; typedef llvm::PointerIntPair MappedType; // Prefer using MapVector to DenseMap, so that iteration order will be @@ -1697,7 +1698,7 @@ class UninitValsDiagReporter : public UninitVariablesHandler { UsesMap uses; public: - UninitValsDiagReporter(Sema &S) : S(S) {} + UninitValsDiagReporter(Sema &S, AnalysisDeclContext &AC) : S(S), AC(AC) {} ~UninitValsDiagReporter() override { flushDiagnostics(); } MappedType &getUses(const VarDecl *vd) { @@ -1745,6 +1746,14 @@ class UninitValsDiagReporter : public UninitVariablesHandler { // diagnostic is printed, further diagnostics for this variable are skipped. void diagnoseUnitializedVar(const VarDecl *vd, bool hasSelfInit, UsesVec *vec) { + // If a CFG-uninit-analysis-requesting profile is enforced at any real + // use site and is not suppressed there, emit the profile diagnostic + // and skip the default warning path entirely. + for (const auto &U : *vec) { + if (S.tryEmitCFGUninitAnalysisDiagnostic(vd, U.getUser(), AC)) + return; + } + // Specially handle the case where we have uses of an uninitialized // variable, but the root cause is an idiomatic self-init. We want // to report the diagnostic at the self-init since that is the root cause. @@ -3302,13 +3311,14 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( Analyzer.run(AC); } - if (!Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || + if (S.anyProfileRequestsCFGUninitAnalysis() || + !Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_sometimes_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_maybe_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_uninit_const_reference, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_uninit_const_pointer, D->getBeginLoc())) { if (CFG *cfg = AC.getCFG()) { - UninitValsDiagReporter reporter(S); + UninitValsDiagReporter reporter(S, AC); UninitVariablesAnalysisStats stats; std::memset(&stats, 0, sizeof(UninitVariablesAnalysisStats)); runUninitializedVariablesAnalysis(*cast(D), *cfg, AC, diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 3fd220f2479ff..4a04b62e535d8 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -20,9 +20,11 @@ #include "clang/AST/DeclObjC.h" #include "clang/AST/Expr.h" #include "clang/AST/ExprCXX.h" +#include "clang/AST/ParentMap.h" #include "clang/AST/PrettyDeclStackTrace.h" #include "clang/AST/StmtCXX.h" #include "clang/AST/TypeOrdering.h" +#include "clang/Analysis/AnalysisDeclContext.h" #include "clang/Basic/DarwinSDKInfo.h" #include "clang/Basic/DiagnosticOptions.h" #include "clang/Basic/PartialDiagnostic.h" @@ -3095,19 +3097,55 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { RawArgumentKinds.size()); } +static bool profileSuppressMatches(StringRef EntryProfile, StringRef EntryRule, + StringRef Profile, StringRef Rule) { + return EntryProfile == Profile && + (EntryRule.empty() || EntryRule == Rule); +} + bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName) const { - for (const auto &E : ProfileSuppressStack) { - if (E.ProfileName != ProfileName) - continue; - if (E.RuleName.empty() || E.RuleName == RuleName) + for (const auto &E : ProfileSuppressStack) + if (profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, + RuleName)) return true; + return false; +} + +bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Stmt *S, + AnalysisDeclContext &AC) const { + ParentMap &PM = AC.getParentMap(); + for (const Stmt *Cur = S; Cur; Cur = PM.getParent(Cur)) { + if (const auto *AS = dyn_cast(Cur)) + for (const Attr *A : AS->getAttrs()) + if (const auto *PSA = dyn_cast(A)) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + // [[profiles::suppress]] on a local variable attaches to the VarDecl, + // not the enclosing DeclStmt. Walk the declared decls so the post-parse + // walker matches the parse-time ProfileSuppressForInit RAII behavior. + if (const auto *DS = dyn_cast(Cur)) + for (const Decl *D : DS->decls()) + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + } + for (const Decl *D = AC.getDecl(); D;) { + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + const DeclContext *DC = D->getLexicalDeclContext(); + D = DC ? dyn_cast(DC) : nullptr; } return false; } -bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc, unsigned DiagID) { +bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc) { if (!isProfileEnforced(ProfileName)) return false; if (isProfileSuppressed(ProfileName, RuleName)) @@ -3121,10 +3159,43 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, return false; if (currentEvaluationContext().isDiscardedStatementContext()) return false; + return true; +} + +bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + const Stmt *UseStmt, + AnalysisDeclContext &AC) const { + if (!isProfileEnforced(ProfileName)) + return false; + if (isProfileSuppressed(ProfileName, RuleName, UseStmt, AC)) + return false; + return true; +} + +bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, unsigned DiagID) { + if (!shouldEmitProfileViolation(ProfileName, RuleName, Loc)) + return false; Diag(Loc, DiagID) << ProfileName; return true; } +bool Sema::anyProfileRequestsCFGUninitAnalysis() const { + return isProfileEnforced("test::uninit_read"); +} + +bool Sema::tryEmitCFGUninitAnalysisDiagnostic(const VarDecl *VD, + const Expr *UseExpr, + AnalysisDeclContext &AC) { + static constexpr StringRef Profile = "test::uninit_read"; + if (!shouldEmitProfileViolation(Profile, /*RuleName=*/"", UseExpr, AC)) + return false; + Diag(UseExpr->getBeginLoc(), diag::err_profile_uninit_read) + << Profile << VD->getDeclName(); + Diag(VD->getLocation(), diag::note_var_declared_here) << VD->getDeclName(); + return true; +} + Sema::ProfileSuppressScope::ProfileSuppressScope( Sema &S, const ParsedAttributesView &Attrs) : S(S) { diff --git a/clang/test/SemaCXX/safety-profile-uninit-read.cpp b/clang/test/SemaCXX/safety-profile-uninit-read.cpp new file mode 100644 index 0000000000000..cc5ac810985c4 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-uninit-read.cpp @@ -0,0 +1,141 @@ +// Each CASE selects one violation test so the analysis-based-warnings early +// exit on first error doesn't hide later cases. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=0 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=1 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=2 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=3 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=4 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=5 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=6 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized -DCASE=0 %s + +#if CASE == 0 +// expected-no-diagnostics +#endif + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::uninit_read)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; + +// Cases that never diagnose are always compiled. + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::uninit_read)]] +void test_suppress_decl() { + int x; + int y = x; + (void)y; +} + +void test_suppress_stmt_inner() { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } +} + +void test_suppress_var_init() { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] int y = x; + (void)y; +} + +void test_discarded_branch() { + int x; + if constexpr (false) { + int y = x; + (void)y; + } +} + +void test_unevaluated() { + int x; + using T = decltype(x); + (void)sizeof(x); + (void)static_cast(0); +} + +void test_param(int p) { + int y = p; + (void)y; +} + +int g_static; +void test_global() { + int y = g_static; + (void)y; +} + +void test_self_init_no_use() { + int x = x; + (void)&x; +} + +void test_suppress_self_init() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::uninit_read)]] int x = x; + (void)&x; +} + +#if CASE == 1 +void test_violation() { + int x; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; +} +#endif + +#if CASE == 2 +void test_suppress_stmt_outer() { + int x; // expected-note {{variable 'x' is declared here}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } + int z = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)z; +} +#endif + +#if CASE == 3 +template +T template_uninit() { + T x; // expected-note {{variable 'x' is declared here}} + return x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} +} +void instantiate_template_uninit() { + template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} +} +#endif + +#if CASE == 4 +void test_self_init_with_use() { + int x = x; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; +} +#endif + +#if CASE == 5 +void test_selective_suppress() { + int x; // expected-note {{variable 'x' is declared here}} + [[profiles::suppress(test::other)]] { + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; + } +} +#endif + +#if CASE == 6 +// Suppress on a declaration is token-based: it covers the initializer of +// that declaration but not later uses that live in different declarations. +void test_decl_suppress_does_not_extend() { + [[profiles::suppress(test::uninit_read)]] int x; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + (void)y; +} +#endif From 510b55bca7557a51a8b971dc9836de2d29a08e84 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 4 May 2026 15:54:13 -0400 Subject: [PATCH 064/289] Doc improvements --- clang/docs/ProfilesFramework.rst | 308 +++++++++++++++++++++++++++---- clang/lib/Sema/Sema.cpp | 4 + clang/lib/Sema/SemaCast.cpp | 2 +- 3 files changed, 278 insertions(+), 36 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 6f29c5993db87..a8ef066998d34 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -27,14 +27,72 @@ violation. The framework is profile-agnostic. It handles attribute parsing, enforcement tracking, suppression scoping, module integration, and serialization. -Individual profiles only need to call a single API -(``Sema::checkProfileViolation``) at the appropriate semantic check sites. -Everything else -- suppression, template instantiation, SFINAE exclusion, -module propagation, and PCH/BMI serialization -- is handled by the framework -automatically. +Individual profiles only need to call a single API at the appropriate semantic +check sites (``Sema::checkProfileViolation`` for parse-time checks, or +``Sema::tryEmitCFGUninitAnalysisDiagnostic``-style helpers for post-parse +analyses). Everything else -- suppression, template instantiation, SFINAE +exclusion, module propagation, and PCH/BMI serialization -- is handled by the +framework automatically. -The entire framework is gated on the ``-fprofiles`` command-line flag -(``LangOpts.Profiles``). + +Driver Flag +----------- + +The entire framework is gated on the ``-fprofiles`` command-line flag, which +sets ``LangOpts.Profiles``. The flag is C++-only (declared with +``ShouldParseIf`` in ``clang/include/clang/Options/Options.td``) +and defaults to off. + +.. code-block:: bash + + clang++ -std=c++23 -fprofiles example.cpp + +Without ``-fprofiles``: + +- ``[[profiles::enforce]]``, ``[[profiles::suppress]]``, and + ``[[profiles::require]]`` are diagnosed as ``warn_attribute_ignored`` and + have no semantic effect. +- No profile rule check ever fires, even at sites that call + ``checkProfileViolation``. + +The framework's parse-time bookkeeping (``ProfileSuppressScope``, attribute +custom parsing, etc.) is also no-ops when ``LangOpts.Profiles`` is false, so +the flag is the single switch that turns the entire feature on or off. + + +Attribute Reference +=================== + +The three attributes are spelled in the ``profiles`` scope and accept either +the ``[[profiles::name(...)]]`` or ``[[using profiles: name(...)]]`` syntax. +Each attribute requires an argument clause; ``[[profiles::enforce]]`` and +``[[profiles::require]]`` with no parentheses are diagnosed. + +``[[profiles::enforce(profile-designator-list)]]`` + Allowed only on an *empty-declaration* at translation-unit scope or on a + *module-declaration*. At TU scope, it must precede every non-empty + declaration in the translation unit. Each profile-designator is a + ``::``-separated identifier sequence optionally followed by an + argument-clause (e.g. ``vendor(fortify: 3)``). Repeating the same name + with the same canonical designator is allowed; repeating it with a + different canonical designator is an error. + +``[[profiles::suppress(profile-name [, justification: "..."] [, rule: "..."])]]`` + Allowed on declarations and statements. Suppresses violations of the named + profile (optionally narrowed to a single rule) within the appertaining + declaration or statement; see :ref:`profiles-token-dominion` below. The + ``justification:`` argument, if present, must be a string literal; the + ``rule:`` argument may be a string literal or a bare token. Both + arguments are recorded but otherwise opaque to the framework. + +``[[profiles::require(profile-designator)]]`` + Allowed only on a *module-import-declaration*. Diagnoses if the imported + module's exported enforced-profile set does not contain a designator + matching the requested one. Importing a module does **not** retroactively + enforce its profiles in the importer. + +See the auto-generated :doc:`AttributeReference` for the AttrDocs entries +linked from these attributes. Implementing a New Profile @@ -45,50 +103,129 @@ defined entirely by: 1. A **profile name** (a ``::``-separated identifier sequence such as ``vendor::safety`` or ``std::type``). -2. One or more **rule names** (string identifiers such as +2. Zero or more **rule names** (string identifiers such as ``"reinterpret_cast"``). 3. **Diagnostics** emitted when a rule is violated. -4. **Check sites** in the compiler where ``checkProfileViolation`` is called. +4. **Check sites** in the compiler where the framework is consulted. + +There is no central registry of profiles. The framework treats profile names +as opaque strings; a profile is considered "enforced" simply because the user +wrote ``[[profiles::enforce(name)]]`` in their source. Each rule within a +profile is likewise just a string identifier; users can suppress individual +rules with ``[[profiles::suppress(profile_name, rule: "rule_name")]]``. + +There are two implementation patterns, depending on when the rule is checked. -There is no central registry of profiles. The framework treats profile names as -opaque strings; a profile is considered "enforced" simply because the user wrote -``[[profiles::enforce(name)]]`` in their source. Each rule within a profile is -likewise just a string identifier; users can suppress individual rules with -``[[profiles::suppress(profile_name, rule: "rule_name")]]``. Define Diagnostics ------------------ Add diagnostics to ``clang/include/clang/Basic/DiagnosticSemaKinds.td`` in the ``// C++ Profiles framework (P3589R2)`` group. Each diagnostic should accept -``%0`` for the profile name, since ``checkProfileViolation`` passes the profile -name as the first diagnostic argument. +``%0`` for the profile name, since the framework passes the profile name as +the first diagnostic argument. The group declares the helper class + +.. code-block:: text + + class ProfileRuleError : Error { + let SFINAE = SFINAE_Suppress; + } -For example, the ``test::type_cast`` profile defines: +so that profile-rule diagnostics participate in the SFINAE machinery as +suppressed errors -- they do not count as substitution failures and cannot +change overload resolution, but selected specializations replay them when +they are actually used. Define new rules using ``ProfileRuleError`` rather +than ``Error`` directly: .. code-block:: text - def err_profile_type_cast_reinterpret : Error< + def err_profile_type_cast_reinterpret : ProfileRuleError< "'reinterpret_cast' is unsafe under profile '%0'">; -Add ``checkProfileViolation`` Calls ------------------------------------- -At each semantic site where a rule can be violated, call -``Sema::checkProfileViolation``: +Pattern 1: Sema Check-Site Profile +---------------------------------- + +Used when the rule can be checked at a single, well-defined parse-time site +in Sema (typically inside a ``Sema::Build*`` or ``Sema::Act*`` routine). +``test::type_cast`` is the in-tree example. + +At each such site, call ``Sema::checkProfileViolation``: .. code-block:: c++ checkProfileViolation("my::profile", "my_rule", Loc, diag::err_my_profile_rule); -This function checks whether the profile is enforced and not suppressed. During -template argument deduction, profile rule diagnostics are suppressed by Clang's -normal SFINAE machinery so they cannot affect overload resolution or template -instantiation; selected specializations replay suppressed diagnostics when used. -Unevaluated and discarded-statement contexts are skipped. The profile name is -passed as ``%0``. +This function checks whether the profile is enforced and not suppressed. +During template argument deduction, profile rule diagnostics are suppressed +by Clang's normal SFINAE machinery so they cannot affect overload resolution +or template instantiation; selected specializations replay suppressed +diagnostics when used. Unevaluated and discarded-statement contexts are +skipped. The profile name is passed as ``%0``. + +Suppression for parse-time check sites is consulted via the +``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` +RAII guards (see :ref:`profiles-internals` below). Profile implementers do +not need to interact with the stack directly. + + +Pattern 2: Post-Parse / CFG-Based Profile +----------------------------------------- + +Used when the rule cannot be checked at a single Sema entry point because it +depends on whole-function analysis -- typically a CFG-based analysis run +after a function body is complete. ``test::uninit_read`` is the in-tree +example: it diagnoses reads of uninitialized variables on top of Clang's +existing CFG-based uninitialized-variables analysis. +This pattern needs three pieces: + +1. **Opt the function into the analysis pass.** + Analysis-based passes in ``AnalysisBasedWarnings.cpp::IssueWarnings`` are + normally run only when their corresponding warning flag is enabled. + To run the pass for an enforced profile even when the underlying warning + is silenced, add a check in + ``Sema::anyProfileRequestsCFGUninitAnalysis()`` (or its equivalent for + another analysis) that returns true when the profile is enforced. The + pass guard then becomes + ``S.anyProfileRequestsCFGUninitAnalysis() || !Diags.isIgnored(...)``. + +2. **Consult the framework in the analysis's diagnostic reporter.** + Add a Sema helper such as ``tryEmitCFGUninitAnalysisDiagnostic`` that: + + - Uses ``Sema::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)`` + to decide whether to emit, walking parent statements and lexical + declaration contexts to honor ``[[profiles::suppress]]`` placed on + enclosing AST nodes (this is the post-parse counterpart to + ``ProfileSuppressStack``). + - Emits the diagnostic and any companion notes when it returns true. + - Returns whether a diagnostic was emitted. + + The analysis's reporter (``UninitValsDiagReporter`` in the in-tree + example) calls this helper before its own warning path: + + .. code-block:: c++ + + for (const auto &U : *vec) { + if (S.tryEmitCFGUninitAnalysisDiagnostic(vd, U.getUser(), AC)) + return; + } + +3. **Define the diagnostic** with ``ProfileRuleError`` as in pattern 1. + +The post-parse Stmt-walking suppression check is intentionally separate from +the parse-time stack check: by the time CFG analysis runs, the parse stack +has been unwound, so the framework instead walks the AST. The Stmt-walking +overload of ``isProfileSuppressed`` examines: + +- ``AttributedStmt`` ancestors of the use site. +- ``DeclStmt`` ancestors (whose declared ``VarDecl``\ s carry the attribute, + not the enclosing statement). +- The enclosing ``Decl`` chain via ``getLexicalDeclContext()``. + + +.. _profiles-token-dominion: Suppression Dominion is Token-Based =================================== @@ -105,6 +242,12 @@ per-object "opt-out of this check everywhere this value is used" semantics initialization profile) must introduce their own, separate, decl-scoped marker. +This applies identically to the parse-time suppression stack and the +post-parse Stmt-tree walker described in pattern 2. + + +.. _profiles-internals: + Framework Internals Reference ============================= @@ -121,6 +264,17 @@ during the appropriate region. ``checkProfileViolation`` consults ``ProfileSuppressStack`` directly, so profile implementers never need to create ``ProfileSuppressScope`` objects. +Stmt-Tree Suppression Walker +---------------------------- + +The ``isProfileSuppressed(name, rule, Stmt*, AnalysisDeclContext&)`` overload +is the post-parse counterpart to ``ProfileSuppressStack``. It is used by +analyses that run after parsing (when the parse-time stack no longer +reflects the enclosing region) and walks the AST upward from a use site to +find any matching ``[[profiles::suppress]]`` attribute on an enclosing +``AttributedStmt``, ``DeclStmt``-declared ``VarDecl``, or lexical +``Decl`` parent. + Template Instantiation ---------------------- @@ -174,19 +328,103 @@ The following parts of P3589R2 are deliberately not implemented: on redeclarations are parsed and recorded, but no cross-redeclaration compatibility check is performed. + +Built-in Test Profiles +====================== + +The tree ships two minimal, test-only profiles -- one per implementation +pattern -- so the framework's behavior can be exercised without depending on +any user-facing profile. Both are gated on ``-fprofiles``. + +By convention: + +- Real test profiles live under the ``test::`` namespace. Today there are + two: ``test::type_cast`` and ``test::uninit_read``. +- The names ``test::other``, ``test::bounds``, ``test::new_profile``, and + ``test::not_enforced`` are deliberately *not* implemented and appear only + in negative tests as stand-in "some other profile" names. Adding a real + profile under any of these names would invalidate those tests. + + The ``test::type_cast`` Profile -=============================== +------------------------------- + +A pattern-1 (Sema check-site) profile. Demonstrates the simple case where +a rule can be checked from a single Sema entry point. -The ``test::type_cast`` profile is a minimal, test-only profile included in the -tree. It demonstrates everything needed to implement a profile. +- **Rules**: ``reinterpret_cast``. +- **Diagnostic**: ``err_profile_type_cast_reinterpret`` + ("'reinterpret_cast' is unsafe under profile '%0'"). +- **Check site**: ``Sema::BuildCXXNamedCast`` in ``clang/lib/Sema/SemaCast.cpp``, + inside the ``reinterpret_cast`` arm. -The profile has a single rule, ``reinterpret_cast``, which diagnoses uses of -``reinterpret_cast``. In ``clang/lib/Sema/SemaCast.cpp``, inside the -``reinterpret_cast`` handling of ``Sema::BuildCXXNamedCast``: +The entire profile implementation is the single call: .. code-block:: c++ checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, diag::err_profile_type_cast_reinterpret); -That single call is the entire profile implementation. + +The ``test::uninit_read`` Profile +--------------------------------- + +A pattern-2 (post-parse / CFG-based) profile. Demonstrates the case where +the rule depends on whole-function analysis and must run after parsing. +It diagnoses reads of uninitialized variables by reusing Clang's existing +CFG-based uninitialized-variables analysis. + +- **Rules**: none (the profile has a single implicit rule, so the rule + string is empty). +- **Diagnostic**: ``err_profile_uninit_read`` + ("variable %1 is read before initialization under profile '%0'"). + A companion ``note_var_declared_here`` is emitted at the variable's + declaration. +- **Opt-in to CFG analysis**: ``Sema::anyProfileRequestsCFGUninitAnalysis`` + in ``clang/lib/Sema/Sema.cpp``, consulted by ``IssueWarnings`` in + ``clang/lib/Sema/AnalysisBasedWarnings.cpp`` so that the analysis pass + runs even when ``-Wuninitialized`` is silenced. +- **Check site**: ``Sema::tryEmitCFGUninitAnalysisDiagnostic``, called from + ``UninitValsDiagReporter::diagnoseUnitializedVar`` *before* the default + warning path. When it emits, the default warning is skipped entirely. + +The Stmt-tree suppression walker is what makes ``[[profiles::suppress]]`` +work for this profile: by the time the CFG analysis runs, the parse-time +``ProfileSuppressStack`` has been unwound, so the helper consults the AST +directly via ``shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``. + + +In-Tree Tests +============= + +These tests collectively exercise the framework and the two built-in +profiles. When changing the framework, run them all with +``check-clang-sema``, ``check-clang-parser``, and ``check-clang-pch``. + +- ``clang/test/Parser/cxx-profiles-framework.cpp`` -- attribute parser: + valid ``enforce``/``suppress``/``require`` forms, the ``[[using profiles: + ...]]`` syntax, profile-name and profile-argument grammar, and the + parse-error / missing-argument-clause paths. +- ``clang/test/SemaCXX/safety-profile-framework.cpp`` -- attribute placement + and basic semantic checks (``enforce`` only on empty-declarations at TU + scope, ``require`` only on imports, ``suppress`` on declarations and + statements, ``justification:`` must be a string literal, etc.). +- ``clang/test/SemaCXX/safety-profile-framework-modules.cppm`` -- module + integration: ``enforce`` on a module-declaration is exported via the BMI, + ``require`` validates against an imported module's exported set, GMF-only + ``enforce`` does not leak through the BMI, interface-to-implementation + propagation, partition interfaces, and the without-``-fprofiles`` ignored + paths. +- ``clang/test/SemaCXX/safety-profile-type-cast.cpp`` -- the + ``test::type_cast`` profile: enforcement, suppression on every supported + declaration and statement form, template instantiation, SFINAE + exclusion, lambdas (including generic lambdas with suppression carried + through instantiation), and out-of-line members of suppressed classes + and namespaces. +- ``clang/test/SemaCXX/safety-profile-uninit-read.cpp`` -- the + ``test::uninit_read`` profile. Cases are gated on ``-DCASE=N`` so the + analysis-based-warnings early-exit-on-first-error does not hide later + cases; case 0 is the no-violation baseline used by both the + ``-fprofiles`` and the without-``-fprofiles`` runs. +- ``clang/test/PCH/cxx-profiles-enforce.cpp`` -- ``[[profiles::enforce]]`` + state survives PCH serialization round-trip. diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 4a04b62e535d8..3eaa92b93f259 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3180,6 +3180,10 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, return true; } +// test::uninit_read is a built-in test profile that rides on Clang's existing +// CFG-based uninitialized-variables analysis. See ProfilesFramework.rst for +// the post-parse / CFG-based profile pattern these helpers implement. + bool Sema::anyProfileRequestsCFGUninitAnalysis() const { return isProfileEnforced("test::uninit_read"); } diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index aae3ad0fec9f2..795f0425818df 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -400,7 +400,7 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, if (Op.SrcExpr.isInvalid()) return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); - // "test::type_cast" is a testing-only attribute + // test::type_cast is a built-in test profile; see ProfilesFramework.rst. checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, diag::err_profile_type_cast_reinterpret); } From cbe7087f82e317f513819db8042887e762c971ee Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 11 May 2026 13:52:42 -0400 Subject: [PATCH 065/289] Add table to register CFG analysis based profiles --- clang/docs/ProfilesFramework.rst | 95 +++++++++++++++--------- clang/include/clang/Sema/Sema.h | 4 - clang/lib/Sema/AnalysisBasedWarnings.cpp | 31 +++++++- clang/lib/Sema/Sema.cpp | 20 ----- 4 files changed, 88 insertions(+), 62 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index a8ef066998d34..18026f7261217 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -29,10 +29,10 @@ The framework is profile-agnostic. It handles attribute parsing, enforcement tracking, suppression scoping, module integration, and serialization. Individual profiles only need to call a single API at the appropriate semantic check sites (``Sema::checkProfileViolation`` for parse-time checks, or -``Sema::tryEmitCFGUninitAnalysisDiagnostic``-style helpers for post-parse -analyses). Everything else -- suppression, template instantiation, SFINAE -exclusion, module propagation, and PCH/BMI serialization -- is handled by the -framework automatically. +``Sema::shouldEmitProfileViolation`` from a per-pass dispatch table for +post-parse analyses). Everything else -- suppression, template instantiation, +SFINAE exclusion, module propagation, and PCH/BMI serialization -- is handled +by the framework automatically. Driver Flag @@ -179,40 +179,62 @@ after a function body is complete. ``test::uninit_read`` is the in-tree example: it diagnoses reads of uninitialized variables on top of Clang's existing CFG-based uninitialized-variables analysis. -This pattern needs three pieces: +This pattern needs three pieces, all colocated with the analysis pass (the +framework intentionally does not learn the profile name). -1. **Opt the function into the analysis pass.** +1. **Add the profile to the analysis pass's per-pass opt-in table.** + Each post-parse analysis owns a small table of the profiles that ride it, + one row per profile (profile name, rule name, diagnostic id). The + in-tree example is ``CFGUninitProfiles`` in + ``clang/lib/Sema/AnalysisBasedWarnings.cpp``: + + .. code-block:: c++ + + struct CFGUninitProfileEntry { + StringRef Name; + StringRef Rule; + unsigned DiagID; + }; + constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"my::profile", /*Rule=*/"", diag::err_my_profile_rule}, + }; + +2. **Gate the analysis pass on the table.** Analysis-based passes in ``AnalysisBasedWarnings.cpp::IssueWarnings`` are - normally run only when their corresponding warning flag is enabled. - To run the pass for an enforced profile even when the underlying warning - is silenced, add a check in - ``Sema::anyProfileRequestsCFGUninitAnalysis()`` (or its equivalent for - another analysis) that returns true when the profile is enforced. The - pass guard then becomes - ``S.anyProfileRequestsCFGUninitAnalysis() || !Diags.isIgnored(...)``. - -2. **Consult the framework in the analysis's diagnostic reporter.** - Add a Sema helper such as ``tryEmitCFGUninitAnalysisDiagnostic`` that: - - - Uses ``Sema::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)`` - to decide whether to emit, walking parent statements and lexical - declaration contexts to honor ``[[profiles::suppress]]`` placed on - enclosing AST nodes (this is the post-parse counterpart to - ``ProfileSuppressStack``). - - Emits the diagnostic and any companion notes when it returns true. - - Returns whether a diagnostic was emitted. - - The analysis's reporter (``UninitValsDiagReporter`` in the in-tree - example) calls this helper before its own warning path: + normally run only when their corresponding warning flag is enabled. To + run the pass for an enforced profile even when the underlying warning is + silenced, OR a ``llvm::any_of(Table, [&](const auto &E) { return + S.isProfileEnforced(E.Name); })`` check into the existing pass guard. + The in-tree example wraps this as ``anyCFGUninitProfileEnforced(S)`` and + the pass guard becomes + ``anyCFGUninitProfileEnforced(S) || !Diags.isIgnored(...)``. + +3. **Walk the table in the analysis's diagnostic reporter.** + For each use site the analysis would have warned about, iterate the + table and call + ``Sema::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``, + which walks parent statements and lexical declaration contexts to honor + ``[[profiles::suppress]]`` on enclosing AST nodes (the post-parse + counterpart to ``ProfileSuppressStack``). Emit the entry's diagnostic + when it returns true, and skip the default warning path. In the + in-tree example (``UninitValsDiagReporter`` in + ``AnalysisBasedWarnings.cpp``): .. code-block:: c++ for (const auto &U : *vec) { - if (S.tryEmitCFGUninitAnalysisDiagnostic(vd, U.getUser(), AC)) + for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) + continue; + S.Diag(U.getUser()->getBeginLoc(), E.DiagID) + << E.Name << vd->getDeclName(); + S.Diag(vd->getLocation(), diag::note_var_declared_here) + << vd->getDeclName(); return; + } } -3. **Define the diagnostic** with ``ProfileRuleError`` as in pattern 1. +The diagnostic itself is defined with ``ProfileRuleError`` as in pattern 1. The post-parse Stmt-walking suppression check is intentionally separate from the parse-time stack check: by the time CFG analysis runs, the parse stack @@ -380,13 +402,14 @@ CFG-based uninitialized-variables analysis. ("variable %1 is read before initialization under profile '%0'"). A companion ``note_var_declared_here`` is emitted at the variable's declaration. -- **Opt-in to CFG analysis**: ``Sema::anyProfileRequestsCFGUninitAnalysis`` - in ``clang/lib/Sema/Sema.cpp``, consulted by ``IssueWarnings`` in - ``clang/lib/Sema/AnalysisBasedWarnings.cpp`` so that the analysis pass - runs even when ``-Wuninitialized`` is silenced. -- **Check site**: ``Sema::tryEmitCFGUninitAnalysisDiagnostic``, called from - ``UninitValsDiagReporter::diagnoseUnitializedVar`` *before* the default - warning path. When it emits, the default warning is skipped entirely. +- **Opt-in table**: ``CFGUninitProfiles`` in + ``clang/lib/Sema/AnalysisBasedWarnings.cpp``. The ``IssueWarnings`` pass + guard consults it via ``anyCFGUninitProfileEnforced(S)`` so the analysis + runs even when ``-Wuninitialized`` is silenced, and + ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the + default warning path -- when an entry's + ``Sema::shouldEmitProfileViolation`` returns true the entry's diagnostic + fires and the default warning is skipped entirely. The Stmt-tree suppression walker is what makes ``[[profiles::suppress]]`` work for this profile: by the time the CFG analysis runs, the parse-time diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index d87d8a87d9d44..10a022540ae81 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1081,10 +1081,6 @@ class Sema final : public SemaBase { bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); - bool anyProfileRequestsCFGUninitAnalysis() const; - bool tryEmitCFGUninitAnalysisDiagnostic(const VarDecl *VD, const Expr *UseExpr, - AnalysisDeclContext &AC); - class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 6fed7d1b399e2..29aaff711a40f 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1686,6 +1686,26 @@ struct SortDiagBySourceLocation { } // namespace clang namespace { +// Profiles that opt into Clang's CFG-uninitialized-variables analysis. Each +// entry pairs the profile name with the diagnostic to emit when an +// uninitialized read is found and not suppressed at the use site. Adding a +// new profile that wants to ride this analysis is a single row here plus a +// ProfileRuleError diagnostic in DiagnosticSemaKinds.td. +struct CFGUninitProfileEntry { + StringRef Name; + StringRef Rule; + unsigned DiagID; +}; +constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read}, +}; + +bool anyCFGUninitProfileEnforced(const Sema &S) { + return llvm::any_of(CFGUninitProfiles, [&](const CFGUninitProfileEntry &E) { + return S.isProfileEnforced(E.Name); + }); +} + class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; AnalysisDeclContext &AC; @@ -1750,8 +1770,15 @@ class UninitValsDiagReporter : public UninitVariablesHandler { // use site and is not suppressed there, emit the profile diagnostic // and skip the default warning path entirely. for (const auto &U : *vec) { - if (S.tryEmitCFGUninitAnalysisDiagnostic(vd, U.getUser(), AC)) + for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) + continue; + S.Diag(U.getUser()->getBeginLoc(), E.DiagID) + << E.Name << vd->getDeclName(); + S.Diag(vd->getLocation(), diag::note_var_declared_here) + << vd->getDeclName(); return; + } } // Specially handle the case where we have uses of an uninitialized @@ -3311,7 +3338,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( Analyzer.run(AC); } - if (S.anyProfileRequestsCFGUninitAnalysis() || + if (anyCFGUninitProfileEnforced(S) || !Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_sometimes_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_maybe_uninit_var, D->getBeginLoc()) || diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 3eaa92b93f259..2815850c92597 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3180,26 +3180,6 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, return true; } -// test::uninit_read is a built-in test profile that rides on Clang's existing -// CFG-based uninitialized-variables analysis. See ProfilesFramework.rst for -// the post-parse / CFG-based profile pattern these helpers implement. - -bool Sema::anyProfileRequestsCFGUninitAnalysis() const { - return isProfileEnforced("test::uninit_read"); -} - -bool Sema::tryEmitCFGUninitAnalysisDiagnostic(const VarDecl *VD, - const Expr *UseExpr, - AnalysisDeclContext &AC) { - static constexpr StringRef Profile = "test::uninit_read"; - if (!shouldEmitProfileViolation(Profile, /*RuleName=*/"", UseExpr, AC)) - return false; - Diag(UseExpr->getBeginLoc(), diag::err_profile_uninit_read) - << Profile << VD->getDeclName(); - Diag(VD->getLocation(), diag::note_var_declared_here) << VD->getDeclName(); - return true; -} - Sema::ProfileSuppressScope::ProfileSuppressScope( Sema &S, const ParsedAttributesView &Attrs) : S(S) { From 3aba38066069164d1a829234dc2a14155905c623 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 11 May 2026 14:11:22 -0400 Subject: [PATCH 066/289] Add CXX11Uninitialized attribute ([[uninitialized]]) --- clang/include/clang/Basic/Attr.td | 7 ++++++ clang/include/clang/Basic/AttrDocs.td | 33 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 901f4b7f5f9ad..f0566f0391ffb 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1721,6 +1721,13 @@ def CXX11NoReturn : InheritableAttr { let Documentation = [CXX11NoReturnDocs]; } +def CXX11Uninitialized : InheritableAttr { + let Spellings = [CXX11<"", "uninitialized", 202602>]; + let Subjects = SubjectList<[Var], ErrorDiag>; + let Documentation = [CXX11UninitializedDocs]; + let SimpleHandler = 1; +} + def NonBlocking : TypeAttr { let Spellings = [Clang<"nonblocking">]; let Args = [ExprArgument<"Cond", /*optional*/1>]; diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index b2d2a8b5cf90e..44743d281ca52 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -875,6 +875,39 @@ migration for code using ``[[noreturn]]`` after including ````. }]; } +def CXX11UninitializedDocs : Documentation { + let Category = DocCatVariable; + let Heading = "uninitialized"; + let Content = [{ +The ``[[uninitialized]]`` attribute marks a variable definition as +intentionally left uninitialized. It is purely informational: it has no +effect on code generation and the variable still has the same indeterminate +value it would have without the attribute. + +The attribute exists to support the initialization profile (see +:doc:`ProfilesFramework`). Under that profile, a definition such as +``int x;`` is diagnosed because it leaves ``x`` with an indeterminate +value; writing ``int x [[uninitialized]];`` instead suppresses that +declaration-site diagnostic and documents that the lack of an initializer +is intentional. The attribute does **not** suppress diagnostics that fire +when the variable is later read before being assigned; the read-before-init +rule still applies. + +A declaration that has both ``[[uninitialized]]`` and an initializer is +ill-formed under the initialization profile (the marker contradicts the +explicit initialization). + +Outside the initialization profile the attribute is silently accepted and +has no effect, so code that uses it remains valid when compiled without +``-fprofiles``. + +This attribute is distinct from the Clang vendor attribute +``[[clang::uninitialized]]``, which interacts with +``-ftrivial-auto-var-init`` to force a local variable to remain +uninitialized. + }]; +} + def NoMergeDocs : Documentation { let Category = DocCatStmt; let Content = [{ From 3b0917520e39414c0032a0171d5c8d8da20bb0f5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 11 May 2026 15:31:32 -0400 Subject: [PATCH 067/289] Initial initialization profile implementation --- clang/docs/ProfilesFramework.rst | 140 ++++++++++++++++- .../clang/Basic/DiagnosticSemaKinds.td | 11 ++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 1 + clang/lib/Sema/SemaDecl.cpp | 41 +++++ .../test/SemaCXX/safety-profile-init-decl.cpp | 93 ++++++++++++ .../test/SemaCXX/safety-profile-init-read.cpp | 141 ++++++++++++++++++ .../SemaCXX/safety-profile-init-static.cpp | 48 ++++++ .../safety-profile-init-with-initializer.cpp | 60 ++++++++ 8 files changed, 530 insertions(+), 5 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-init-decl.cpp create mode 100644 clang/test/SemaCXX/safety-profile-init-read.cpp create mode 100644 clang/test/SemaCXX/safety-profile-init-static.cpp create mode 100644 clang/test/SemaCXX/safety-profile-init-with-initializer.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 18026f7261217..239cfab2ca9a5 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -351,12 +351,18 @@ The following parts of P3589R2 are deliberately not implemented: compatibility check is performed. -Built-in Test Profiles -====================== +Built-in Profiles +================= -The tree ships two minimal, test-only profiles -- one per implementation -pattern -- so the framework's behavior can be exercised without depending on -any user-facing profile. Both are gated on ``-fprofiles``. +The tree ships three built-in profiles, all gated on ``-fprofiles``: + +- ``test::type_cast`` (test-only) -- pattern-1 example. +- ``test::uninit_read`` (test-only) -- pattern-2 example riding the existing + CFG uninitialized-variables analysis. +- ``std::init`` (initial slice of the proposed initialization profile from + Stroustrup's draft, on top of P3589R2 and P3402R3). It rides the same + CFG dispatch as ``test::uninit_read`` for one of its rules and adds three + parse-time (pattern-1) rules. By convention: @@ -417,6 +423,111 @@ work for this profile: by the time the CFG analysis runs, the parse-time directly via ``shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``. +The ``std::init`` Profile (initial slice) +----------------------------------------- + +A first slice of the proposed initialization profile, intentionally minimal: +no ``[[ref_to_uninit]]``, no field-level marker, no class-finalization hook, +no new dataflow analyses. See the audit and plan documents in +``.cursor/plans/`` for the deferred items. + +The slice introduces one new attribute and four rules. + +Marker attribute +~~~~~~~~~~~~~~~~ + +``[[uninitialized]]`` (a standard C++11 attribute, distinct from the Clang +vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` as +intentionally left uninitialized. Recognised by Clang regardless of +``-fprofiles``; only carries semantic weight when ``std::init`` is enforced. + +- TableGen def: ``CXX11Uninitialized`` in ``clang/include/clang/Basic/Attr.td``. +- Subjects: ``Var`` only. Field-level support is the framework gap §2.A.2 + and is deferred. +- Behaviour: + + - Suppresses R2 (``uninit_decl``) on the marked declaration. + - Does **not** suppress R1 (``uninit_read``). Per the paper, the marker + excuses the declaration but a read before any subsequent assignment is + still ill-formed. + - Triggers R4 (``uninit_with_initializer``) when combined with an + initializer, including a language-synthesized one (e.g., a class-typed + variable whose default constructor would run). + +Rules +~~~~~ + +R1. ``uninit_read`` -- pattern 2 (CFG) +...................................... + +Reads of an uninitialized variable. Implemented as a second row in the +existing ``CFGUninitProfiles`` table beside ``test::uninit_read``: + +.. code-block:: c++ + + constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read}, + {"std::init", "uninit_read", diag::err_init_uninit_read}, + }; + +If both ``test::uninit_read`` and ``std::init`` are enforced in the same TU, +the table-order priority makes ``test::uninit_read`` fire first. Use +``[[profiles::suppress(test::uninit_read)]]`` to demote it at a use site +and surface the ``std::init`` diagnostic. + +R2. ``uninit_decl`` -- pattern 1 +................................. + +A definition that, after attempted default-initialization, has no +initializer expression must either carry ``[[uninitialized]]`` or have +static / thread storage duration (zero-initialized by language rule). + +- Diagnostic: ``err_init_uninit_decl``. +- Check site: end of ``Sema::ActOnUninitializedDecl`` in + ``clang/lib/Sema/SemaDecl.cpp``. +- Conservative classification: a class type whose default constructor + produces an initializer expression (``Var->getInit()`` non-null after + ``InitSeq.Perform``) is trusted. Aggregates / POD class types whose + default-init leaves members indeterminate are *not* diagnosed in this + slice; that is paper §6 ("classes without constructors") work, deferred. + +R3. ``static_runtime_init`` -- pattern 1 +......................................... + +A non-local variable whose initializer is not a constant expression must +be ``constinit`` (which is already a hard error from the existing +``ConstInitAttr`` arm). Without ``constinit``, the existing +``-Wglobal-constructors`` warning would fire (off by default); under +``std::init`` this rule promotes that to a profile error. + +- Diagnostic: ``err_init_static_runtime_init``. +- Check site: in ``Sema::CheckCompleteVariableDeclaration``, in the + constinit cascade, immediately before the ``-Wglobal-constructors`` arm + (so the profile error takes precedence when both would fire). + +R4. ``uninit_with_initializer`` -- pattern 1 +............................................ + +``[[uninitialized]]`` and an initializer on the same declaration is a +contradiction (the marker means "no initialization here"). + +- Diagnostic: ``err_init_uninit_with_initializer``. +- Check site: top of ``Sema::CheckCompleteVariableDeclaration``. +- Note: also fires when the initializer is language-synthesized (e.g., + ``WithCtor x [[uninitialized]];`` -- the implicit default-constructor + call counts as an initializer). Combining the marker with class types + that have a default constructor is therefore ill-formed. + +Diagnostic suppression +~~~~~~~~~~~~~~~~~~~~~~ + +All four rules are suppressible per-site with +``[[profiles::suppress(std::init)]]`` (covers all rules) or +``[[profiles::suppress(std::init, rule: "rule_name")]]`` (rule-targeted). +The token-based-dominion limitation noted earlier applies: a suppress +attribute on a ``VarDecl`` covers only that declaration's tokens. + + In-Tree Tests ============= @@ -449,5 +560,24 @@ profiles. When changing the framework, run them all with analysis-based-warnings early-exit-on-first-error does not hide later cases; case 0 is the no-violation baseline used by both the ``-fprofiles`` and the without-``-fprofiles`` runs. +- ``clang/test/SemaCXX/safety-profile-init-read.cpp`` -- the ``std::init`` + profile's ``uninit_read`` rule. Same ``-DCASE=N`` style as the + ``test::uninit_read`` test; CASE=4 additionally enforces + ``test::uninit_read`` to exercise the table-order priority. +- ``clang/test/SemaCXX/safety-profile-init-decl.cpp`` -- the ``std::init`` + profile's ``uninit_decl`` rule (R2): scalars / pointers / enums require + an initializer or ``[[uninitialized]]``; statics / thread-locals are + excluded; class types with a user-provided default constructor are + trusted; trivial / aggregate types are conservatively *not* diagnosed + (deferred to §6 work). +- ``clang/test/SemaCXX/safety-profile-init-static.cpp`` -- the ``std::init`` + profile's ``static_runtime_init`` rule (R3): non-local vars need a + constant initializer; locals / static-locals / thread-locals are + excluded; ``constinit`` failures still produce the existing hard error + regardless of ``-fprofiles``. +- ``clang/test/SemaCXX/safety-profile-init-with-initializer.cpp`` -- the + ``std::init`` profile's ``uninit_with_initializer`` rule (R4): every + combination of ``[[uninitialized]]`` placement (prefix / postfix) with + every initializer form (``= e``, ``{}``, ``(e)``). - ``clang/test/PCH/cxx-profiles-enforce.cpp`` -- ``[[profiles::enforce]]`` state survives PCH serialization round-trip. diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index b6a77d406fafb..4b78a60255621 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14138,4 +14138,15 @@ def err_profile_type_cast_reinterpret : ProfileRuleError< "'reinterpret_cast' is unsafe under profile '%0'">; def err_profile_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; +def err_init_uninit_read : ProfileRuleError< + "variable %1 is read before initialization under profile '%0'">; +def err_init_uninit_decl : ProfileRuleError< + "variable %1 must be initialized or marked '[[uninitialized]]' under " + "profile '%0'">; +def err_init_static_runtime_init : ProfileRuleError< + "non-local variable %1 requires constant initialization under " + "profile '%0'">; +def err_init_uninit_with_initializer : ProfileRuleError< + "variable %1 cannot be both '[[uninitialized]]' and have an initializer " + "under profile '%0'">; } // end of sema component. diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 29aaff711a40f..8112d3ddf80a6 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1698,6 +1698,7 @@ struct CFGUninitProfileEntry { }; constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read}, + {"std::init", "uninit_read", diag::err_init_uninit_read}, }; bool anyCFGUninitProfileEnforced(const Sema &S) { diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 1de6f8639cefb..fe2c50acced59 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14715,6 +14715,20 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { Var->setInit(RecoveryExpr.get()); } + // std::init / uninit_decl: a definition without any initializer (after + // attempted default-initialization) must either carry [[uninitialized]] or + // be initialized by a language rule. Static / thread storage duration is + // excluded -- those are zero-initialized; runtime-init concerns are R3's. + if (!Var->isInvalidDecl() && !Var->getInit() && + Var->getStorageDuration() == SD_Automatic && + !Var->hasAttr()) { + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_decl"; + if (shouldEmitProfileViolation(Profile, Rule, Var->getLocation())) + Diag(Var->getLocation(), diag::err_init_uninit_decl) + << Profile << Var->getDeclName(); + } + CheckCompleteVariableDeclaration(Var); } } @@ -14821,6 +14835,17 @@ void Sema::addLifetimeBoundToImplicitThis(CXXMethodDecl *MD) { void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; + // std::init / uninit_with_initializer: [[uninitialized]] documents that + // the variable is intentionally left uninitialized, so it contradicts an + // explicit initializer. + if (var->hasInit() && var->hasAttr()) { + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_with_initializer"; + if (shouldEmitProfileViolation(Profile, Rule, var->getLocation())) + Diag(var->getLocation(), diag::err_init_uninit_with_initializer) + << Profile << var->getDeclName(); + } + CUDA().MaybeAddConstantAttr(var); if (getLangOpts().OpenCL) { @@ -15057,6 +15082,22 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { for (auto &it : Notes) Diag(it.first, it.second); var->setInvalidDecl(); + } else if (IsGlobal && [&] { + // std::init / static_runtime_init: paper says non-local + // statics must be initialized at compile or link time. + // Runs before -Wglobal-constructors so the profile error + // (when enforced) takes precedence over the standalone + // warning. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "static_runtime_init"; + if (!shouldEmitProfileViolation(Profile, Rule, + var->getLocation())) + return false; + Diag(var->getLocation(), diag::err_init_static_runtime_init) + << Profile << var->getDeclName(); + return true; + }()) { + // Diagnostic emitted in the lambda above. } else if (IsGlobal && !getDiagnostics().isIgnored(diag::warn_global_constructor, var->getLocation())) { diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp new file mode 100644 index 0000000000000..24fd46b62bd8e --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -0,0 +1,93 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +int sink(int); + +struct Trivial { int m; }; +struct WithCtor { WithCtor(); int m; }; + +void test_scalars() { + int a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + int b = 0; + int c [[uninitialized]]; + int d{}; + int e = sink(1); + (void)b; (void)c; (void)d; (void)e; +} + +void test_pointer() { + int* p; // expected-error {{variable 'p' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + int* q = nullptr; + int* r [[uninitialized]]; + (void)q; (void)r; +} + +enum E { E0, E1 }; +void test_enum() { + E x; // expected-error {{variable 'x' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + E y = E0; + E z [[uninitialized]]; + (void)y; (void)z; +} + +void test_class_with_user_ctor() { + // OK: user-provided default constructor is trusted. The marker is + // unnecessary on class types whose default-init runs a constructor; in + // fact combining them is a contradiction caught by R4 (the constructor + // call is the synthesized initializer). + WithCtor w; + (void)w; +} + +void test_class_trivial() { + // Conservative: trivial / aggregate class types are not diagnosed by R2 in + // this minimal slice. Field-level initialization tracking is the §6 + // "classes without constructors" work and is explicitly deferred. + Trivial t; + (void)t; +} + +void test_static_local() { + static int s; + thread_local int t; + (void)s; (void)t; +} + +int g_namespace_scalar; +thread_local int g_thread; +extern int g_extern; + +void test_param(int p) { + (void)p; +} + +void test_marker_then_assign() { + int x [[uninitialized]]; + x = 7; + (void)x; +} + +void test_suppress_decl() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_decl")]] int x; + (void)x; +} + +void test_suppress_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int a; + int b; + (void)a; (void)b; + } +} + +template +void template_uninit() { + T x; // expected-error {{variable 'x' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + (void)x; +} +template void template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp new file mode 100644 index 0000000000000..8011a91a1c9d4 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -0,0 +1,141 @@ +// Each CASE selects one violation test so the analysis-based-warnings early +// exit on first error doesn't hide later cases. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=0 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=1 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=2 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=3 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=4 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=5 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=6 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized -DCASE=0 %s + +#if CASE == 0 +// expected-no-diagnostics +#endif + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; +#if CASE == 4 +[[profiles::enforce(test::uninit_read)]]; +#endif + +// Cases that never diagnose are always compiled. + +// CASE=4 also enforces test::uninit_read; the always-compiled suppress tests +// suppress both so the function-under-test demonstrates std::init behavior in +// isolation. +// no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] +void test_suppress_decl() { + int x [[uninitialized]]; + int y = x; + (void)y; +} + +void test_suppress_stmt_inner() { + int x [[uninitialized]]; + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } +} + +void test_suppress_var_init() { + int x [[uninitialized]]; + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] + int y = x; + (void)y; +} + +void test_suppress_rule_targeted() { + int x [[uninitialized]]; + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] + [[profiles::suppress(test::uninit_read)]] { + int y = x; + (void)y; + } +} + +void test_marker_then_write_then_read() { + int x [[uninitialized]]; + x = 7; + int y = x; + (void)y; +} + +void test_param(int p) { + int y = p; + (void)y; +} + +#if CASE == 1 +void test_marker_does_not_excuse_read() { + int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; +} +#endif + +#if CASE == 2 +void test_suppress_stmt_outer() { + int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + [[profiles::suppress(std::init)]] { + int y = x; + (void)y; + } + int z = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)z; +} +#endif + +#if CASE == 3 +template +T template_uninit() { + T x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + return x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} +} +void instantiate_template_uninit() { + template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} +} +#endif + +#if CASE == 4 +// When both test::uninit_read and std::init are enforced (the conditional +// enforce above adds test::uninit_read for this case), table order in +// CFGUninitProfiles makes test::uninit_read fire first. Suppressing it at +// the use site lets the std::init diagnostic surface. +void test_demote_test_profile() { + int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; + } +} +#endif + +#if CASE == 5 +void test_selective_suppress() { + int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + [[profiles::suppress(test::other)]] { + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; + } +} +#endif + +#if CASE == 6 +void test_decl_suppress_does_not_extend() { + [[profiles::suppress(std::init)]] int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; +} +#endif diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp new file mode 100644 index 0000000000000..53897a7ba25a6 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -0,0 +1,48 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +int runtime(); // expected-note {{declared here}} + // no-profiles-note@-1 {{declared here}} +constexpr int compile_time() { return 7; } + +int g_const = 0; +int g_constexpr = compile_time(); +int g_array_const[3] = {1, 2, 3}; +int g_runtime = runtime(); // expected-error {{non-local variable 'g_runtime' requires constant initialization under profile 'std::init'}} +int g_runtime_array[3] = {1, runtime(), 3}; // expected-error {{non-local variable 'g_runtime_array' requires constant initialization under profile 'std::init'}} + +constinit int g_ci = 0; +// The constinit hard error fires regardless of -fprofiles. +constinit int g_ci_runtime = runtime(); +// expected-error@-1 {{variable does not have a constant initializer}} +// expected-note@-2 {{required by 'constinit' specifier here}} +// expected-note@-3 {{non-constexpr function 'runtime' cannot be used in a constant expression}} +// no-profiles-error@-4 {{variable does not have a constant initializer}} +// no-profiles-note@-5 {{required by 'constinit' specifier here}} +// no-profiles-note@-6 {{non-constexpr function 'runtime' cannot be used in a constant expression}} + +namespace inside { + int n_runtime = runtime(); // expected-error {{non-local variable 'n_runtime' requires constant initialization under profile 'std::init'}} +} + +void test_locals() { + int x = runtime(); + static int s = runtime(); + thread_local int t = runtime(); + (void)x; (void)s; (void)t; +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_runtime_init")]] +int g_suppressed = runtime(); + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] +int g_suppressed_all = runtime(); + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::other)]] +int g_wrong_suppress = runtime(); // expected-error {{non-local variable 'g_wrong_suppress' requires constant initialization under profile 'std::init'}} diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp new file mode 100644 index 0000000000000..74e74cc4f33ee --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -0,0 +1,60 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +void test_postfix_eq() { + int c [[uninitialized]] = 0; // expected-error {{variable 'c' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + (void)c; +} + +void test_prefix_eq() { + [[uninitialized]] int d = 7; // expected-error {{variable 'd' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + (void)d; +} + +void test_brace_init() { + int e [[uninitialized]] {}; // expected-error {{variable 'e' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + (void)e; +} + +void test_paren_init() { + int f [[uninitialized]] (3); // expected-error {{variable 'f' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + (void)f; +} + +void test_marker_alone() { + int x [[uninitialized]]; + (void)x; +} + +void test_init_alone() { + int x = 0; + (void)x; +} + +int g_marker_with_init [[uninitialized]] = 42; // expected-error {{variable 'g_marker_with_init' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] + int x [[uninitialized]] = 0; + (void)x; +} + +void test_suppress_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int a [[uninitialized]] = 0; + int b [[uninitialized]] = 1; + (void)a; (void)b; + } +} + +template +void template_marker_with_init() { + T x [[uninitialized]] = T{}; // expected-error 2 {{variable 'x' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + (void)x; +} +template void template_marker_with_init(); // expected-note {{in instantiation of function template specialization 'template_marker_with_init' requested here}} From faec8e4c80132ffb75b72855402f8cd63abd3996 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 19 May 2026 12:49:33 -0400 Subject: [PATCH 068/289] Class finalization profiles hook + test::class_final test profile --- clang/docs/ProfilesFramework.rst | 115 +++++++++++++- .../clang/Basic/DiagnosticSemaKinds.td | 2 + clang/include/clang/Sema/Sema.h | 6 + clang/lib/Sema/SemaDeclCXX.cpp | 40 +++++ .../SemaCXX/safety-profile-class-final.cpp | 150 ++++++++++++++++++ 5 files changed, 309 insertions(+), 4 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-class-final.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 239cfab2ca9a5..8c20891812c19 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -247,6 +247,79 @@ overload of ``isProfileSuppressed`` examines: - The enclosing ``Decl`` chain via ``getLexicalDeclContext()``. +Pattern 3: Class-Finalization Profile +------------------------------------- + +Used when the rule applies to a class as a whole and needs to run once after +the class definition is complete -- for example, "every non-private field of +this class must satisfy property X" or "this class's field set must look +like Y." ``test::class_final`` is the in-tree example. + +The dispatch point is ``Sema::checkProfileViolationsAtClassFinalization``, +called from the end of ``Sema::CheckCompletedCXXClass`` in +``clang/lib/Sema/SemaDeclCXX.cpp``. ``CheckCompletedCXXClass`` is the +single function reached from every class-completion path -- the parser +(``ActOnFinishCXXMemberSpecification``), template instantiation +(``InstantiateClass``), and lambda completion -- so wiring the hook there +covers all of them with no extra plumbing. + +The dispatcher filters out classes the rules are not meant to see: + +- Dependent classes (``isDependentType()``). The hook will re-fire on each + instantiation via the template-instantiation completion path. +- Invalid classes (``isInvalidDecl()``). +- Lambdas (``isLambda()``). Closure types have no user-controlled field + shape, so class-finalization rules do not apply. + +This pattern needs two pieces, both colocated with the dispatcher. + +1. **Add the profile to the class-finalization opt-in table.** + ``ClassFinalizationProfiles`` in ``clang/lib/Sema/SemaDeclCXX.cpp`` is a + small per-pass table of profile name plus callback. One row per profile: + + .. code-block:: c++ + + struct ClassFinalizationProfileEntry { + StringRef Name; + void (*Callback)(Sema &, CXXRecordDecl *); + }; + constexpr ClassFinalizationProfileEntry ClassFinalizationProfiles[] = { + {"my::profile", &runMyProfileCallback}, + }; + + The dispatcher iterates the table, skips entries whose profile is not + enforced, sets up a ``ProfileSuppressScope(*this, RD, + /*WalkLexicalParents=*/true)``, and invokes the callback. Because the + suppress scope is established by the dispatcher, the callback can use + the location-based ``shouldEmitProfileViolation`` overload and have + ``[[profiles::suppress]]`` on the class or any enclosing lexical + ``Decl`` work correctly. + +2. **Emit diagnostics from the callback via** + ``Sema::shouldEmitProfileViolation``. Each callback decides where on + the class to point and which diagnostic to use, possibly with notes: + + .. code-block:: c++ + + void runMyProfileCallback(Sema &S, CXXRecordDecl *RD) { + if (!S.shouldEmitProfileViolation("my::profile", /*Rule=*/"", + RD->getLocation())) + return; + S.Diag(RD->getLocation(), diag::err_my_profile_rule) + << "my::profile" << RD; + } + +The diagnostic itself is defined with ``ProfileRuleError`` as in patterns 1 +and 2. + +Unlike the post-parse CFG pass, class-finalization callbacks run while the +``CXXRecordDecl`` is being finalized (immediately before +``CheckCompletedCXXClass`` returns). Out-of-line member definitions -- +including constructor bodies -- have not yet been parsed when the callback +runs. Rules that need ctor-body flow analysis must therefore live in a +post-parse CFG pass (pattern 2), not here. + + .. _profiles-token-dominion: Suppression Dominion is Token-Based @@ -354,11 +427,13 @@ The following parts of P3589R2 are deliberately not implemented: Built-in Profiles ================= -The tree ships three built-in profiles, all gated on ``-fprofiles``: +The tree ships four built-in profiles, all gated on ``-fprofiles``: - ``test::type_cast`` (test-only) -- pattern-1 example. - ``test::uninit_read`` (test-only) -- pattern-2 example riding the existing CFG uninitialized-variables analysis. +- ``test::class_final`` (test-only) -- pattern-3 example riding the + class-finalization dispatch. - ``std::init`` (initial slice of the proposed initialization profile from Stroustrup's draft, on top of P3589R2 and P3402R3). It rides the same CFG dispatch as ``test::uninit_read`` for one of its rules and adds three @@ -367,7 +442,8 @@ The tree ships three built-in profiles, all gated on ``-fprofiles``: By convention: - Real test profiles live under the ``test::`` namespace. Today there are - two: ``test::type_cast`` and ``test::uninit_read``. + three: ``test::type_cast``, ``test::uninit_read``, and + ``test::class_final``. - The names ``test::other``, ``test::bounds``, ``test::new_profile``, and ``test::not_enforced`` are deliberately *not* implemented and appear only in negative tests as stand-in "some other profile" names. Adding a real @@ -423,12 +499,37 @@ work for this profile: by the time the CFG analysis runs, the parse-time directly via ``shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``. +The ``test::class_final`` Profile +--------------------------------- + +A pattern-3 (class-finalization) profile. Demonstrates the case where the +rule applies once per completed class definition and runs from the +class-finalization dispatch in ``Sema::CheckCompletedCXXClass``. + +- **Rules**: none (the profile has a single implicit rule, so the rule + string is empty). +- **Diagnostic**: ``err_profile_class_final_test`` ("test profile fired on + completion of class %1 under profile '%0'"). +- **Opt-in table**: ``ClassFinalizationProfiles`` in + ``clang/lib/Sema/SemaDeclCXX.cpp``. + +Because dependent classes are filtered out by the dispatcher, the +diagnostic fires on class template *instantiations* rather than on the +primary template. Lambda closures are also skipped. +``[[profiles::suppress(test::class_final)]]`` on the class or any +enclosing lexical ``Decl`` silences the diagnostic via the +``ProfileSuppressScope(*this, RD, /*WalkLexicalParents=*/true)`` the +dispatcher establishes around each callback. + + The ``std::init`` Profile (initial slice) ----------------------------------------- A first slice of the proposed initialization profile, intentionally minimal: -no ``[[ref_to_uninit]]``, no field-level marker, no class-finalization hook, -no new dataflow analyses. See the audit and plan documents in +no ``[[ref_to_uninit]]``, no field-level marker, no new dataflow analyses. +The class-finalization dispatch (pattern 3) now exists in the framework but +``std::init`` does not yet register any class-finalization callbacks; those +land with the §6 / §6.2 work. See the audit and plan documents in ``.cursor/plans/`` for the deferred items. The slice introduces one new attribute and four rules. @@ -560,6 +661,12 @@ profiles. When changing the framework, run them all with analysis-based-warnings early-exit-on-first-error does not hide later cases; case 0 is the no-violation baseline used by both the ``-fprofiles`` and the without-``-fprofiles`` runs. +- ``clang/test/SemaCXX/safety-profile-class-final.cpp`` -- the + ``test::class_final`` profile: end-to-end exercise of the + class-finalization dispatch (pattern 3) including basic firing, class + template instantiation, lambda skipping, suppression on the class and on + enclosing lexical parents, SFINAE exclusion, and the + without-``-fprofiles`` ignored path. - ``clang/test/SemaCXX/safety-profile-init-read.cpp`` -- the ``std::init`` profile's ``uninit_read`` rule. Same ``-DCASE=N`` style as the ``test::uninit_read`` test; CASE=4 additionally enforces diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 4b78a60255621..caf96415a4b0e 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14138,6 +14138,8 @@ def err_profile_type_cast_reinterpret : ProfileRuleError< "'reinterpret_cast' is unsafe under profile '%0'">; def err_profile_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; +def err_profile_class_final_test : ProfileRuleError< + "test profile fired on completion of class %1 under profile '%0'">; def err_init_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; def err_init_uninit_decl : ProfileRuleError< diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 10a022540ae81..5368110c810d6 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -6035,6 +6035,12 @@ class Sema final : public SemaBase { /// \param Record The completed class. void CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record); + /// Dispatch class-finalization profile callbacks for a completed class. + /// Called from \c CheckCompletedCXXClass so parser, template instantiation, + /// and lambda finalization paths all reach the same hook. Dependent, + /// invalid, and lambda classes are filtered out. + void checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD); + /// Check that the C++ class annoated with "trivial_abi" satisfies all the /// conditions that are needed for the attribute to have an effect. void checkIllFormedTrivialABIStruct(CXXRecordDecl &RD); diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 2ae6e5de0e3ee..a82bbe56f8dbd 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7021,6 +7021,31 @@ ReportOverrides(Sema &S, unsigned DiagID, const CXXMethodDecl *MD, return IssuedDiagnostic; } +namespace { +// Profiles that opt into the class-finalization dispatch. Each entry pairs +// the profile name with a callback invoked once per completed, +// non-dependent, non-lambda, non-invalid CXXRecordDecl. Adding a new +// profile is a single row here plus a ProfileRuleError diagnostic in +// DiagnosticSemaKinds.td and a callback that consults +// Sema::shouldEmitProfileViolation before emitting. +struct ClassFinalizationProfileEntry { + StringRef Name; + void (*Callback)(Sema &, CXXRecordDecl *); +}; + +void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { + if (!S.shouldEmitProfileViolation("test::class_final", /*Rule=*/"", + RD->getLocation())) + return; + S.Diag(RD->getLocation(), diag::err_profile_class_final_test) + << "test::class_final" << RD; +} + +constexpr ClassFinalizationProfileEntry ClassFinalizationProfiles[] = { + {"test::class_final", &runTestClassFinalCallback}, +}; +} // namespace + void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { if (!Record) return; @@ -7440,6 +7465,21 @@ void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { }; CheckMismatchedTypeAwareAllocators(OO_New, OO_Delete); CheckMismatchedTypeAwareAllocators(OO_Array_New, OO_Array_Delete); + + checkProfileViolationsAtClassFinalization(Record); +} + +void Sema::checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD) { + if (!getLangOpts().Profiles || !RD) + return; + if (RD->isInvalidDecl() || RD->isDependentType() || RD->isLambda()) + return; + for (const auto &E : ClassFinalizationProfiles) { + if (!isProfileEnforced(E.Name)) + continue; + ProfileSuppressScope Scope(*this, RD, /*WalkLexicalParents=*/true); + E.Callback(*this, RD); + } } /// Look up the special member function that would be called by a special diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp new file mode 100644 index 0000000000000..ecebdc0b1cb42 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -0,0 +1,150 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::class_final)]]; +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::other)]]; + +struct Plain { // expected-error {{test profile fired on completion of class 'Plain' under profile 'test::class_final'}} + int m; +}; + +class PlainClass { // expected-error {{test profile fired on completion of class 'PlainClass' under profile 'test::class_final'}} +public: + int m; +}; + +union PlainUnion { // expected-error {{test profile fired on completion of class 'PlainUnion' under profile 'test::class_final'}} + int a; + float b; +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final)]] SuppressedOnClass { + int m; +}; + +// Suppress with rule restriction (the implicit-rule profile uses rule ""). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final, rule: "")]] SuppressedOnClassByRule { + int m; +}; + +// Non-matching profile does not suppress. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::other)]] NotSuppressedWrongProfile { // expected-error {{test profile fired on completion of class 'NotSuppressedWrongProfile' under profile 'test::class_final'}} + int m; +}; + +// Lambdas are filtered out by the dispatcher: the implicit closure types +// are not diagnosed even though they are CXXRecordDecls that go through +// CheckCompletedCXXClass. +void test_lambda_skip() { + auto f = []() { return 1; }; + auto g = [](int x) { return x; }; + (void)f; (void)g; +} + +// Generic lambdas: the closure's call operator is a template, but the +// closure type itself is still a lambda and must be filtered. +void test_generic_lambda_skip() { + auto f = [](auto x) { return x; }; + (void)f(0); + (void)f(0.0); +} + +// A dependent primary template does not fire on the template itself; the +// instantiated specialization's diagnostic is emitted at the primary +// template's location with a note at the instantiation site. +template +struct PrimaryTemplate { // expected-error {{test profile fired on completion of class 'PrimaryTemplate' under profile 'test::class_final'}} \ + // expected-error {{test profile fired on completion of class 'PrimaryTemplate' under profile 'test::class_final'}} + T m; +}; + +PrimaryTemplate instantiate_primary; // expected-note {{in instantiation of template class 'PrimaryTemplate' requested here}} + +PrimaryTemplate instantiate_primary_float; // expected-note {{in instantiation of template class 'PrimaryTemplate' requested here}} + +// Explicit specialization is a fresh definition and fires at its own line. +template <> +struct PrimaryTemplate { // expected-error {{test profile fired on completion of class 'PrimaryTemplate' under profile 'test::class_final'}} + char m; +}; + +// Suppress on the primary template carries through instantiation via the +// lexical-parent walk on the instantiated CXXRecordDecl. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final)]] SuppressedTemplate { + T m; +}; +SuppressedTemplate suppressed_template_inst; + +// Suppress on an enclosing namespace silences a nested class via the +// lexical-parent walk. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::class_final)]] suppressed_ns { + struct NestedInSuppressedNS { + int m; + }; + struct AlsoNested { + int m; + }; +} + +// Suppress on an enclosing class silences a nested class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::class_final)]] OuterSuppressed { // OuterSuppressed itself is suppressed. + struct Inner { // Inner reaches OuterSuppressed via lexical-parent walk. + int m; + }; + struct AlsoInner { + int m; + }; +}; + +// Suppress on an enclosing namespace also silences template instantiations +// because the instantiated CXXRecordDecl's lexical parent chain still +// reaches the namespace. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::class_final)]] suppressed_template_ns { + template + struct InsideSuppressedNS { + T m; + }; +} +suppressed_template_ns::InsideSuppressedNS inside_suppressed_ns_inst; + +// Non-matching suppress at namespace scope does not silence the inner class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(test::other)]] wrong_profile_ns { + struct NotSuppressed { // expected-error {{test profile fired on completion of class 'NotSuppressed' under profile 'test::class_final'}} + int m; + }; +} + +// SFINAE: a class template whose instantiation would fire the profile +// diagnostic must not cause the substitution to fail. ProfileRuleError uses +// SFINAE_Suppress, so the diagnostic is suppressed during deduction; the +// first overload is selected because the substitution succeeds. The +// diagnostic is then replayed at the class definition (with the usual +// instantiation-context notes). +template +struct SfinaeTriggered { // expected-error {{test profile fired on completion of class 'SfinaeTriggered' under profile 'test::class_final'}} + using type = T; +}; + +template +auto sfinae_pick(T) -> typename SfinaeTriggered::type { return T{}; } // expected-note {{in instantiation of template class 'SfinaeTriggered' requested here}} + +template +auto sfinae_pick(...) -> int { return 1; } + +static_assert(__is_same(decltype(sfinae_pick(0L)), long), // expected-note {{while substituting explicitly-specified template arguments into function template 'sfinae_pick'}} + "profile violation must not SFINAE out the first overload"); + +// Without `-fprofiles`, the enforce attribute is `warn_attribute_ignored` +// and the diagnostic never fires. This is exercised by the no-profiles RUN +// line, which expects only the two attribute-ignored warnings above. From 5f155d7498ae9664ec6a6ebb24b686a2a2bb476a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 19 May 2026 15:31:01 -0400 Subject: [PATCH 069/289] Hoist ProfileSuppressScope for class finalization callbacks out of loop --- clang/lib/Sema/SemaDeclCXX.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index a82bbe56f8dbd..b2796dbc93118 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7474,10 +7474,13 @@ void Sema::checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD) { return; if (RD->isInvalidDecl() || RD->isDependentType() || RD->isLambda()) return; + // ProfileSuppressScope is profile-agnostic (it pushes every + // [[profiles::suppress]] entry it finds and isProfileSuppressed filters by + // name later), so set it up once for all callbacks. + ProfileSuppressScope Scope(*this, RD, /*WalkLexicalParents=*/true); for (const auto &E : ClassFinalizationProfiles) { if (!isProfileEnforced(E.Name)) continue; - ProfileSuppressScope Scope(*this, RD, /*WalkLexicalParents=*/true); E.Callback(*this, RD); } } From 1863184dcf1c0837a97d615b85ce75f07b7369cc Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 19 May 2026 15:33:42 -0400 Subject: [PATCH 070/289] Add more class finalization profile callback tests --- .../SemaCXX/safety-profile-class-final.cpp | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp index ecebdc0b1cb42..126d1164242c8 100644 --- a/clang/test/SemaCXX/safety-profile-class-final.cpp +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -145,6 +145,79 @@ auto sfinae_pick(...) -> int { return 1; } static_assert(__is_same(decltype(sfinae_pick(0L)), long), // expected-note {{while substituting explicitly-specified template arguments into function template 'sfinae_pick'}} "profile violation must not SFINAE out the first overload"); +// Local classes inside a function body fire (a function is not itself a +// class-finalization subject). +void test_local_class() { + struct LocalInFn { int m; }; // expected-error {{test profile fired on completion of class 'LocalInFn' under profile 'test::class_final'}} + LocalInFn x; + (void)x; +} + +// Function-level suppress silences a local class defined in its body via +// the parse-time ProfileSuppressStack (not via the lexical-parent walk; +// the walk goes through the *Decl* chain, not statement context). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] +void test_local_class_suppressed_via_fn() { + struct LocalInSuppressedFn { int m; }; + LocalInSuppressedFn x; + (void)x; +} + +// Local classes inside a lambda *body* still fire. Only the closure type +// itself is filtered by isLambda(); user-defined classes nested inside the +// closure's call operator are not lambdas. +void test_local_class_inside_lambda() { + auto f = []() { + struct LocalInLambda { int m; }; // expected-error {{test profile fired on completion of class 'LocalInLambda' under profile 'test::class_final'}} + LocalInLambda x; + (void)x; + }; + f(); +} + +// Anonymous union and anonymous struct members fire with synthesized +// "(unnamed ...)" diagnostic names. +struct HasAnonymousMembers { // expected-error {{test profile fired on completion of class 'HasAnonymousMembers' under profile 'test::class_final'}} + union { // expected-error {{test profile fired on completion of class '(unnamed union}} + int a; + float b; + }; + struct { // expected-error {{test profile fired on completion of class '(unnamed struct}} + int x; + }; +}; + +// Class template partial specialization instantiation. The partial +// specialization itself is dependent (skipped); its concrete instantiation +// fires at the primary template's location with a note at the use site. +template struct PartialSpec { T m; }; // expected-error {{test profile fired on completion of class 'PartialSpec' under profile 'test::class_final'}} +template struct PartialSpec { + T *p; +}; +void use_partial_spec() { + PartialSpec x; // expected-note {{in instantiation of template class 'PartialSpec' requested here}} + (void)x; +} + +// Explicit instantiation *definition* fires at the explicit-instantiation +// directive's own line (unlike implicit instantiation, which fires at the +// primary template's line). +template struct ExplicitInst { T m; }; +template struct ExplicitInst; // expected-error {{test profile fired on completion of class 'ExplicitInst' under profile 'test::class_final'}} \ + // expected-note {{in instantiation of template class 'ExplicitInst' requested here}} + +// Explicit instantiation *declaration* (extern template) still instantiates +// the class itself per C++ rules, so it also fires at its own line. +extern template struct ExplicitInst; // expected-error {{test profile fired on completion of class 'ExplicitInst' under profile 'test::class_final'}} \ + // expected-note {{in instantiation of template class 'ExplicitInst' requested here}} + +// Friend class definitions are completed normally and fire at their own line. +class HasFriendDecl { // expected-error {{test profile fired on completion of class 'HasFriendDecl' under profile 'test::class_final'}} + friend struct FriendedLater; +}; +struct FriendedLater { int m; }; // expected-error {{test profile fired on completion of class 'FriendedLater' under profile 'test::class_final'}} + // Without `-fprofiles`, the enforce attribute is `warn_attribute_ignored` // and the diagnostic never fires. This is exercised by the no-profiles RUN // line, which expects only the two attribute-ignored warnings above. From e6fbc63ccf0ad7aaaacea98f9c3c01633cb824ac Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 22 May 2026 13:41:56 -0400 Subject: [PATCH 071/289] Expand [[uninitalized]] to FieldDecl --- clang/docs/ProfilesFramework.rst | 13 +++--- clang/include/clang/Basic/Attr.td | 2 +- clang/include/clang/Basic/AttrDocs.td | 20 ++++++--- .../safety-profile-init-field-marker.cpp | 44 +++++++++++++++++++ 4 files changed, 66 insertions(+), 13 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-init-field-marker.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 8c20891812c19..b497b5a282117 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -538,13 +538,16 @@ Marker attribute ~~~~~~~~~~~~~~~~ ``[[uninitialized]]`` (a standard C++11 attribute, distinct from the Clang -vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` as -intentionally left uninitialized. Recognised by Clang regardless of -``-fprofiles``; only carries semantic weight when ``std::init`` is enforced. +vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` or +``FieldDecl`` as intentionally left uninitialized. Recognised by Clang +regardless of ``-fprofiles``; only carries semantic weight when +``std::init`` is enforced. - TableGen def: ``CXX11Uninitialized`` in ``clang/include/clang/Basic/Attr.td``. -- Subjects: ``Var`` only. Field-level support is the framework gap §2.A.2 - and is deferred. +- Subjects: ``Var`` and ``Field``. Field-level placement is accepted but + currently inert; rules that consume the field marker (paper §6 + aggregate-without-ctor, §6.1 ctor-body member-init, classes-exposing- + uninitialized-memory) are deferred to later stages. - Behaviour: - Suppresses R2 (``uninit_decl``) on the marked declaration. diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index f0566f0391ffb..be9213b6bf27b 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1723,7 +1723,7 @@ def CXX11NoReturn : InheritableAttr { def CXX11Uninitialized : InheritableAttr { let Spellings = [CXX11<"", "uninitialized", 202602>]; - let Subjects = SubjectList<[Var], ErrorDiag>; + let Subjects = SubjectList<[Var, Field], ErrorDiag>; let Documentation = [CXX11UninitializedDocs]; let SimpleHandler = 1; } diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 44743d281ca52..d1144e9e09360 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -879,10 +879,10 @@ def CXX11UninitializedDocs : Documentation { let Category = DocCatVariable; let Heading = "uninitialized"; let Content = [{ -The ``[[uninitialized]]`` attribute marks a variable definition as -intentionally left uninitialized. It is purely informational: it has no -effect on code generation and the variable still has the same indeterminate -value it would have without the attribute. +The ``[[uninitialized]]`` attribute marks a variable or non-static data +member definition as intentionally left uninitialized. It is purely +informational: it has no effect on code generation and the entity still +has the same indeterminate value it would have without the attribute. The attribute exists to support the initialization profile (see :doc:`ProfilesFramework`). Under that profile, a definition such as @@ -893,9 +893,15 @@ is intentional. The attribute does **not** suppress diagnostics that fire when the variable is later read before being assigned; the read-before-init rule still applies. -A declaration that has both ``[[uninitialized]]`` and an initializer is -ill-formed under the initialization profile (the marker contradicts the -explicit initialization). +A declaration or non-static data member with both ``[[uninitialized]]`` +and an initializer is ill-formed under the initialization profile (the +marker contradicts the explicit initialization). + +When placed on a non-static data member, the attribute is currently inert: +the parser accepts the placement but no diagnostic is tied to it yet. Its +semantic weight is added by the classes-without-constructors rule, the +class-exposes-uninitialized-memory rule, and the constructor-body +member-initialization rule, which are staged separately. Outside the initialization profile the attribute is silently accepted and has no effect, so code that uses it remains valid when compiled without diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp new file mode 100644 index 0000000000000..657afa47ee9f7 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -0,0 +1,44 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +struct PlainField { + int m [[uninitialized]]; +}; + +struct PlainFieldPrefix { + [[uninitialized]] int m; +}; + +struct FieldWithNSDMI { + int m [[uninitialized]] = 0; +}; + +struct FieldWithNSDMIPrefix { + [[uninitialized]] int m = 0; +}; + +struct WithStaticDataMember { + static int s [[uninitialized]]; + [[uninitialized]] static int t; +}; +int WithStaticDataMember::s; +int WithStaticDataMember::t; + +struct MultipleFields { + int a [[uninitialized]]; + int b = 0; + int c [[uninitialized]] = 0; +}; + +template +struct DependentField { + T m [[uninitialized]]; +}; +template struct DependentField; + +// expected-error@+2 {{'uninitialized' attribute only applies to variables and non-static data members}} +// no-profiles-error@+1 {{'uninitialized' attribute only applies to variables and non-static data members}} +[[uninitialized]] void f(); From 49efd24f1f2815aaafbcd77f7705591a7a1f1c5c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 14:48:33 -0400 Subject: [PATCH 072/289] Remove stale .cursor/plans reference from profiles docs --- clang/docs/ProfilesFramework.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index b497b5a282117..5973c0c7e6f2c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -528,9 +528,8 @@ The ``std::init`` Profile (initial slice) A first slice of the proposed initialization profile, intentionally minimal: no ``[[ref_to_uninit]]``, no field-level marker, no new dataflow analyses. The class-finalization dispatch (pattern 3) now exists in the framework but -``std::init`` does not yet register any class-finalization callbacks; those -land with the §6 / §6.2 work. See the audit and plan documents in -``.cursor/plans/`` for the deferred items. +``std::init`` does not yet register any class-finalization callbacks; the +paper §6 / §6.2 class rules are deferred to later stages. The slice introduces one new attribute and four rules. From 4a9e83d3781701d0a686b51e262a33006d7d4e84 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 14:49:56 -0400 Subject: [PATCH 073/289] Test [[uninitialized]] with a language-synthesized initializer A class-typed variable whose default constructor runs counts as having an initializer, so combining it with [[uninitialized]] fires R4 (uninit_with_initializer). The behavior already holds; add coverage. --- .../SemaCXX/safety-profile-init-with-initializer.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index 74e74cc4f33ee..a32aa53ecd632 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -34,6 +34,16 @@ void test_init_alone() { (void)x; } +struct WithCtor { WithCtor(); }; + +void test_class_synthesized_init() { + // The implicit default-constructor call is the initializer, so the marker + // contradicts it even though there is no explicit initializer. + WithCtor x [[uninitialized]]; // expected-error {{variable 'x' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + [[uninitialized]] WithCtor y; // expected-error {{variable 'y' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + (void)x; (void)y; +} + int g_marker_with_init [[uninitialized]] = 42; // expected-error {{variable 'g_marker_with_init' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} void test_suppress() { From f26e19e3b0e6aea8983646df639b75a973f161c2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 15:11:58 -0400 Subject: [PATCH 074/289] Don't fire std::init static_runtime_init on trivial default-init R3 (static_runtime_init) fired for any non-local variable with a non-constant formal initializer, including zero-initialized aggregates like `struct S { int x; }; S g;` that need no global constructor. Gate it on checkConstInit() so it mirrors -Wglobal-constructors and only rejects globals that actually require run-time initialization. --- clang/lib/Sema/SemaDecl.cpp | 11 ++++++++--- clang/test/SemaCXX/safety-profile-init-static.cpp | 10 ++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index fe2c50acced59..ee7b3618d48f6 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15085,11 +15085,16 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { } else if (IsGlobal && [&] { // std::init / static_runtime_init: paper says non-local // statics must be initialized at compile or link time. - // Runs before -Wglobal-constructors so the profile error - // (when enforced) takes precedence over the standalone - // warning. + // checkConstInit() permits trivial default initialization + // (not a constant initializer but needs no global + // constructor), so a zero-initialized aggregate such as + // `struct S { int x; }; S g;` is not a violation. Runs before + // -Wglobal-constructors so the profile error (when enforced) + // takes precedence over the standalone warning. static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "static_runtime_init"; + if (checkConstInit()) + return false; if (!shouldEmitProfileViolation(Profile, Rule, var->getLocation())) return false; diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp index 53897a7ba25a6..408cddf1f4271 100644 --- a/clang/test/SemaCXX/safety-profile-init-static.cpp +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -14,6 +14,16 @@ int g_array_const[3] = {1, 2, 3}; int g_runtime = runtime(); // expected-error {{non-local variable 'g_runtime' requires constant initialization under profile 'std::init'}} int g_runtime_array[3] = {1, runtime(), 3}; // expected-error {{non-local variable 'g_runtime_array' requires constant initialization under profile 'std::init'}} +struct Trivial { int x; }; +struct WithDtor { ~WithDtor(); }; +struct WithCtor { WithCtor(); }; + +int g_scalar; +Trivial g_trivial; +Trivial g_trivial_braced = {}; +WithDtor g_with_dtor; +WithCtor g_with_ctor; // expected-error {{non-local variable 'g_with_ctor' requires constant initialization under profile 'std::init'}} + constinit int g_ci = 0; // The constinit hard error fires regardless of -fprofiles. constinit int g_ci_runtime = runtime(); From be05a1f5fa13b92893980b57430818b6c8597367 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 15:51:16 -0400 Subject: [PATCH 075/289] Don't fire uninit_with_initializer on a RecoveryExpr placeholder When default-initialization of a [[uninitialized]]-marked variable fails (e.g. a deleted default constructor), a RecoveryExpr is installed as the initializer. That is a placeholder for a failed initialization, not a user-written initializer, so R4 (uninit_with_initializer) must not add a spurious second diagnostic. --- clang/lib/Sema/SemaDecl.cpp | 7 +++++-- .../safety-profile-init-with-initializer.cpp | 13 +++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index ee7b3618d48f6..4f8045e87ccc0 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14837,8 +14837,11 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { // std::init / uninit_with_initializer: [[uninitialized]] documents that // the variable is intentionally left uninitialized, so it contradicts an - // explicit initializer. - if (var->hasInit() && var->hasAttr()) { + // explicit initializer. A RecoveryExpr is a placeholder for an + // initialization that already failed (e.g. default-init of a const scalar), + // not an initializer the user wrote, so it must not trigger this rule. + if (var->hasInit() && !isa(var->getInit()->IgnoreParens()) && + var->hasAttr()) { static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "uninit_with_initializer"; if (shouldEmitProfileViolation(Profile, Rule, var->getLocation())) diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index a32aa53ecd632..692368d189aff 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -44,6 +44,19 @@ void test_class_synthesized_init() { (void)x; (void)y; } +struct NoDefaultCtor { NoDefaultCtor() = delete; }; // expected-note {{'NoDefaultCtor' has been explicitly marked deleted here}} \ + // no-profiles-note {{'NoDefaultCtor' has been explicitly marked deleted here}} + +void test_failed_init_no_double_diag() { + // Default-init fails and installs a RecoveryExpr placeholder. That is not a + // user-written initializer, so R4 must not pile a spurious diagnostic on top + // of the real error (the absence of an extra '...have an initializer...' + // diagnostic here is the assertion). + NoDefaultCtor z [[uninitialized]]; // expected-error {{call to deleted constructor of 'NoDefaultCtor'}} \ + // no-profiles-error {{call to deleted constructor of 'NoDefaultCtor'}} + (void)z; +} + int g_marker_with_init [[uninitialized]] = 42; // expected-error {{variable 'g_marker_with_init' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} void test_suppress() { From 7093919c6f50493cf2bc7fb7359bb160524c2669 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 16:12:20 -0400 Subject: [PATCH 076/289] Reject [[uninitialized]] on subjects that can't be uninitialized Replace the auto-generated SimpleHandler for CXX11Uninitialized with a custom handler that diagnoses placement on a reference, a function parameter, or a structured binding, where "leave uninitialized" is meaningless. Like other ill-formed attribute placements, these are rejected regardless of -fprofiles. --- clang/include/clang/Basic/Attr.td | 1 - .../clang/Basic/DiagnosticSemaKinds.td | 3 ++ clang/lib/Sema/SemaDeclAttr.cpp | 31 +++++++++++++++++++ .../safety-profile-init-field-marker.cpp | 18 +++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index be9213b6bf27b..5c27f306a5162 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1725,7 +1725,6 @@ def CXX11Uninitialized : InheritableAttr { let Spellings = [CXX11<"", "uninitialized", 202602>]; let Subjects = SubjectList<[Var, Field], ErrorDiag>; let Documentation = [CXX11UninitializedDocs]; - let SimpleHandler = 1; } def NonBlocking : TypeAttr { diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index caf96415a4b0e..c6e8803899757 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14151,4 +14151,7 @@ def err_init_static_runtime_init : ProfileRuleError< def err_init_uninit_with_initializer : ProfileRuleError< "variable %1 cannot be both '[[uninitialized]]' and have an initializer " "under profile '%0'">; +def err_uninitialized_attr_invalid_subject : Error< + "'uninitialized' attribute cannot be applied to %select{a reference|" + "a function parameter|a structured binding}0">; } // end of sema component. diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 158f0d2d2b144..54de4cda07579 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6941,6 +6941,33 @@ static void handleUninitializedAttr(Sema &S, Decl *D, const ParsedAttr &AL) { D->addAttr(::new (S.Context) UninitializedAttr(S.Context, AL)); } +static void handleCXX11UninitializedAttr(Sema &S, Decl *D, + const ParsedAttr &AL) { + // The SubjectList has already restricted D to a variable or non-static data + // member. Reject the subjects for which "leave uninitialized" is + // meaningless: a reference (must bind when declared), a function parameter + // (initialized by the caller), and a structured binding (requires an + // initializer). These are rejected regardless of -fprofiles, like any other + // ill-formed attribute placement. + enum InvalidSubject { Reference, Parameter, StructuredBinding }; + std::optional Invalid; + if (isa(D)) + Invalid = Parameter; + else if (isa(D)) + Invalid = StructuredBinding; + else if (cast(D)->getType()->isReferenceType()) + Invalid = Reference; + + if (Invalid) { + S.Diag(AL.getLoc(), diag::err_uninitialized_attr_invalid_subject) + << static_cast(*Invalid); + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) CXX11UninitializedAttr(S.Context, AL)); +} + static void handleMIGServerRoutineAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // Check that the return type is a `typedef int kern_return_t` or a typedef // around it, because otherwise MIG convention checks make no sense. @@ -8207,6 +8234,10 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, handleUninitializedAttr(S, D, AL); break; + case ParsedAttr::AT_CXX11Uninitialized: + handleCXX11UninitializedAttr(S, D, AL); + break; + case ParsedAttr::AT_ObjCExternallyRetained: S.ObjC().handleExternallyRetainedAttr(D, AL); break; diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 657afa47ee9f7..b57b5d6879ff1 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -42,3 +42,21 @@ template struct DependentField; // expected-error@+2 {{'uninitialized' attribute only applies to variables and non-static data members}} // no-profiles-error@+1 {{'uninitialized' attribute only applies to variables and non-static data members}} [[uninitialized]] void f(); + +// Subjects on which "leave uninitialized" is meaningless are rejected +// regardless of -fprofiles. +struct ReferenceField { + int &r [[uninitialized]]; // expected-error {{'uninitialized' attribute cannot be applied to a reference}} \ + // no-profiles-error {{'uninitialized' attribute cannot be applied to a reference}} +}; + +void test_invalid_subjects(int p [[uninitialized]]) { // expected-error {{'uninitialized' attribute cannot be applied to a function parameter}} \ + // no-profiles-error {{'uninitialized' attribute cannot be applied to a function parameter}} + int n = 0; + int &lr [[uninitialized]] = n; // expected-error {{'uninitialized' attribute cannot be applied to a reference}} \ + // no-profiles-error {{'uninitialized' attribute cannot be applied to a reference}} + int arr[2] = {1, 2}; + [[uninitialized]] auto [a, b] = arr; // expected-error {{'uninitialized' attribute cannot be applied to a structured binding}} \ + // no-profiles-error {{'uninitialized' attribute cannot be applied to a structured binding}} + (void)p; (void)lr; (void)a; (void)b; +} From 77576cb394cda8c4be5e29884ba88cf3a25045e2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 16:30:55 -0400 Subject: [PATCH 077/289] Ban [[uninitialized]] on unions under std::init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the union_marker rule of the std::init profile (paper §6.5): the marker on a union object or a union member is diagnosed because delayed initialization by assigning a member would be an erroneous assignment when compiled without the profile. Unlike a reference or parameter, a union may legitimately be uninitialized, so this is a gated, suppressible ProfileRuleError rather than an unconditional rejection. --- .../clang/Basic/DiagnosticSemaKinds.td | 3 ++ clang/lib/Sema/SemaDeclAttr.cpp | 17 ++++++++++ .../SemaCXX/safety-profile-init-union.cpp | 33 +++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-init-union.cpp diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index c6e8803899757..7838c99cce63d 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14154,4 +14154,7 @@ def err_init_uninit_with_initializer : ProfileRuleError< def err_uninitialized_attr_invalid_subject : Error< "'uninitialized' attribute cannot be applied to %select{a reference|" "a function parameter|a structured binding}0">; +def err_init_union_marker : ProfileRuleError< + "'[[uninitialized]]' cannot be applied to %select{a variable of union type|" + "a union member}1 under profile '%0'">; } // end of sema component. diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 54de4cda07579..1d9018fadbcf6 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6965,6 +6965,23 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, return; } + // std::init / union_marker (paper §6.5): the initialization profile bans the + // marker on a union object or a union member, because delayed initialization + // by assigning a member would be an erroneous assignment when compiled + // without the profile. Unlike the cases above this is profile policy rather + // than a meaningless subject, so it is gated on enforcement (and a union may + // still legitimately carry the marker when the profile is not enforced). + bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); + bool UnionMember = + isa(D) && cast(D)->getParent()->isUnion(); + if ((UnionVar || UnionMember) && + S.shouldEmitProfileViolation("std::init", "union_marker", AL.getLoc())) { + S.Diag(AL.getLoc(), diag::err_init_union_marker) + << "std::init" << (UnionMember ? 1 : 0); + AL.setInvalid(); + return; + } + D->addAttr(::new (S.Context) CXX11UninitializedAttr(S.Context, AL)); } diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp new file mode 100644 index 0000000000000..0452048caa804 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -0,0 +1,33 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +union U { int x; float y; }; + +U g_union [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a variable of union type under profile 'std::init'}} + +void test_union_var() { + U a [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a variable of union type under profile 'std::init'}} + (void)a; +} + +void test_union_var_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] U a [[uninitialized]]; + (void)a; +} + +union MarkedMember { + int x [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a union member under profile 'std::init'}} + float y; +}; + +// A marker on a union member of a non-enforced profile is silently accepted; +// exercised by the no-profiles run above. + +// A non-union class member may carry the marker (it is not banned here). +struct NotUnion { + int x [[uninitialized]]; +}; From f6717c9e8d0585e33047f6b651fc2486c069d257 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 16:44:23 -0400 Subject: [PATCH 078/289] Apply uninit_with_initializer to data members with an NSDMI A non-static data member that is both [[uninitialized]] and has a default member initializer is the same contradiction already diagnosed for variables. Factor the check into a shared Sema helper and call it from the NSDMI completion path so `int m [[uninitialized]] = 0;` is diagnosed. --- clang/include/clang/Sema/Sema.h | 10 ++++++ clang/lib/Sema/SemaDecl.cpp | 33 +++++++++++-------- clang/lib/Sema/SemaDeclCXX.cpp | 4 +++ .../safety-profile-init-field-marker.cpp | 6 ++-- 4 files changed, 37 insertions(+), 16 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 5368110c810d6..fe076058d17e7 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1081,6 +1081,16 @@ class Sema final : public SemaBase { bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); + /// std::init / uninit_with_initializer (R4): diagnose an entity that is both + /// marked [[uninitialized]] and has an initializer. Shared by the variable + /// (\c CheckCompleteVariableDeclaration) and non-static data member + /// (\c ActOnFinishCXXInClassMemberInitializer) paths. \p Init is the + /// (possibly null) initializer; a RecoveryExpr placeholder for a failed + /// initialization does not count as a user-written initializer. + void checkInitProfileUninitWithInitializer(SourceLocation Loc, + DeclarationName Name, + const Expr *Init, bool HasMarker); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 4f8045e87ccc0..e9003aa082adc 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14832,22 +14832,29 @@ void Sema::addLifetimeBoundToImplicitThis(CXXMethodDecl *MD) { MD->setTypeSourceInfo(TLB.getTypeSourceInfo(Context, AttributedType)); } +void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, + DeclarationName Name, + const Expr *Init, + bool HasMarker) { + // [[uninitialized]] documents that the entity is intentionally left + // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr + // is a placeholder for an initialization that already failed (e.g. + // default-init of a const scalar), not an initializer the user wrote, so it + // must not trigger this rule. + if (!HasMarker || !Init || isa(Init->IgnoreParens())) + return; + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_with_initializer"; + if (shouldEmitProfileViolation(Profile, Rule, Loc)) + Diag(Loc, diag::err_init_uninit_with_initializer) << Profile << Name; +} + void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; - // std::init / uninit_with_initializer: [[uninitialized]] documents that - // the variable is intentionally left uninitialized, so it contradicts an - // explicit initializer. A RecoveryExpr is a placeholder for an - // initialization that already failed (e.g. default-init of a const scalar), - // not an initializer the user wrote, so it must not trigger this rule. - if (var->hasInit() && !isa(var->getInit()->IgnoreParens()) && - var->hasAttr()) { - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "uninit_with_initializer"; - if (shouldEmitProfileViolation(Profile, Rule, var->getLocation())) - Diag(var->getLocation(), diag::err_init_uninit_with_initializer) - << Profile << var->getDeclName(); - } + checkInitProfileUninitWithInitializer( + var->getLocation(), var->getDeclName(), var->getInit(), + var->hasAttr()); CUDA().MaybeAddConstantAttr(var); diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index b2796dbc93118..2a3126490ad0e 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4225,6 +4225,10 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, } FD->setInClassInitializer(InitExpr.get()); + + checkInitProfileUninitWithInitializer(FD->getLocation(), FD->getDeclName(), + FD->getInClassInitializer(), + FD->hasAttr()); } /// Find the direct and/or virtual base specifiers that diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index b57b5d6879ff1..64f904ee9b3e2 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -13,11 +13,11 @@ struct PlainFieldPrefix { }; struct FieldWithNSDMI { - int m [[uninitialized]] = 0; + int m [[uninitialized]] = 0; // expected-error {{variable 'm' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} }; struct FieldWithNSDMIPrefix { - [[uninitialized]] int m = 0; + [[uninitialized]] int m = 0; // expected-error {{variable 'm' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} }; struct WithStaticDataMember { @@ -30,7 +30,7 @@ int WithStaticDataMember::t; struct MultipleFields { int a [[uninitialized]]; int b = 0; - int c [[uninitialized]] = 0; + int c [[uninitialized]] = 0; // expected-error {{variable 'c' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} }; template From 75884185b1009e2744ac8d005a1325d6328d1438 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 17:25:19 -0400 Subject: [PATCH 079/289] Diagnose default-init of aggregates that leave members indeterminate Extend the std::init uninit_decl rule to a variable whose default-initialization leaves a scalar subobject indeterminate (e.g. struct S { int x; }; S s;), trusting user-provided default constructors. The shared indeterminacy predicate also refines the marker/initializer check so the marker is accepted on a trivial aggregate while still rejected on an explicit initializer or a constructor that runs. --- clang/include/clang/Sema/Sema.h | 9 ++ clang/lib/Sema/SemaDecl.cpp | 84 ++++++++++++++++--- clang/lib/Sema/SemaDeclCXX.cpp | 1 + .../SemaCXX/safety-profile-init-aggregate.cpp | 47 +++++++++++ .../test/SemaCXX/safety-profile-init-decl.cpp | 9 +- 5 files changed, 136 insertions(+), 14 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-init-aggregate.cpp diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index fe076058d17e7..a00423b4ddeb7 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1089,8 +1089,17 @@ class Sema final : public SemaBase { /// initialization does not count as a user-written initializer. void checkInitProfileUninitWithInitializer(SourceLocation Loc, DeclarationName Name, + QualType DeclType, const Expr *Init, bool HasMarker); + /// True if default-initialization of \p T would leave at least one scalar + /// subobject with an indeterminate value. Shared by the std::init rules + /// uninit_decl (R5, at the variable declaration) and ctor_uninit_member + /// (R6, for a class-typed member). A class with a user-provided default + /// constructor is trusted (that constructor is checked by R6 at its own + /// definition). Dependent and incomplete types are treated as determinate. + bool defaultInitLeavesScalarIndeterminate(QualType T); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index e9003aa082adc..2ec085eec82f9 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14719,14 +14719,24 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { // attempted default-initialization) must either carry [[uninitialized]] or // be initialized by a language rule. Static / thread storage duration is // excluded -- those are zero-initialized; runtime-init concerns are R3's. - if (!Var->isInvalidDecl() && !Var->getInit() && + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_decl"; + // The enforcement check gates the (possibly recursive) type walk below so + // it runs only under the profile, not on every default-initialized + // variable. + if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && - !Var->hasAttr()) { - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "uninit_decl"; - if (shouldEmitProfileViolation(Profile, Rule, Var->getLocation())) - Diag(Var->getLocation(), diag::err_init_uninit_decl) - << Profile << Var->getDeclName(); + !Var->hasAttr() && + shouldEmitProfileViolation(Profile, Rule, Var->getLocation()) && + // A definition with no initializer (scalar / pointer / enum, or an + // array of them), or a class/aggregate type whose default-init leaves + // a scalar subobject indeterminate (its synthesized constructor call + // provides an initializer, so the !getInit() test alone misses it). + (!Var->getInit() || + (Var->getType()->isRecordType() && + defaultInitLeavesScalarIndeterminate(Var->getType())))) { + Diag(Var->getLocation(), diag::err_init_uninit_decl) + << Profile << Var->getDeclName(); } CheckCompleteVariableDeclaration(Var); @@ -14832,8 +14842,50 @@ void Sema::addLifetimeBoundToImplicitThis(CXXMethodDecl *MD) { MD->setTypeSourceInfo(TLB.getTypeSourceInfo(Context, AttributedType)); } +static bool defaultInitLeavesScalarIndeterminateImpl( + ASTContext &Ctx, QualType T, + llvm::SmallPtrSetImpl &Visited) { + if (T->isDependentType() || T->isIncompleteType()) + return false; + if (const ArrayType *AT = Ctx.getAsArrayType(T)) + return defaultInitLeavesScalarIndeterminateImpl(Ctx, AT->getElementType(), + Visited); + if (T->isReferenceType()) + return false; + const auto *RD = T->getAsCXXRecordDecl(); + if (!RD) + // Scalars, pointers, and enums are left indeterminate by default-init. + return T->isScalarType(); + // A union is handled by the union_marker rule, not here; its members are + // mutually exclusive so the member walk below does not apply. + if (RD->isUnion() || RD->isInvalidDecl()) + return false; + // Break cycles from ill-formed self-containing types (e.g. struct S { S x; }). + if (!Visited.insert(RD->getCanonicalDecl()).second) + return false; + // Trust a user-provided default constructor: R6 checks at its definition. + if (RD->hasUserProvidedDefaultConstructor()) + return false; + for (const CXXBaseSpecifier &Base : RD->bases()) + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, Base.getType(), Visited)) + return true; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField() || F->hasInClassInitializer()) + continue; + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), Visited)) + return true; + } + return false; +} + +bool Sema::defaultInitLeavesScalarIndeterminate(QualType T) { + llvm::SmallPtrSet Visited; + return defaultInitLeavesScalarIndeterminateImpl(Context, T, Visited); +} + void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, DeclarationName Name, + QualType DeclType, const Expr *Init, bool HasMarker) { // [[uninitialized]] documents that the entity is intentionally left @@ -14845,15 +14897,27 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, return; static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "uninit_with_initializer"; - if (shouldEmitProfileViolation(Profile, Rule, Loc)) - Diag(Loc, diag::err_init_uninit_with_initializer) << Profile << Name; + // Gate the (possibly recursive) type walk below on enforcement. + if (!shouldEmitProfileViolation(Profile, Rule, Loc)) + return; + // A synthesized default-initialization that leaves the object indeterminate + // (a trivial or aggregate type, no user-written initializer) is consistent + // with the marker: the object really is left uninitialized, to be + // initialized later (e.g. via construct_at), mirroring the scalar case. Only + // a real initializer -- an explicit one, or a constructor that actually runs + // -- contradicts the marker. + if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) + if (CCE->getConstructor()->isDefaultConstructor() && + defaultInitLeavesScalarIndeterminate(DeclType)) + return; + Diag(Loc, diag::err_init_uninit_with_initializer) << Profile << Name; } void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; checkInitProfileUninitWithInitializer( - var->getLocation(), var->getDeclName(), var->getInit(), + var->getLocation(), var->getDeclName(), var->getType(), var->getInit(), var->hasAttr()); CUDA().MaybeAddConstantAttr(var); diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 2a3126490ad0e..43780febe7688 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4227,6 +4227,7 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, FD->setInClassInitializer(InitExpr.get()); checkInitProfileUninitWithInitializer(FD->getLocation(), FD->getDeclName(), + FD->getType(), FD->getInClassInitializer(), FD->hasAttr()); } diff --git a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp new file mode 100644 index 0000000000000..25c9149ae7f0f --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp @@ -0,0 +1,47 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +struct Trivial { int x; }; +struct AllInit { int x = 0; }; +struct WithCtor { WithCtor(); int x; }; +struct PartlyInit { int x; struct Inner { Inner(); } s; }; +struct Nested { Trivial t; }; +struct WithBase : Trivial {}; + +void test_aggregate() { + Trivial a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Trivial b = {1}; + Trivial c = {}; + Trivial d{}; + Trivial e [[uninitialized]]; + (void)a; (void)b; (void)c; (void)d; (void)e; +} + +void test_nested_and_base() { + PartlyInit a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Nested b; // expected-error {{variable 'b' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + WithBase c; // expected-error {{variable 'c' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + (void)a; (void)b; (void)c; +} + +void test_trusted() { + // A type with a user-provided default constructor is trusted (the + // constructor is checked separately), and a non-static data member + // initializer covers the member. + WithCtor a; + AllInit b; + (void)a; (void)b; +} + +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_decl")]] Trivial a; + (void)a; +} + +// Non-local aggregates are zero-initialized at static-init time, so they are +// not diagnosed. +Trivial g_trivial; diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index 24fd46b62bd8e..844e4ec4c7de4 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -43,10 +43,11 @@ void test_class_with_user_ctor() { } void test_class_trivial() { - // Conservative: trivial / aggregate class types are not diagnosed by R2 in - // this minimal slice. Field-level initialization tracking is the §6 - // "classes without constructors" work and is explicitly deferred. - Trivial t; + // A trivial / aggregate class whose default-initialization leaves a scalar + // member indeterminate is diagnosed by R5 (uninit_decl), the §6 + // "classes without constructors" rule. (Detailed coverage lives in + // safety-profile-init-aggregate.cpp.) + Trivial t; // expected-error {{variable 't' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} (void)t; } From 399cfda834ade08f2d2bd1904078e4deef45634c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 17:54:25 -0400 Subject: [PATCH 080/289] Add a constructor-finalization profile dispatch Class-finalization callbacks run before any constructor body is parsed, so they cannot see a constructor's member-initializer list. Add a parallel dispatch keyed off the point where a constructor's inits() are complete (ActOnMemInitializers / ActOnDefaultCtorInitializers, which also serve template instantiations), filtering out dependent, invalid, and delegating constructors. Ship the test-only test::ctor_final profile and a test that exercises written, no-list, out-of-line, and instantiated constructors, the delegating and defaulted skips, and suppression. --- .../clang/Basic/DiagnosticSemaKinds.td | 3 + clang/include/clang/Sema/Sema.h | 10 +++ clang/lib/Sema/SemaDeclCXX.cpp | 44 +++++++++++ .../SemaCXX/safety-profile-ctor-final.cpp | 75 +++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-ctor-final.cpp diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 7838c99cce63d..857726632cbaf 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14140,6 +14140,9 @@ def err_profile_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; def err_profile_class_final_test : ProfileRuleError< "test profile fired on completion of class %1 under profile '%0'">; +def err_profile_ctor_final_test : ProfileRuleError< + "test profile fired on finalization of a constructor for class %1 under " + "profile '%0'">; def err_init_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; def err_init_uninit_decl : ProfileRuleError< diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index a00423b4ddeb7..0ae71fd1d6b49 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -6060,6 +6060,16 @@ class Sema final : public SemaBase { /// invalid, and lambda classes are filtered out. void checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD); + /// Dispatch constructor-finalization profile callbacks once a constructor's + /// member-initializer list is complete. Called from \c ActOnMemInitializers + /// and \c ActOnDefaultCtorInitializers, which also serve template + /// instantiations (via \c InstantiateMemInitializers), so every + /// user-defined constructor is covered at the point its \c inits() is fully + /// populated -- unlike class finalization, which runs before any + /// constructor body is parsed. Dependent, invalid, and delegating + /// constructors are filtered out. + void checkProfileViolationsAtConstructorFinalization(CXXConstructorDecl *Ctor); + /// Check that the C++ class annoated with "trivial_abi" satisfies all the /// conditions that are needed for the attribute to have an effect. void checkIllFormedTrivialABIStruct(CXXRecordDecl &RD); diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 43780febe7688..47fa9100e2131 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -5924,6 +5924,8 @@ void Sema::ActOnMemInitializers(Decl *ConstructorDecl, SetCtorInitializers(Constructor, AnyErrors, MemInits); DiagnoseUninitializedFields(*this, Constructor); + + checkProfileViolationsAtConstructorFinalization(Constructor); } void Sema::MarkBaseAndMemberDestructorsReferenced(SourceLocation Location, @@ -5990,6 +5992,7 @@ void Sema::ActOnDefaultCtorInitializers(Decl *CDtorDecl) { } SetCtorInitializers(Constructor, /*AnyErrors=*/false); DiagnoseUninitializedFields(*this, Constructor); + checkProfileViolationsAtConstructorFinalization(Constructor); } } @@ -7049,6 +7052,30 @@ void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { constexpr ClassFinalizationProfileEntry ClassFinalizationProfiles[] = { {"test::class_final", &runTestClassFinalCallback}, }; + +// Profiles that opt into the constructor-finalization dispatch. Each entry +// pairs the profile name with a callback invoked once per fully-processed, +// non-dependent, non-invalid, non-delegating constructor (the point at which +// its member-initializer list, including synthesized entries, is complete). +// Adding a new profile is a single row here plus a ProfileRuleError diagnostic +// and a callback that consults Sema::shouldEmitProfileViolation. +struct ConstructorFinalizationProfileEntry { + StringRef Name; + void (*Callback)(Sema &, CXXConstructorDecl *); +}; + +void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { + if (!S.shouldEmitProfileViolation("test::ctor_final", /*Rule=*/"", + Ctor->getLocation())) + return; + S.Diag(Ctor->getLocation(), diag::err_profile_ctor_final_test) + << "test::ctor_final" << Ctor->getParent(); +} + +constexpr ConstructorFinalizationProfileEntry + ConstructorFinalizationProfiles[] = { + {"test::ctor_final", &runTestCtorFinalCallback}, +}; } // namespace void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { @@ -7490,6 +7517,23 @@ void Sema::checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD) { } } +void Sema::checkProfileViolationsAtConstructorFinalization( + CXXConstructorDecl *Ctor) { + if (!getLangOpts().Profiles || !Ctor) + return; + // A dependent constructor pattern re-fires on instantiation; a delegating + // constructor leaves member initialization to its target. + if (Ctor->isInvalidDecl() || Ctor->isDependentContext() || + Ctor->isDelegatingConstructor()) + return; + ProfileSuppressScope Scope(*this, Ctor, /*WalkLexicalParents=*/true); + for (const auto &E : ConstructorFinalizationProfiles) { + if (!isProfileEnforced(E.Name)) + continue; + E.Callback(*this, Ctor); + } +} + /// Look up the special member function that would be called by a special /// member function for a subobject of class type. /// diff --git a/clang/test/SemaCXX/safety-profile-ctor-final.cpp b/clang/test/SemaCXX/safety-profile-ctor-final.cpp new file mode 100644 index 0000000000000..2e957eb8fcfec --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-ctor-final.cpp @@ -0,0 +1,75 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(test::ctor_final)]]; + +// A constructor with a member-initializer list fires once, at finalization. +struct Written { + int x; + Written() : x(0) {} // expected-error {{test profile fired on finalization of a constructor for class 'Written' under profile 'test::ctor_final'}} +}; + +// A constructor with no member-initializer list reaches the same dispatch. +struct NoList { + int x; + NoList() { x = 0; } // expected-error {{test profile fired on finalization of a constructor for class 'NoList' under profile 'test::ctor_final'}} +}; + +// Out-of-line definitions fire at the definition, not the declaration. +struct OutOfLine { + OutOfLine(); +}; +OutOfLine::OutOfLine() {} // expected-error {{test profile fired on finalization of a constructor for class 'OutOfLine' under profile 'test::ctor_final'}} + +// A defaulted constructor has no body that reaches the dispatch. +struct Defaulted { + int x; + Defaulted() = default; +}; + +// A class with no user-declared constructor never reaches the dispatch. +struct NoCtor { + int x; +}; + +// A delegating constructor leaves member initialization to its target, so it +// is skipped; the target constructor fires. +struct Delegating { + int x; + Delegating() : Delegating(0) {} + Delegating(int v) : x(v) {} // expected-error {{test profile fired on finalization of a constructor for class 'Delegating' under profile 'test::ctor_final'}} +}; + +// A dependent constructor pattern is skipped; the diagnostic fires on the +// instantiation. +template +struct Tmpl { + T x; + Tmpl() : x() {} // expected-error {{test profile fired on finalization of a constructor for class 'Tmpl' under profile 'test::ctor_final'}} +}; +template struct Tmpl; // expected-note {{in instantiation of member function 'Tmpl::Tmpl' requested here}} + +// Suppression on the constructor. +struct SuppressedCtor { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::ctor_final)]] SuppressedCtor() {} +}; + +// Suppression on the enclosing class. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::ctor_final)]] SuppressedClass { + SuppressedClass() {} +}; + +// Rule-targeted suppression (the profile has a single implicit rule). +struct SuppressedByRule { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::ctor_final, rule: "")]] SuppressedByRule() {} +}; + +// A non-matching suppress does not silence the diagnostic. +struct WrongSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::other)]] WrongSuppress() {} // expected-error {{test profile fired on finalization of a constructor for class 'WrongSuppress' under profile 'test::ctor_final'}} +}; From 5e74d3c911549d04081c00c3e5f5b1596b070bc0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 18:12:52 -0400 Subject: [PATCH 081/289] Require constructors to initialize every member under std::init Register std::init on the constructor-finalization dispatch: a user-provided constructor must initialize each non-static data member via its member-initializer list or an NSDMI, or the member must be marked [[uninitialized]]. A plain body assignment does not count, and a member whose own default-initialization leaves a scalar subobject indeterminate (a nested aggregate) is flagged too. Reference and const members keep their existing diagnostics. --- .../clang/Basic/DiagnosticSemaKinds.td | 4 + clang/lib/Sema/SemaDeclCXX.cpp | 38 ++++++++ .../test/SemaCXX/safety-profile-init-ctor.cpp | 89 +++++++++++++++++++ 3 files changed, 131 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-init-ctor.cpp diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 857726632cbaf..71808a28c8bc0 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14160,4 +14160,8 @@ def err_uninitialized_attr_invalid_subject : Error< def err_init_union_marker : ProfileRuleError< "'[[uninitialized]]' cannot be applied to %select{a variable of union type|" "a union member}1 under profile '%0'">; +def err_init_ctor_uninit_member : ProfileRuleError< + "constructor does not initialize member %1 under profile '%0'">; +def note_init_uninit_member_here : Note< + "member %0 declared here">; } // end of sema component. diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 47fa9100e2131..301dd8a241234 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7072,9 +7072,47 @@ void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { << "test::ctor_final" << Ctor->getParent(); } +void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { + // Paper §6.1: a user-provided constructor must initialize every member via + // its member-initializer list or an NSDMI, unless the member is marked + // [[uninitialized]] (whose body initialization is the deferred R7 check). + // A plain assignment in the constructor body does not count. + if (!Ctor->isUserProvided()) + return; + + // Members given a written member-initializer by this constructor. + llvm::SmallPtrSet Written; + for (const CXXCtorInitializer *Init : Ctor->inits()) + if (Init->isWritten() && Init->isAnyMemberInitializer()) + if (const FieldDecl *F = Init->getAnyMember()) + Written.insert(F); + + for (const FieldDecl *F : Ctor->getParent()->fields()) { + // Anonymous aggregate members and bit-fields are conservatively skipped in + // this slice; reference and const members already have dedicated + // diagnostics when left uninitialized. + if (F->isUnnamedBitField() || !F->getDeclName() || + F->getType()->isReferenceType() || F->getType().isConstQualified()) + continue; + if (F->hasAttr() || F->hasInClassInitializer() || + Written.count(F)) + continue; + if (!S.defaultInitLeavesScalarIndeterminate(F->getType())) + continue; + if (!S.shouldEmitProfileViolation("std::init", "ctor_uninit_member", + Ctor->getLocation())) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) + << "std::init" << F->getDeclName(); + S.Diag(F->getLocation(), diag::note_init_uninit_member_here) + << F->getDeclName(); + } +} + constexpr ConstructorFinalizationProfileEntry ConstructorFinalizationProfiles[] = { {"test::ctor_final", &runTestCtorFinalCallback}, + {"std::init", &runStdInitCtorUninitMemberCallback}, }; } // namespace diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp new file mode 100644 index 0000000000000..6ccb24e74a1d5 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -0,0 +1,89 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +struct WithCtor { WithCtor(); }; +struct Inner { int y; }; + +struct MissingMember { + int x; // expected-note {{member 'x' declared here}} + MissingMember() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; + +struct MemInit { + int x; + MemInit() : x(0) {} +}; + +struct DefaultMemberInit { + int x = 0; + DefaultMemberInit() {} +}; + +struct Marked { + int x [[uninitialized]]; + Marked() {} +}; + +struct BodyAssignment { + int x; // expected-note {{member 'x' declared here}} + // A plain body assignment is not initialization for this rule. + BodyAssignment() { x = 0; } // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; + +struct NestedAggregate { + Inner m; // expected-note {{member 'm' declared here}} + NestedAggregate() {} // expected-error {{constructor does not initialize member 'm' under profile 'std::init'}} +}; + +struct TrustedMemberCtor { + WithCtor m; + TrustedMemberCtor() {} +}; + +struct PartialInit { + int x; + int y; // expected-note {{member 'y' declared here}} + PartialInit() : x(0) {} // expected-error {{constructor does not initialize member 'y' under profile 'std::init'}} +}; + +struct OutOfLine { + int x; // expected-note {{member 'x' declared here}} + OutOfLine(); +}; +OutOfLine::OutOfLine() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} + +template +struct Tmpl { + T x; // expected-note {{member 'x' declared here}} + Tmpl() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; +template struct Tmpl; // expected-note {{in instantiation of member function 'Tmpl::Tmpl' requested here}} + +// A delegating constructor relies on its target, which does initialize the +// member, so nothing fires. +struct Delegating { + int x; + Delegating() : Delegating(0) {} + Delegating(int v) : x(v) {} +}; + +struct SuppressedCtor { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] SuppressedCtor() {} +}; + +struct SuppressedByRule { + int x; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] SuppressedByRule() {} +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedClass { + int x; + SuppressedClass() {} +}; From 1c39bab1d50dc8a49d7ef717eca022e046452860 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 2 Jun 2026 18:22:41 -0400 Subject: [PATCH 082/289] Document the constructor-finalization pattern and new std::init rules Add a "Pattern 4" section for the constructor-finalization dispatch and its test::ctor_final example, tighten the class-finalization section to state it is for structural checks only, and document the std::init rules for aggregates, constructor member coverage, and unions. Update the [[uninitialized]] attribute reference, and note that the constructor-body flow check, [[ref_to_uninit]], and the array and class-exposing-memory rules remain future work. --- clang/docs/ProfilesFramework.rst | 239 ++++++++++++++++++++------ clang/include/clang/Basic/AttrDocs.td | 23 ++- 2 files changed, 198 insertions(+), 64 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 5973c0c7e6f2c..cb8dfc75d39d3 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -312,12 +312,53 @@ This pattern needs two pieces, both colocated with the dispatcher. The diagnostic itself is defined with ``ProfileRuleError`` as in patterns 1 and 2. -Unlike the post-parse CFG pass, class-finalization callbacks run while the -``CXXRecordDecl`` is being finalized (immediately before -``CheckCompletedCXXClass`` returns). Out-of-line member definitions -- -including constructor bodies -- have not yet been parsed when the callback -runs. Rules that need ctor-body flow analysis must therefore live in a -post-parse CFG pass (pattern 2), not here. +Class-finalization is for **structural** rules -- those answerable from the +class's declared members, their types, and their attributes. The callbacks +run while the ``CXXRecordDecl`` is being finalized (immediately before +``CheckCompletedCXXClass`` returns), which is *before any constructor body or +member-initializer list has been parsed* -- inline member bodies are +late-parsed afterward, and out-of-line and template member constructors later +still. A class-finalization callback therefore must not inspect a +constructor's ``inits()`` (they are empty here). Rules that depend on what a +constructor initializes belong on the constructor-finalization dispatch +(pattern 4); rules that need whole-function flow analysis belong on a +post-parse CFG pass (pattern 2). + + +Pattern 4: Constructor-Finalization Profile +------------------------------------------- + +Used when the rule applies to a single constructor and needs that +constructor's complete member-initializer list -- for example, "every member +must be initialized by this constructor." ``test::ctor_final`` is the in-tree +example, and the ``std::init`` ``ctor_uninit_member`` rule is the real one. + +The dispatch point is ``Sema::checkProfileViolationsAtConstructorFinalization``, +called right after ``DiagnoseUninitializedFields`` in +``Sema::ActOnMemInitializers`` and ``Sema::ActOnDefaultCtorInitializers`` in +``clang/lib/Sema/SemaDeclCXX.cpp``. Those two functions are the funnel for +every user-defined constructor -- written or implicit member-initializer +list, inline or out-of-line -- and template instantiation reaches the first +of them through ``Sema::InstantiateMemInitializers``, so the hook sees every +constructor at the point its ``inits()`` (including synthesized entries) is +complete. + +The dispatcher filters out constructors the rules are not meant to see: + +- Dependent constructors (``isDependentContext()``). The hook re-fires on + each instantiation. +- Invalid constructors (``isInvalidDecl()``). +- Delegating constructors (``isDelegatingConstructor()``), which leave member + initialization to their target. + +The two pieces mirror pattern 3: a per-pass opt-in table +``ConstructorFinalizationProfiles`` (profile name plus a +``void (*)(Sema &, CXXConstructorDecl *)`` callback), and a callback that +emits via ``Sema::shouldEmitProfileViolation``. The dispatcher establishes a +``ProfileSuppressScope(*this, Ctor, /*WalkLexicalParents=*/true)`` around each +callback, so ``[[profiles::suppress]]`` on the constructor, the class, or an +enclosing lexical ``Decl`` works. A callback that should only apply to +user-written constructors checks ``Ctor->isUserProvided()``. .. _profiles-token-dominion: @@ -427,23 +468,25 @@ The following parts of P3589R2 are deliberately not implemented: Built-in Profiles ================= -The tree ships four built-in profiles, all gated on ``-fprofiles``: +The tree ships five built-in profiles, all gated on ``-fprofiles``: - ``test::type_cast`` (test-only) -- pattern-1 example. - ``test::uninit_read`` (test-only) -- pattern-2 example riding the existing CFG uninitialized-variables analysis. - ``test::class_final`` (test-only) -- pattern-3 example riding the class-finalization dispatch. +- ``test::ctor_final`` (test-only) -- pattern-4 example riding the + constructor-finalization dispatch. - ``std::init`` (initial slice of the proposed initialization profile from - Stroustrup's draft, on top of P3589R2 and P3402R3). It rides the same - CFG dispatch as ``test::uninit_read`` for one of its rules and adds three - parse-time (pattern-1) rules. + Stroustrup's draft, on top of P3589R2 and P3402R3). It uses all four + patterns: the CFG dispatch (with ``test::uninit_read``), the + constructor-finalization dispatch, and several parse-time check sites. By convention: - Real test profiles live under the ``test::`` namespace. Today there are - three: ``test::type_cast``, ``test::uninit_read``, and - ``test::class_final``. + four: ``test::type_cast``, ``test::uninit_read``, ``test::class_final``, + and ``test::ctor_final``. - The names ``test::other``, ``test::bounds``, ``test::new_profile``, and ``test::not_enforced`` are deliberately *not* implemented and appear only in negative tests as stand-in "some other profile" names. Adding a real @@ -522,16 +565,37 @@ enclosing lexical ``Decl`` silences the diagnostic via the dispatcher establishes around each callback. +The ``test::ctor_final`` Profile +-------------------------------- + +A pattern-4 (constructor-finalization) profile. Demonstrates the case where +the rule applies once per user-defined constructor, after its +member-initializer list is complete. + +- **Rules**: none (single implicit rule, empty rule string). +- **Diagnostic**: ``err_profile_ctor_final_test`` ("test profile fired on + finalization of a constructor for class %1 under profile '%0'"). +- **Opt-in table**: ``ConstructorFinalizationProfiles`` in + ``clang/lib/Sema/SemaDeclCXX.cpp``. + +The diagnostic fires once per user-defined constructor -- written or implicit +member-initializer list, inline or out-of-line -- and on constructor template +*instantiations* rather than the dependent pattern. Defaulted and implicit +constructors (no body) and delegating constructors are skipped. + + The ``std::init`` Profile (initial slice) ----------------------------------------- -A first slice of the proposed initialization profile, intentionally minimal: -no ``[[ref_to_uninit]]``, no field-level marker, no new dataflow analyses. -The class-finalization dispatch (pattern 3) now exists in the framework but -``std::init`` does not yet register any class-finalization callbacks; the -paper §6 / §6.2 class rules are deferred to later stages. +A slice of the proposed initialization profile. It does not yet implement +``[[ref_to_uninit]]`` (paper §5), classes that expose uninitialized memory to +users (paper §6.2), or random-access initialization of uninitialized arrays +(paper §6.4); and the constructor-body flow check that would let a +``[[uninitialized]]`` member be initialized by assignment in the body (the +dynamic half of paper §6.1) is deferred to a future CFG-based pass. Until it +lands, a ``[[uninitialized]]`` data member is trusted. -The slice introduces one new attribute and four rules. +The slice introduces one new attribute and the rules below. Marker attribute ~~~~~~~~~~~~~~~~ @@ -539,23 +603,27 @@ Marker attribute ``[[uninitialized]]`` (a standard C++11 attribute, distinct from the Clang vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` or ``FieldDecl`` as intentionally left uninitialized. Recognised by Clang -regardless of ``-fprofiles``; only carries semantic weight when +regardless of ``-fprofiles``; its profile rules carry weight only when ``std::init`` is enforced. -- TableGen def: ``CXX11Uninitialized`` in ``clang/include/clang/Basic/Attr.td``. -- Subjects: ``Var`` and ``Field``. Field-level placement is accepted but - currently inert; rules that consume the field marker (paper §6 - aggregate-without-ctor, §6.1 ctor-body member-init, classes-exposing- - uninitialized-memory) are deferred to later stages. +- TableGen def: ``CXX11Uninitialized`` in ``clang/include/clang/Basic/Attr.td``, + with a custom handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. +- Subjects: ``Var`` and ``Field``. The handler rejects placement where the + marker is meaningless -- a reference, a function parameter, or a structured + binding -- regardless of ``-fprofiles``. - Behaviour: - - Suppresses R2 (``uninit_decl``) on the marked declaration. - - Does **not** suppress R1 (``uninit_read``). Per the paper, the marker - excuses the declaration but a read before any subsequent assignment is - still ill-formed. - - Triggers R4 (``uninit_with_initializer``) when combined with an - initializer, including a language-synthesized one (e.g., a class-typed - variable whose default constructor would run). + - Suppresses ``uninit_decl`` on the marked declaration (scalar or aggregate). + - Excuses a non-static data member from ``ctor_uninit_member``. + - Does **not** suppress ``uninit_read``. Per the paper, the marker excuses + the declaration but a read before any subsequent assignment is still + ill-formed. + - Triggers ``uninit_with_initializer`` when combined with an initializer, + including a language-synthesized one from a constructor that actually runs + (e.g. ``WithCtor x [[uninitialized]];``). A trivial/aggregate type whose + default-initialization is a no-op is *not* such an initializer, so the + marker is accepted there (the object is genuinely left uninitialized). + - Is banned on a union object or union member by ``union_marker``. Rules ~~~~~ @@ -581,18 +649,25 @@ and surface the ``std::init`` diagnostic. R2. ``uninit_decl`` -- pattern 1 ................................. -A definition that, after attempted default-initialization, has no -initializer expression must either carry ``[[uninitialized]]`` or have -static / thread storage duration (zero-initialized by language rule). +An automatic-storage variable definition whose default-initialization +leaves it (or a scalar subobject) indeterminate must either carry +``[[uninitialized]]`` or be initialized. This covers a scalar / pointer / +enum with no initializer, and -- per paper §6 ("classes without +constructors") -- an aggregate or trivially-default-constructible class type +whose default-initialization leaves a scalar subobject indeterminate (e.g. +``struct S { int x; }; S s;``). A class type with a user-provided default +constructor is trusted; static / thread storage duration is excluded +(zero-initialized by language rule). - Diagnostic: ``err_init_uninit_decl``. -- Check site: end of ``Sema::ActOnUninitializedDecl`` in - ``clang/lib/Sema/SemaDecl.cpp``. -- Conservative classification: a class type whose default constructor - produces an initializer expression (``Var->getInit()`` non-null after - ``InitSeq.Perform``) is trusted. Aggregates / POD class types whose - default-init leaves members indeterminate are *not* diagnosed in this - slice; that is paper §6 ("classes without constructors") work, deferred. +- Check site: ``Sema::ActOnUninitializedDecl`` in + ``clang/lib/Sema/SemaDecl.cpp``, which is only reached for declarations + with no initializer (so braced or value initialization such as + ``S s = {1};`` and ``S s{};`` is unaffected -- omitted aggregate members + are value-initialized). +- The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate``, + which recurses through bases and members, trusts user-provided default + constructors, and excludes unions. R3. ``static_runtime_init`` -- pattern 1 ......................................... @@ -615,16 +690,50 @@ R4. ``uninit_with_initializer`` -- pattern 1 contradiction (the marker means "no initialization here"). - Diagnostic: ``err_init_uninit_with_initializer``. -- Check site: top of ``Sema::CheckCompleteVariableDeclaration``. -- Note: also fires when the initializer is language-synthesized (e.g., - ``WithCtor x [[uninitialized]];`` -- the implicit default-constructor - call counts as an initializer). Combining the marker with class types - that have a default constructor is therefore ill-formed. +- Check site: ``Sema::checkInitProfileUninitWithInitializer``, shared by + ``Sema::CheckCompleteVariableDeclaration`` (variables) and + ``Sema::ActOnFinishCXXInClassMemberInitializer`` (data members with a + default member initializer). +- A ``RecoveryExpr`` placeholder (from a failed initialization) is not a + user-written initializer and does not trigger the rule. +- The "initializer" includes a language-synthesized one from a constructor + that actually runs (e.g. ``WithCtor x [[uninitialized]];``), but *not* a + no-op trivial/aggregate default-initialization, where the marker is + consistent with the object being left uninitialized. + +R5. ``ctor_uninit_member`` -- pattern 4 +....................................... + +A user-provided constructor must initialize every non-static data member +via its member-initializer list or an NSDMI, unless the member is marked +``[[uninitialized]]`` (paper §6.1). A plain assignment in the constructor +body does not count. A member whose own default-initialization leaves a +scalar subobject indeterminate (a nested aggregate) is flagged as well. + +- Diagnostic: ``err_init_ctor_uninit_member`` (with a + ``note_init_uninit_member_here`` note at the member). +- Opt-in table: ``ConstructorFinalizationProfiles`` (pattern 4). +- Reference and const members keep their existing dedicated diagnostics; + anonymous-aggregate members and bit-fields are conservatively skipped. + +R6. ``union_marker`` -- attribute handler +......................................... + +``[[uninitialized]]`` on a union object or a union member is banned (paper +§6.5): delayed initialization by assigning a member would be an erroneous +assignment when compiled without the profile. + +- Diagnostic: ``err_init_union_marker``. +- Check site: the ``CXX11Uninitialized`` handler in + ``clang/lib/Sema/SemaDeclAttr.cpp``. Unlike the reference / parameter / + structured-binding rejections, which are unconditional, this is gated on + enforcement -- a union may legitimately carry the marker without the + profile. Diagnostic suppression ~~~~~~~~~~~~~~~~~~~~~~ -All four rules are suppressible per-site with +Every rule is suppressible per-site with ``[[profiles::suppress(std::init)]]`` (covers all rules) or ``[[profiles::suppress(std::init, rule: "rule_name")]]`` (rule-targeted). The token-based-dominion limitation noted earlier applies: a suppress @@ -634,7 +743,7 @@ attribute on a ``VarDecl`` covers only that declaration's tokens. In-Tree Tests ============= -These tests collectively exercise the framework and the two built-in +These tests collectively exercise the framework and the built-in profiles. When changing the framework, run them all with ``check-clang-sema``, ``check-clang-parser``, and ``check-clang-pch``. @@ -673,20 +782,40 @@ profiles. When changing the framework, run them all with profile's ``uninit_read`` rule. Same ``-DCASE=N`` style as the ``test::uninit_read`` test; CASE=4 additionally enforces ``test::uninit_read`` to exercise the table-order priority. +- ``clang/test/SemaCXX/safety-profile-ctor-final.cpp`` -- the + ``test::ctor_final`` profile: end-to-end exercise of the + constructor-finalization dispatch (pattern 4) including written / + no-list / out-of-line / instantiated constructors, the delegating and + defaulted skips, suppression, and the without-``-fprofiles`` path. - ``clang/test/SemaCXX/safety-profile-init-decl.cpp`` -- the ``std::init`` - profile's ``uninit_decl`` rule (R2): scalars / pointers / enums require - an initializer or ``[[uninitialized]]``; statics / thread-locals are + profile's ``uninit_decl`` rule for scalars / pointers / enums: require an + initializer or ``[[uninitialized]]``; statics / thread-locals are excluded; class types with a user-provided default constructor are - trusted; trivial / aggregate types are conservatively *not* diagnosed - (deferred to §6 work). + trusted. +- ``clang/test/SemaCXX/safety-profile-init-aggregate.cpp`` -- the + ``uninit_decl`` rule for aggregates / trivially-default-constructible + class types whose default-init leaves a scalar subobject indeterminate + (paper §6); braced and value initialization are accepted. - ``clang/test/SemaCXX/safety-profile-init-static.cpp`` -- the ``std::init`` - profile's ``static_runtime_init`` rule (R3): non-local vars need a + profile's ``static_runtime_init`` rule: non-local vars need a constant initializer; locals / static-locals / thread-locals are excluded; ``constinit`` failures still produce the existing hard error regardless of ``-fprofiles``. - ``clang/test/SemaCXX/safety-profile-init-with-initializer.cpp`` -- the - ``std::init`` profile's ``uninit_with_initializer`` rule (R4): every + ``std::init`` profile's ``uninit_with_initializer`` rule: every combination of ``[[uninitialized]]`` placement (prefix / postfix) with - every initializer form (``= e``, ``{}``, ``(e)``). + every initializer form (``= e``, ``{}``, ``(e)``), plus the + synthesized-initializer and RecoveryExpr cases. +- ``clang/test/SemaCXX/safety-profile-init-field-marker.cpp`` -- placement + of ``[[uninitialized]]`` on data members, the marker / NSDMI + contradiction, and rejection on references, parameters, and structured + bindings. +- ``clang/test/SemaCXX/safety-profile-init-ctor.cpp`` -- the ``std::init`` + profile's ``ctor_uninit_member`` rule: member-initializer-list / NSDMI / + marker coverage, the nested-aggregate and body-assignment cases, + out-of-line and instantiated constructors, and suppression. +- ``clang/test/SemaCXX/safety-profile-init-union.cpp`` -- the ``std::init`` + profile's ``union_marker`` rule banning the marker on a union object or + union member. - ``clang/test/PCH/cxx-profiles-enforce.cpp`` -- ``[[profiles::enforce]]`` state survives PCH serialization round-trip. diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index d1144e9e09360..448e7563dff56 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -897,15 +897,20 @@ A declaration or non-static data member with both ``[[uninitialized]]`` and an initializer is ill-formed under the initialization profile (the marker contradicts the explicit initialization). -When placed on a non-static data member, the attribute is currently inert: -the parser accepts the placement but no diagnostic is tied to it yet. Its -semantic weight is added by the classes-without-constructors rule, the -class-exposes-uninitialized-memory rule, and the constructor-body -member-initialization rule, which are staged separately. - -Outside the initialization profile the attribute is silently accepted and -has no effect, so code that uses it remains valid when compiled without -``-fprofiles``. +On a non-static data member the marker also excuses the member from the +profile's requirement that every constructor initialize it. (The profile +does not yet verify that such a member is assigned in the constructor +body; until it does, a marked member is trusted.) + +The attribute cannot be applied where leaving the entity uninitialized is +meaningless -- a reference, a function parameter, or a structured binding -- +and these placements are diagnosed regardless of ``-fprofiles``. The +initialization profile additionally rejects the marker on a union object or +union member. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. This attribute is distinct from the Clang vendor attribute ``[[clang::uninitialized]]``, which interacts with From 0f64c23a676be4e5c0ba70f5d839a09d7efd3d3c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 3 Jun 2026 11:51:21 -0400 Subject: [PATCH 083/289] Inherit enforced profiles in partition implementation units --- clang/docs/ProfilesFramework.rst | 20 +++++++++-- clang/lib/Sema/SemaModule.cpp | 14 ++++++++ .../safety-profile-framework-modules.cppm | 33 +++++++++++++++++++ 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index cb8dfc75d39d3..568a455797173 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -429,11 +429,27 @@ Module Enforcement ------------------ ``[[profiles::enforce(...)]]`` on a module interface declaration records the -enforced profile designators on ``Module::EnforcedProfileDesignators``. Module -implementation units automatically inherit the interface's enforcements. +enforced profile designators on ``Module::EnforcedProfileDesignators``. A +(non-partition) module implementation unit ``module M;`` automatically inherits +the interface's enforcements, because it implicitly imports the primary +interface unit of ``M``. ``[[profiles::require(...)]]`` on an import-declaration validates that the imported module's ``EnforcedProfileDesignators`` contains a matching designator. +A module partition implementation unit ``module M:P;`` is also a module +implementation unit of ``M``, so the primary interface's enforcements apply to +it as well. However, it does **not** implicitly import the primary interface, +and the primary interface is normally compiled *after* its partitions, so its +BMI is usually not available when the partition implementation unit is compiled. +Inheritance here is therefore **best-effort**: the enforcements are inherited +only when the primary interface's BMI is already resident in the compilation +(for example, supplied via an eager ``-fmodule-file=``); the BMI is never +force-loaded and its absence is never diagnosed. When it is not available the +partition implementation unit is simply not subject to the inherited profile -- +a missed diagnostic, never a change to the meaning of a well-formed program. +For guaranteed enforcement, **repeat** ``[[profiles::enforce(...)]]`` in the +partition implementation unit rather than relying on inheritance. + Importing a module that enforces a profile does **not** enforce that profile in the importing translation unit. Enforcement is always explicit and local. diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index edb13336fb5b6..fc38213709ec9 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -486,6 +486,20 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, if (Interface) { for (const auto &EP : Interface->EnforcedProfileDesignators) addProfileEnforcement(EP.ProfileName, EP.Designator, ModuleLoc); + } else if (getLangOpts().Profiles && + MDK == ModuleDeclKind::PartitionImplementation) { + // A partition implementation unit is a module implementation unit of M, so + // the primary interface's enforced profiles apply to it. Unlike a + // non-partition implementation unit it does not implicitly import that + // interface, and by build order M's BMI is usually not built yet here, so + // this is best-effort: inherit only when the interface is already resident; + // never force a load and never diagnose its absence. For guaranteed + // enforcement a partition implementation unit should repeat + // [[profiles::enforce]] (see ProfilesFramework.rst). + if (Module *Primary = PP.getHeaderSearchInfo().getModuleMap().findModule( + Mod->getPrimaryModuleInterfaceName())) + for (const auto &EP : Primary->EnforcedProfileDesignators) + addProfileEnforcement(EP.ProfileName, EP.Designator, ModuleLoc); } // We already potentially made an implicit import (in the case of a module diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 0f689f55592b6..6a1b5490fc04e 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -17,6 +17,8 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_fail.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_iface_violation.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_enforce.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_inherit.cppm -fmodule-file=%t/mod_enforced.pcm -Wno-eager-load-cxx-named-modules -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_no_inherit.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_no_local_enforce.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_gmf_only_no_leak.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_different_desig.cppm -o %t/mod_different_desig.pcm -verify @@ -153,6 +155,37 @@ void impl_func() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } +// =================================================================== +// Partition implementation inherits the primary interface's enforced +// profiles when that interface's BMI is resident (best-effort). Only the +// eager -fmodule-file= form makes TestMod resident here; the lazy +// -fmodule-file== form loads on import only and would not +// trigger inheritance (the partition impl does not import the primary). +// TestMod enforces test::type_cast, so the cast is diagnosed without a +// local enforce. +// =================================================================== +//--- part_impl_inherit.cppm +module TestMod:inherit; + +void part_inherit_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// Normal build: the primary interface is not implicitly imported and is +// usually built after its partitions, so its BMI is absent here and the +// partition implementation unit does NOT inherit the enforcement. This is +// the best-effort limitation; repeat [[profiles::enforce]] for guaranteed +// enforcement. +// =================================================================== +//--- part_impl_no_inherit.cppm +// expected-no-diagnostics +module TestMod:inherit; + +void part_no_inherit_func() { + int *p = reinterpret_cast(0); +} + // =================================================================== // Importing a module with enforce must NOT leak enforcement into the // importer's own TU. Without a local enforce, reinterpret_cast is OK. From 059e2bbacac43a9e66d8eb4503edab14a49eb47c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 8 Jun 2026 08:52:48 -0400 Subject: [PATCH 084/289] Unify class and constructor finialization --- clang/docs/ProfilesFramework.rst | 46 +++++++++------ clang/include/clang/Sema/Sema.h | 10 ++++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 8 +-- clang/lib/Sema/SemaDeclCXX.cpp | 74 ++++++++++++------------ 4 files changed, 75 insertions(+), 63 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 568a455797173..52b38c41989e3 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -203,11 +203,10 @@ framework intentionally does not learn the profile name). Analysis-based passes in ``AnalysisBasedWarnings.cpp::IssueWarnings`` are normally run only when their corresponding warning flag is enabled. To run the pass for an enforced profile even when the underlying warning is - silenced, OR a ``llvm::any_of(Table, [&](const auto &E) { return - S.isProfileEnforced(E.Name); })`` check into the existing pass guard. - The in-tree example wraps this as ``anyCFGUninitProfileEnforced(S)`` and - the pass guard becomes - ``anyCFGUninitProfileEnforced(S) || !Diags.isIgnored(...)``. + silenced, OR an ``S.anyProfileEnforced(Table)`` check (the shared + ``Sema::anyProfileEnforced`` gate, also used by the finalization dispatch) + into the existing pass guard. The in-tree example's pass guard becomes + ``S.anyProfileEnforced(CFGUninitProfiles) || !Diags.isIgnored(...)``. 3. **Walk the table in the analysis's diagnostic reporter.** For each use site the analysis would have warned about, iterate the @@ -263,7 +262,9 @@ single function reached from every class-completion path -- the parser (``InstantiateClass``), and lambda completion -- so wiring the hook there covers all of them with no extra plumbing. -The dispatcher filters out classes the rules are not meant to see: +The class-finalization entry point +``checkProfileViolationsAtClassFinalization`` filters out classes the rules +are not meant to see: - Dependent classes (``isDependentType()``). The hook will re-fire on each instantiation via the template-instantiation completion path. @@ -279,17 +280,20 @@ This pattern needs two pieces, both colocated with the dispatcher. .. code-block:: c++ - struct ClassFinalizationProfileEntry { + // FinalizationProfile is shared with pattern 4. + template struct FinalizationProfile { StringRef Name; - void (*Callback)(Sema &, CXXRecordDecl *); + void (*Callback)(Sema &, Node *); }; - constexpr ClassFinalizationProfileEntry ClassFinalizationProfiles[] = { + constexpr FinalizationProfile ClassFinalizationProfiles[] = { {"my::profile", &runMyProfileCallback}, }; - The dispatcher iterates the table, skips entries whose profile is not - enforced, sets up a ``ProfileSuppressScope(*this, RD, - /*WalkLexicalParents=*/true)``, and invokes the callback. Because the + The shared ``dispatchFinalizationProfiles`` dispatcher (used by both + patterns 3 and 4) checks ``anyProfileEnforced(Table)``, sets up a + ``ProfileSuppressScope(S, RD, /*WalkLexicalParents=*/true)``, iterates the + table, skips entries whose profile is not enforced, and invokes the + callback. Because the suppress scope is established by the dispatcher, the callback can use the location-based ``shouldEmitProfileViolation`` overload and have ``[[profiles::suppress]]`` on the class or any enclosing lexical @@ -343,7 +347,9 @@ of them through ``Sema::InstantiateMemInitializers``, so the hook sees every constructor at the point its ``inits()`` (including synthesized entries) is complete. -The dispatcher filters out constructors the rules are not meant to see: +The constructor-finalization entry point +``checkProfileViolationsAtConstructorFinalization`` filters out constructors +the rules are not meant to see: - Dependent constructors (``isDependentContext()``). The hook re-fires on each instantiation. @@ -351,11 +357,13 @@ The dispatcher filters out constructors the rules are not meant to see: - Delegating constructors (``isDelegatingConstructor()``), which leave member initialization to their target. -The two pieces mirror pattern 3: a per-pass opt-in table -``ConstructorFinalizationProfiles`` (profile name plus a -``void (*)(Sema &, CXXConstructorDecl *)`` callback), and a callback that -emits via ``Sema::shouldEmitProfileViolation``. The dispatcher establishes a -``ProfileSuppressScope(*this, Ctor, /*WalkLexicalParents=*/true)`` around each +The two pieces mirror pattern 3 and share its machinery: a per-pass opt-in +table ``ConstructorFinalizationProfiles`` of the same +``FinalizationProfile`` row (here +``FinalizationProfile``), and a callback that emits via +``Sema::shouldEmitProfileViolation``. The same shared +``dispatchFinalizationProfiles`` dispatcher establishes a +``ProfileSuppressScope(S, Ctor, /*WalkLexicalParents=*/true)`` around each callback, so ``[[profiles::suppress]]`` on the constructor, the class, or an enclosing lexical ``Decl`` works. A callback that should only apply to user-written constructors checks ``Ctor->isUserProvided()``. @@ -545,7 +553,7 @@ CFG-based uninitialized-variables analysis. declaration. - **Opt-in table**: ``CFGUninitProfiles`` in ``clang/lib/Sema/AnalysisBasedWarnings.cpp``. The ``IssueWarnings`` pass - guard consults it via ``anyCFGUninitProfileEnforced(S)`` so the analysis + guard consults it via ``S.anyProfileEnforced(CFGUninitProfiles)`` so the analysis runs even when ``-Wuninitialized`` is silenced, and ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the default warning path -- when an entry's diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 0ae71fd1d6b49..b7ad5fcb2b386 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1052,6 +1052,16 @@ class Sema final : public SemaBase { SmallVector ProfileSuppressStack; bool isProfileEnforced(StringRef ProfileName) const; + + /// True if any entry of \p Entries names an enforced profile. \p Entries is + /// any profile opt-in table whose elements expose a \c Name member; shared by + /// the post-parse dispatch gates (the CFG analysis pass guard and the + /// finalization dispatcher). + template bool anyProfileEnforced(const Table &Entries) const { + return llvm::any_of( + Entries, [&](const auto &E) { return isProfileEnforced(E.Name); }); + } + const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; bool addProfileEnforcement(StringRef Name, StringRef Designator, SourceLocation Loc); diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 8112d3ddf80a6..e11317c86af78 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1701,12 +1701,6 @@ constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { {"std::init", "uninit_read", diag::err_init_uninit_read}, }; -bool anyCFGUninitProfileEnforced(const Sema &S) { - return llvm::any_of(CFGUninitProfiles, [&](const CFGUninitProfileEntry &E) { - return S.isProfileEnforced(E.Name); - }); -} - class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; AnalysisDeclContext &AC; @@ -3339,7 +3333,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( Analyzer.run(AC); } - if (anyCFGUninitProfileEnforced(S) || + if (S.anyProfileEnforced(CFGUninitProfiles) || !Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_sometimes_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_maybe_uninit_var, D->getBeginLoc()) || diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 301dd8a241234..ebea3b68a0495 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7030,15 +7030,16 @@ ReportOverrides(Sema &S, unsigned DiagID, const CXXMethodDecl *MD, } namespace { -// Profiles that opt into the class-finalization dispatch. Each entry pairs -// the profile name with a callback invoked once per completed, -// non-dependent, non-lambda, non-invalid CXXRecordDecl. Adding a new -// profile is a single row here plus a ProfileRuleError diagnostic in +// Row for the unified finalization dispatch shared by class-finalization +// (pattern 3) and constructor-finalization (pattern 4): a profile name plus a +// callback invoked once per finalized, non-dependent, non-invalid Node (a +// CXXRecordDecl or a CXXConstructorDecl). Adding a new profile is a single row +// in the matching table below plus a ProfileRuleError diagnostic in // DiagnosticSemaKinds.td and a callback that consults // Sema::shouldEmitProfileViolation before emitting. -struct ClassFinalizationProfileEntry { +template struct FinalizationProfile { StringRef Name; - void (*Callback)(Sema &, CXXRecordDecl *); + void (*Callback)(Sema &, Node *); }; void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { @@ -7049,21 +7050,6 @@ void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { << "test::class_final" << RD; } -constexpr ClassFinalizationProfileEntry ClassFinalizationProfiles[] = { - {"test::class_final", &runTestClassFinalCallback}, -}; - -// Profiles that opt into the constructor-finalization dispatch. Each entry -// pairs the profile name with a callback invoked once per fully-processed, -// non-dependent, non-invalid, non-delegating constructor (the point at which -// its member-initializer list, including synthesized entries, is complete). -// Adding a new profile is a single row here plus a ProfileRuleError diagnostic -// and a callback that consults Sema::shouldEmitProfileViolation. -struct ConstructorFinalizationProfileEntry { - StringRef Name; - void (*Callback)(Sema &, CXXConstructorDecl *); -}; - void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { if (!S.shouldEmitProfileViolation("test::ctor_final", /*Rule=*/"", Ctor->getLocation())) @@ -7109,11 +7095,38 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { } } -constexpr ConstructorFinalizationProfileEntry +// Class-finalization opt-in table (pattern 3). +constexpr FinalizationProfile ClassFinalizationProfiles[] = { + {"test::class_final", &runTestClassFinalCallback}, +}; + +// Constructor-finalization opt-in table (pattern 4). +constexpr FinalizationProfile ConstructorFinalizationProfiles[] = { {"test::ctor_final", &runTestCtorFinalCallback}, {"std::init", &runStdInitCtorUninitMemberCallback}, }; + +// Run the enforced finalization-profile callbacks in Table for D, setting up a +// suppress scope so [[profiles::suppress]] on D or a lexical parent is honored. +// Merges the former per-node dispatchers; the per-node filter (dependent, +// lambda, delegating, ...) stays at each call site. The table is taken by +// reference-to-array, not ArrayRef: deducing Node from a C array against an +// ArrayRef> parameter is not possible (no +// array-to-ArrayRef conversion happens during template argument deduction). +template +void dispatchFinalizationProfiles(Sema &S, Node *D, + const FinalizationProfile (&Table)[N]) { + if (!S.anyProfileEnforced(Table)) + return; + // ProfileSuppressScope is profile-agnostic (it pushes every + // [[profiles::suppress]] entry it finds and isProfileSuppressed filters by + // name later), so set it up once for all callbacks. + Sema::ProfileSuppressScope Scope(S, D, /*WalkLexicalParents=*/true); + for (const auto &E : Table) + if (S.isProfileEnforced(E.Name)) + E.Callback(S, D); +} } // namespace void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { @@ -7544,15 +7557,7 @@ void Sema::checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD) { return; if (RD->isInvalidDecl() || RD->isDependentType() || RD->isLambda()) return; - // ProfileSuppressScope is profile-agnostic (it pushes every - // [[profiles::suppress]] entry it finds and isProfileSuppressed filters by - // name later), so set it up once for all callbacks. - ProfileSuppressScope Scope(*this, RD, /*WalkLexicalParents=*/true); - for (const auto &E : ClassFinalizationProfiles) { - if (!isProfileEnforced(E.Name)) - continue; - E.Callback(*this, RD); - } + dispatchFinalizationProfiles(*this, RD, ClassFinalizationProfiles); } void Sema::checkProfileViolationsAtConstructorFinalization( @@ -7564,12 +7569,7 @@ void Sema::checkProfileViolationsAtConstructorFinalization( if (Ctor->isInvalidDecl() || Ctor->isDependentContext() || Ctor->isDelegatingConstructor()) return; - ProfileSuppressScope Scope(*this, Ctor, /*WalkLexicalParents=*/true); - for (const auto &E : ConstructorFinalizationProfiles) { - if (!isProfileEnforced(E.Name)) - continue; - E.Callback(*this, Ctor); - } + dispatchFinalizationProfiles(*this, Ctor, ConstructorFinalizationProfiles); } /// Look up the special member function that would be called by a special From a98df31c0abacdc3647d25c92076889bc62433be Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 11:43:58 -0400 Subject: [PATCH 085/289] Add [[ref_to_uninit]] attribute Introduce the [[ref_to_uninit]] marker attribute used by the std::init profile to denote a pointer, reference, or pointer-returning function that refers to uninitialized memory. This commit only recognizes and attaches the attribute (regardless of -fprofiles); subject-type validation and the profile matching rules follow in later commits. --- clang/include/clang/Basic/Attr.td | 6 +++++ clang/include/clang/Basic/AttrDocs.td | 27 +++++++++++++++++++ clang/lib/Sema/SemaDeclAttr.cpp | 8 ++++++ .../safety-profile-ref-to-uninit-marker.cpp | 23 ++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 5c27f306a5162..97f5f16f83124 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1727,6 +1727,12 @@ def CXX11Uninitialized : InheritableAttr { let Documentation = [CXX11UninitializedDocs]; } +def RefToUninit : InheritableAttr { + let Spellings = [CXX11<"", "ref_to_uninit", 202602>]; + let Subjects = SubjectList<[Var, Field, Function], ErrorDiag>; + let Documentation = [RefToUninitDocs]; +} + def NonBlocking : TypeAttr { let Spellings = [Clang<"nonblocking">]; let Args = [ExprArgument<"Cond", /*optional*/1>]; diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 448e7563dff56..7b315d0203d2b 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -919,6 +919,33 @@ uninitialized. }]; } +def RefToUninitDocs : Documentation { + let Category = DocCatVariable; + let Heading = "ref_to_uninit"; + let Content = [{ +The ``[[ref_to_uninit]]`` attribute marks a pointer, reference, or +pointer-returning function as referring to *uninitialized* memory. It is +purely informational: it has no effect on code generation. + +The attribute exists to support the initialization profile (see +:doc:`ProfilesFramework`). That profile bans forming an ordinary pointer or +reference to uninitialized storage; ``[[ref_to_uninit]]`` is the opt-in that +lets a program thread uninitialized memory (for example, an input buffer or +a memory-pool allocation) through pointers and references. Under the profile +a ``[[ref_to_uninit]]`` pointer or reference may only be bound to +uninitialized memory, and a pointer or reference that is *not* marked may +only be bound to initialized memory. + +The attribute applies to a variable, non-static data member, function +parameter, or function (for its return value) whose type is a pointer or +reference; applying it elsewhere is diagnosed regardless of ``-fprofiles``. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + }]; +} + def NoMergeDocs : Documentation { let Category = DocCatStmt; let Content = [{ diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 1d9018fadbcf6..9e192334e3291 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6985,6 +6985,10 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, D->addAttr(::new (S.Context) CXX11UninitializedAttr(S.Context, AL)); } +static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + D->addAttr(::new (S.Context) RefToUninitAttr(S.Context, AL)); +} + static void handleMIGServerRoutineAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // Check that the return type is a `typedef int kern_return_t` or a typedef // around it, because otherwise MIG convention checks make no sense. @@ -8255,6 +8259,10 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, handleCXX11UninitializedAttr(S, D, AL); break; + case ParsedAttr::AT_RefToUninit: + handleRefToUninitAttr(S, D, AL); + break; + case ParsedAttr::AT_ObjCExternallyRetained: S.ObjC().handleExternallyRetainedAttr(D, AL); break; diff --git a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp new file mode 100644 index 0000000000000..8af4e0d2c454e --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp @@ -0,0 +1,23 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// The [[ref_to_uninit]] marker is recognized regardless of -fprofiles and, +// with no profile enforced, has no effect on valid pointer/reference subjects. + +// expected-no-diagnostics +// no-profiles-no-diagnostics + +int g; + +int *gp [[ref_to_uninit]] = &g; +int &gr [[ref_to_uninit]] = g; + +[[ref_to_uninit]] int *gp_prefix = &g; + +[[ref_to_uninit]] int *allocate(int n); +void fill(int *p [[ref_to_uninit]]); +void bind(int &r [[ref_to_uninit]]); + +struct S { + int *m [[ref_to_uninit]]; +}; From 83121663c39cf2417b0c358c9b9d685519f1624c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 11:58:17 -0400 Subject: [PATCH 086/289] Validate [[ref_to_uninit]] subject types Reject [[ref_to_uninit]] on a variable or data member that is not a pointer or reference, and on a function whose return type is not a pointer or reference. Like the [[uninitialized]] subject checks, this is not profile policy and is diagnosed regardless of -fprofiles. --- .../clang/Basic/DiagnosticSemaKinds.td | 3 +++ clang/lib/Sema/SemaDeclAttr.cpp | 17 +++++++++++++ .../safety-profile-ref-to-uninit-marker.cpp | 25 ++++++++++++++----- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 71808a28c8bc0..6ea0925c137fe 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14157,6 +14157,9 @@ def err_init_uninit_with_initializer : ProfileRuleError< def err_uninitialized_attr_invalid_subject : Error< "'uninitialized' attribute cannot be applied to %select{a reference|" "a function parameter|a structured binding}0">; +def err_ref_to_uninit_attr_invalid_type : Error< + "'ref_to_uninit' attribute only applies to pointers, references, and " + "functions returning them">; def err_init_union_marker : ProfileRuleError< "'[[uninitialized]]' cannot be applied to %select{a variable of union type|" "a union member}1 under profile '%0'">; diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 9e192334e3291..17d490732a46f 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6986,6 +6986,23 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, } static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList restricts D to a variable, non-static data member, or + // function. "Refers to uninitialized memory" is only meaningful for a + // pointer or reference (for a function, its return value), so reject any + // other type. Like the [[uninitialized]] subject checks, this is not + // profile policy and so fires regardless of -fprofiles. + QualType T; + if (const auto *FD = dyn_cast(D)) + T = FD->getReturnType(); + else + T = cast(D)->getType(); + + if (!T->isPointerType() && !T->isReferenceType()) { + S.Diag(AL.getLoc(), diag::err_ref_to_uninit_attr_invalid_type); + AL.setInvalid(); + return; + } + D->addAttr(::new (S.Context) RefToUninitAttr(S.Context, AL)); } diff --git a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp index 8af4e0d2c454e..7eab184c71183 100644 --- a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp @@ -1,23 +1,36 @@ // RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s -// The [[ref_to_uninit]] marker is recognized regardless of -fprofiles and, -// with no profile enforced, has no effect on valid pointer/reference subjects. - -// expected-no-diagnostics -// no-profiles-no-diagnostics +// The [[ref_to_uninit]] marker is recognized regardless of -fprofiles. It +// applies to pointers, references, and pointer/reference-returning functions; +// with no profile enforced it has no effect on those valid subjects. Other +// placements are rejected regardless of -fprofiles. int g; int *gp [[ref_to_uninit]] = &g; int &gr [[ref_to_uninit]] = g; - [[ref_to_uninit]] int *gp_prefix = &g; [[ref_to_uninit]] int *allocate(int n); +[[ref_to_uninit]] int &bind_ret(); void fill(int *p [[ref_to_uninit]]); void bind(int &r [[ref_to_uninit]]); struct S { int *m [[ref_to_uninit]]; }; + +int bad_scalar [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +[[ref_to_uninit]] int bad_array[3]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +[[ref_to_uninit]] void bad_return(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +struct BadMember { + int m [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +}; From 8e7c7f4375644dc64489a22285bf85c647b7401c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 12:17:59 -0400 Subject: [PATCH 087/289] Enforce [[ref_to_uninit]] at variable initialization under std::init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a local recognizer for expressions that refer to uninitialized memory (the address of, or a subobject of, a [[uninitialized]] entity; a value of a [[ref_to_uninit]] pointer/reference or array; a dereference of such a pointer; or a call to a [[ref_to_uninit]]-returning function) and enforce, at each pointer/reference variable initialization, that a [[ref_to_uninit]] target is bound to uninitialized memory and an unmarked target is not (paper §5). The check is local and gated on std::init enforcement; dependent types defer to instantiation. --- .../clang/Basic/DiagnosticSemaKinds.td | 6 ++ clang/include/clang/Sema/Sema.h | 18 ++++ clang/lib/Sema/SemaDecl.cpp | 89 +++++++++++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 59 ++++++++++++ 4 files changed, 172 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 6ea0925c137fe..cbbb413f892a0 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14167,4 +14167,10 @@ def err_init_ctor_uninit_member : ProfileRuleError< "constructor does not initialize member %1 under profile '%0'">; def note_init_uninit_member_here : Note< "member %0 declared here">; +def err_init_ref_to_uninit_requires_uninit : ProfileRuleError< + "%select{pointer|reference}1 marked '[[ref_to_uninit]]' must refer to " + "uninitialized memory under profile '%0'">; +def err_init_uninit_requires_ref_to_uninit : ProfileRuleError< + "%select{pointer|reference}1 to uninitialized memory must be marked " + "'[[ref_to_uninit]]' under profile '%0'">; } // end of sema component. diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index b7ad5fcb2b386..bad48c60d3dea 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1110,6 +1110,24 @@ class Sema final : public SemaBase { /// definition). Dependent and incomplete types are treated as determinate. bool defaultInitLeavesScalarIndeterminate(QualType T); + /// std::init / ref_to_uninit (paper §5): true if \p E refers to (for a + /// pointer source) or, when \p IsReference, denotes (for a glvalue source) + /// uninitialized storage. Recognized purely locally from the expression's + /// syntactic form -- the address of, or a subobject of, a [[uninitialized]] + /// entity; a value of a [[ref_to_uninit]] pointer/reference or array; a + /// dereference of such a pointer; or a call to a [[ref_to_uninit]]-returning + /// function. Anything else is treated as initialized (the trust model; no + /// flow analysis). + bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; + + /// std::init / ref_to_uninit (paper §5): check that the initialization of a + /// pointer or reference is consistent with its [[ref_to_uninit]] marking -- + /// a marked target must refer to uninitialized memory, and an unmarked + /// target must not. Shared by the variable, data-member, assignment, + /// argument, and return check sites; gated by shouldEmitProfileViolation. + void checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, + bool IsReference, const Expr *Src); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 2ec085eec82f9..e2dd60aa8e8b4 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14913,6 +14913,87 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, Diag(Loc, diag::err_init_uninit_with_initializer) << Profile << Name; } +// std::init / ref_to_uninit (paper §5). Two mutually-recursive local +// recognizers over the syntactic form of a source expression -- no flow +// analysis and no type-system tracking. Uninitialized storage is only ever +// introduced by an explicit [[uninitialized]] / [[ref_to_uninit]] marker; +// anything unrecognized is treated as initialized (the trust model). +static bool glvalueDenotesUninitStorage(const Expr *E); + +// \p E is a pointer prvalue. True if it points to uninitialized storage. +static bool pointerRefersToUninitStorage(const Expr *E) { + if (!E) + return false; + E = E->IgnoreParenImpCasts(); + + // Array-to-pointer decay has been stripped above, leaving the array glvalue. + if (E->getType()->isArrayType()) + return glvalueDenotesUninitStorage(E); + + // &G, where G denotes uninitialized storage. + if (const auto *UO = dyn_cast(E)) + if (UO->getOpcode() == UO_AddrOf) + return glvalueDenotesUninitStorage(UO->getSubExpr()); + + // A value of a [[ref_to_uninit]] pointer, or a call to a + // [[ref_to_uninit]]-returning function. + if (const auto *DRE = dyn_cast(E)) + return DRE->getDecl()->hasAttr(); + if (const auto *ME = dyn_cast(E)) + return ME->getMemberDecl()->hasAttr(); + if (const auto *CE = dyn_cast(E)) + if (const FunctionDecl *FD = CE->getDirectCallee()) + return FD->hasAttr(); + + return false; +} + +// \p E is a glvalue. True if it denotes uninitialized storage. +static bool glvalueDenotesUninitStorage(const Expr *E) { + if (!E) + return false; + E = E->IgnoreParenImpCasts(); + + // A [[uninitialized]] variable / data member, or a subobject of one. + if (const auto *DRE = dyn_cast(E)) + return DRE->getDecl()->hasAttr(); + if (const auto *ME = dyn_cast(E)) + return ME->getMemberDecl()->hasAttr() || + glvalueDenotesUninitStorage(ME->getBase()); + if (const auto *ASE = dyn_cast(E)) + return pointerRefersToUninitStorage(ASE->getBase()); + + // *p, where p points to uninitialized storage. + if (const auto *UO = dyn_cast(E)) + if (UO->getOpcode() == UO_Deref) + return pointerRefersToUninitStorage(UO->getSubExpr()); + + return false; +} + +bool Sema::refersToUninitializedMemory(const Expr *E, bool IsReference) const { + return IsReference ? glvalueDenotesUninitStorage(E) + : pointerRefersToUninitStorage(E); +} + +void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, + bool IsReference, const Expr *Src) { + // A RecoveryExpr is a placeholder for an initialization that already failed, + // not a source the user wrote, so it must not drive this rule. + if (!Src || isa(Src->IgnoreParens())) + return; + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "ref_to_uninit"; + if (!shouldEmitProfileViolation(Profile, Rule, Loc)) + return; + bool SrcUninit = refersToUninitializedMemory(Src, IsReference); + unsigned IsRef = IsReference ? 1 : 0; + if (TargetIsRefToUninit && !SrcUninit) + Diag(Loc, diag::err_init_ref_to_uninit_requires_uninit) << Profile << IsRef; + else if (!TargetIsRefToUninit && SrcUninit) + Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; +} + void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; @@ -14920,6 +15001,14 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { var->getLocation(), var->getDeclName(), var->getType(), var->getInit(), var->hasAttr()); + // std::init / ref_to_uninit (paper §5): a pointer or reference variable must + // be bound consistently with its [[ref_to_uninit]] marking. A dependent type + // is deferred to instantiation, where the rule re-runs on the concrete type. + if (QualType VT = var->getType(); !VT->isDependentType() && + (VT->isPointerType() || VT->isReferenceType())) + checkRefToUninitInit(var->getLocation(), var->hasAttr(), + VT->isReferenceType(), var->getInit()); + CUDA().MaybeAddConstantAttr(var); if (getLangOpts().OpenCL) { diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp new file mode 100644 index 0000000000000..a036d89d41604 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -0,0 +1,59 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// std::init / ref_to_uninit (paper §5): a [[ref_to_uninit]] pointer or +// reference must be bound to uninitialized memory, and an unmarked pointer or +// reference must not. This file exercises the rule at variable initialization. + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +int g_init = 0; +[[uninitialized]] int g_uninit; +[[uninitialized]] int g_uninit_arr[3]; +[[ref_to_uninit]] int *allocate(int n); + +void test_pointer_target() { + int *p1 [[ref_to_uninit]] = &g_uninit; // OK + int *p2 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p3 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p4 = &g_init; // OK + int *p5 = nullptr; // OK + (void)p1; (void)p2; (void)p3; (void)p4; (void)p5; +} + +void test_pointer_sources() { + int *base [[ref_to_uninit]] = &g_uninit; + int *from_ptr [[ref_to_uninit]] = base; // OK: base is [[ref_to_uninit]] + int *from_array [[ref_to_uninit]] = g_uninit_arr; // OK: array-to-pointer decay + int *from_call [[ref_to_uninit]] = allocate(3); // OK: [[ref_to_uninit]] return + int *bad_from_array = g_uninit_arr; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *bad_from_call = allocate(3); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)base; (void)from_ptr; (void)from_array; (void)from_call; + (void)bad_from_array; (void)bad_from_call; +} + +void test_reference_target() { + int &r1 [[ref_to_uninit]] = g_uninit; // OK + int &r2 [[ref_to_uninit]] = g_init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r3 = g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r4 = g_init; // OK + int *p [[ref_to_uninit]] = &g_uninit; + int &r5 [[ref_to_uninit]] = *p; // OK: *p denotes uninitialized storage + (void)r1; (void)r2; (void)r3; (void)r4; (void)r5; (void)p; +} + +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *s = &g_uninit; // OK: suppressed + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int *s2 [[ref_to_uninit]] = &g_init; // OK: suppressed + (void)s; (void)s2; +} + +template +void template_bad() { + T *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_bad(); // expected-note {{in instantiation of function template specialization 'template_bad' requested here}} From 57a5fff7ccde243bad89967490bb98d996803d55 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 12:25:17 -0400 Subject: [PATCH 088/289] Enforce [[ref_to_uninit]] on data-member initializers under std::init Apply the ref_to_uninit matching rule to a pointer/reference data member with a default member initializer, in ActOnFinishCXXInClassMemberInitializer (the same site that already runs the uninit_with_initializer check). --- clang/lib/Sema/SemaDeclCXX.cpp | 8 ++++++++ .../test/SemaCXX/safety-profile-init-ref-to-uninit.cpp | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index ebea3b68a0495..1da8d9b146998 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4230,6 +4230,14 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, FD->getType(), FD->getInClassInitializer(), FD->hasAttr()); + + // std::init / ref_to_uninit (paper §5): a pointer or reference data member + // with a default member initializer must be bound consistently with its + // [[ref_to_uninit]] marking. + if (QualType FT = FD->getType(); !FT->isDependentType() && + (FT->isPointerType() || FT->isReferenceType())) + checkRefToUninitInit(FD->getLocation(), FD->hasAttr(), + FT->isReferenceType(), FD->getInClassInitializer()); } /// Find the direct and/or virtual base specifiers that diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index a036d89d41604..275f0bf977568 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -51,6 +51,16 @@ void test_suppress() { (void)s; (void)s2; } +struct WithFields { + int *p1 [[ref_to_uninit]] = &g_uninit; // OK + int *p2 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p3 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p4 = &g_init; // OK + int *p5 = nullptr; // OK + int &r1 [[ref_to_uninit]] = g_uninit; // OK + int &r2 = g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + template void template_bad() { T *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} From 101683da4c90791c9bb66440c6288e1e5389a654 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 12:30:38 -0400 Subject: [PATCH 089/289] Enforce [[ref_to_uninit]] on pointer assignment under std::init Apply the ref_to_uninit matching rule to a plain pointer assignment in CreateBuiltinBinOp when the LHS directly names a pointer entity. References cannot be reseated, so only pointer assignment is checked. --- clang/lib/Sema/SemaExpr.cpp | 17 +++++++++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 13 +++++++++++++ 2 files changed, 30 insertions(+) diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index db6d93ce54791..9bdac20ad003c 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -15483,6 +15483,23 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, DiagnoseSelfAssignment(*this, LHS.get(), RHS.get(), OpLoc, true); DiagnoseSelfMove(LHS.get(), RHS.get(), OpLoc); + // std::init / ref_to_uninit (paper §5): assigning a pointer must respect + // the [[ref_to_uninit]] marking of the assigned-to pointer named on the + // LHS. References cannot be reseated, so only pointer assignment applies; + // the check is limited to an LHS that directly names a pointer entity (so + // its marker can be read locally). + if (getLangOpts().Profiles && LHS.get()->getType()->isPointerType()) { + const Expr *L = LHS.get()->IgnoreParenImpCasts(); + const ValueDecl *VD = nullptr; + if (const auto *DRE = dyn_cast(L)) + VD = DRE->getDecl(); + else if (const auto *ME = dyn_cast(L)) + VD = ME->getMemberDecl(); + if (VD) + checkRefToUninitInit(OpLoc, VD->hasAttr(), + /*IsReference=*/false, RHS.get()); + } + // Avoid copying a block to the heap if the block is assigned to a local // auto variable that is declared in the same scope as the block. This // optimization is unsafe if the local variable is declared in an outer diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 275f0bf977568..a945d7870afe2 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -43,6 +43,19 @@ void test_reference_target() { (void)r1; (void)r2; (void)r3; (void)r4; (void)r5; (void)p; } +void test_assignment() { + int *p [[ref_to_uninit]] = &g_uninit; + p = &g_uninit; // OK + p = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *q = &g_init; + q = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + q = &g_init; // OK + q = nullptr; // OK + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { q = &g_uninit; } // OK: suppressed + (void)p; (void)q; +} + void test_suppress() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *s = &g_uninit; // OK: suppressed From f7f3a80b424007b9cfa68bfcc63cfe8a5ebddaa5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 12:36:09 -0400 Subject: [PATCH 090/289] Enforce [[ref_to_uninit]] on call arguments under std::init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match each pointer/reference call argument against its parameter's [[ref_to_uninit]] marking in GatherArgumentsForCall (paper §5), so a marked parameter requires uninitialized memory and an unmarked parameter rejects it. This realizes the paper's uninitialized_fill() example. --- clang/lib/Sema/SemaExpr.cpp | 10 +++++++ .../safety-profile-init-ref-to-uninit.cpp | 28 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 9bdac20ad003c..97714ead6d281 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6263,6 +6263,16 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, return true; Arg = ArgE.getAs(); + + // std::init / ref_to_uninit (paper §5): a pointer or reference argument + // must match the [[ref_to_uninit]] marking of its parameter. + if (Param && getLangOpts().Profiles) { + QualType PT = Param->getType(); + if (PT->isPointerType() || PT->isReferenceType()) + checkRefToUninitInit(Arg->getExprLoc(), + Param->hasAttr(), + PT->isReferenceType(), Arg); + } } else { assert(Param && "can't use default arguments without a known callee"); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index a945d7870afe2..b2c8039ff08b3 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -64,6 +64,34 @@ void test_suppress() { (void)s; (void)s2; } +void take_uninit_ptr(int *p [[ref_to_uninit]]); +void take_uninit_ref(int &r [[ref_to_uninit]]); +void take_ptr(int *p); +void take_ref(const int &r); +void uninitialized_fill(int *r [[ref_to_uninit]], int val); + +void test_call_arguments() { + take_uninit_ptr(&g_uninit); // OK + take_uninit_ptr(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_uninit_ptr(g_uninit_arr); // OK: array-to-pointer decay + take_uninit_ref(g_uninit); // OK + take_uninit_ref(g_init); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ptr(&g_init); // OK + take_ref(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ref(g_init); // OK + + // The worked example from paper §5. + int a1[] = {1, 2, 3}; + [[uninitialized]] int a2[3]; + uninitialized_fill(a1, 10); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + uninitialized_fill(a2, 10); // OK + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { take_ptr(&g_uninit); } // OK: suppressed +} + struct WithFields { int *p1 [[ref_to_uninit]] = &g_uninit; // OK int *p2 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} From bab54f8ee8a886340c6a69705783ece73647763b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 12:40:28 -0400 Subject: [PATCH 091/289] Enforce [[ref_to_uninit]] on return statements under std::init MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match a returned pointer/reference against the function's [[ref_to_uninit]] return marking in BuildReturnStmt (paper §5): a marked function must return uninitialized memory and an unmarked one must not. --- clang/lib/Sema/SemaStmt.cpp | 9 +++++++++ .../safety-profile-init-ref-to-uninit.cpp | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp index b74af55d1bea1..dd00211f43d38 100644 --- a/clang/lib/Sema/SemaStmt.cpp +++ b/clang/lib/Sema/SemaStmt.cpp @@ -4116,6 +4116,15 @@ StmtResult Sema::BuildReturnStmt(SourceLocation ReturnLoc, Expr *RetValExp, } } } + // std::init / ref_to_uninit (paper §5): the returned pointer or reference + // must match the function's [[ref_to_uninit]] return marking. + if (RetValExp && getLangOpts().Profiles && !FnRetType->isDependentType() && + (FnRetType->isPointerType() || FnRetType->isReferenceType())) + if (const FunctionDecl *FD = getCurFunctionDecl()) + checkRefToUninitInit(RetValExp->getExprLoc(), + FD->hasAttr(), + FnRetType->isReferenceType(), RetValExp); + const VarDecl *NRVOCandidate = getCopyElisionCandidate(NRInfo, FnRetType); bool HasDependentReturnType = FnRetType->isDependentType(); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index b2c8039ff08b3..4a981085a5c4a 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -102,6 +102,25 @@ struct WithFields { int &r2 = g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} }; +[[ref_to_uninit]] int *ret_uninit_ptr_ok() { return &g_uninit; } // OK +[[ref_to_uninit]] int *ret_uninit_ptr_bad() { + return &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +int *ret_ptr_bad() { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +int *ret_ptr_ok() { return &g_init; } // OK + +[[ref_to_uninit]] int &ret_uninit_ref_ok() { return g_uninit; } // OK +int &ret_ref_bad() { + return g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +int *ret_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] return &g_uninit; // OK: suppressed +} + template void template_bad() { T *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} From 699ffacd5da2581e2b7df8aa2ee9c5b56e69ecec Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 12:50:02 -0400 Subject: [PATCH 092/289] Document [[ref_to_uninit]] in the std::init profile docs Move [[ref_to_uninit]] out of the std::init "not yet implemented" list and document the marker attribute, the ref_to_uninit rule (R7), its diagnostics and check sites, and the new tests. --- clang/docs/ProfilesFramework.rst | 66 +++++++++++++++++++++++++++----- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 52b38c41989e3..538232dbbe035 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -612,17 +612,18 @@ The ``std::init`` Profile (initial slice) ----------------------------------------- A slice of the proposed initialization profile. It does not yet implement -``[[ref_to_uninit]]`` (paper §5), classes that expose uninitialized memory to -users (paper §6.2), or random-access initialization of uninitialized arrays -(paper §6.4); and the constructor-body flow check that would let a -``[[uninitialized]]`` member be initialized by assignment in the body (the -dynamic half of paper §6.1) is deferred to a future CFG-based pass. Until it -lands, a ``[[uninitialized]]`` data member is trusted. +classes that expose uninitialized memory to users (paper §6.2) or random-access +initialization of uninitialized arrays (paper §6.4); and the constructor-body +flow check that would let a ``[[uninitialized]]`` member be initialized by +assignment in the body (the dynamic half of paper §6.1) is deferred to a future +CFG-based pass. Until it lands, a ``[[uninitialized]]`` data member is trusted, +and writes *through* a ``[[ref_to_uninit]]`` pointer/reference are not yet +verified (the paper relegates them to ``construct_at`` or suppression). -The slice introduces one new attribute and the rules below. +The slice introduces two marker attributes and the rules below. -Marker attribute -~~~~~~~~~~~~~~~~ +Marker attributes +~~~~~~~~~~~~~~~~~ ``[[uninitialized]]`` (a standard C++11 attribute, distinct from the Clang vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` or @@ -649,6 +650,19 @@ regardless of ``-fprofiles``; its profile rules carry weight only when marker is accepted there (the object is genuinely left uninitialized). - Is banned on a union object or union member by ``union_marker``. +``[[ref_to_uninit]]`` (also a standard C++11 attribute) marks a pointer, +reference, or pointer/reference-returning function as referring to +*uninitialized* memory. Recognised by Clang regardless of ``-fprofiles``; its +profile rule carries weight only when ``std::init`` is enforced. + +- TableGen def: ``RefToUninit`` in ``clang/include/clang/Basic/Attr.td``, with a + custom handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. +- Subjects: ``Var``, ``Field``, and ``Function``. The handler rejects any + subject whose type (or, for a function, return type) is not a pointer or + reference, via ``err_ref_to_uninit_attr_invalid_type`` -- regardless of + ``-fprofiles``. +- Behaviour: drives the ``ref_to_uninit`` rule (below); has no other effect. + Rules ~~~~~ @@ -754,6 +768,32 @@ assignment when compiled without the profile. enforcement -- a union may legitimately carry the marker without the profile. +R7. ``ref_to_uninit`` -- pattern 1 +.................................. + +A pointer or reference must be bound consistently with its +``[[ref_to_uninit]]`` marking (paper §5): a marked pointer/reference may only +refer to uninitialized memory, and an unmarked one may only refer to +initialized memory. "Refers to uninitialized memory" is recognised purely +locally from the source expression's syntactic form (no flow analysis): the +address of, or a subobject of, a ``[[uninitialized]]`` entity; a value of a +``[[ref_to_uninit]]`` pointer/reference or array; a dereference of such a +pointer; or a call to a ``[[ref_to_uninit]]``-returning function. Anything +else is treated as initialized (the trust model). + +- Diagnostics: ``err_init_ref_to_uninit_requires_uninit`` (marked target, + initialized source) and ``err_init_uninit_requires_ref_to_uninit`` (unmarked + target, uninitialized source). +- Recognizer + shared check: ``Sema::refersToUninitializedMemory`` and + ``Sema::checkRefToUninitInit`` in ``clang/lib/Sema/SemaDecl.cpp``. +- Check sites: variable initialization + (``Sema::CheckCompleteVariableDeclaration``), default member initializers + (``Sema::ActOnFinishCXXInClassMemberInitializer``), pointer assignment + (``Sema::CreateBuiltinBinOp``), call arguments + (``Sema::GatherArgumentsForCall``), and return statements + (``Sema::BuildReturnStmt``). A dependent target type defers to + instantiation. + Diagnostic suppression ~~~~~~~~~~~~~~~~~~~~~~ @@ -841,5 +881,13 @@ profiles. When changing the framework, run them all with - ``clang/test/SemaCXX/safety-profile-init-union.cpp`` -- the ``std::init`` profile's ``union_marker`` rule banning the marker on a union object or union member. +- ``clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp`` -- placement + of ``[[ref_to_uninit]]`` and rejection on subjects that are not pointers, + references, or pointer/reference-returning functions. +- ``clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp`` -- the + ``std::init`` profile's ``ref_to_uninit`` rule across every check site: + variable and data-member initialization, assignment, call arguments + (including the paper's ``uninitialized_fill`` example), and return + statements, plus suppression and template instantiation. - ``clang/test/PCH/cxx-profiles-enforce.cpp`` -- ``[[profiles::enforce]]`` state survives PCH serialization round-trip. From 9ee24a003c20b66f415f7ca07bfc22536ffb9fa2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 13:07:45 -0400 Subject: [PATCH 093/289] Honor [[profiles::suppress]] on data-member initializers at finalization The parse-time suppress scope set up in ParseCXXMemberInitializer is destroyed before ActOnFinishCXXInClassMemberInitializer runs, so the std::init finalization checks (uninit_with_initializer and ref_to_uninit) did not see a [[profiles::suppress]] attached to a data member. Re-establish the scope from the field (walking lexical parents) in ActOnFinishCXXInClassMemberInitializer, mirroring the finalization-dispatch pattern. --- clang/lib/Sema/SemaDeclCXX.cpp | 6 ++++++ .../safety-profile-init-ref-to-uninit.cpp | 17 +++++++++++++++++ .../safety-profile-init-with-initializer.cpp | 15 +++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 1da8d9b146998..4a6049172adac 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4226,6 +4226,12 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, FD->setInClassInitializer(InitExpr.get()); + // The parse-time [[profiles::suppress]] scope set up while parsing the + // initializer is gone by the time this finalization runs (a late-parsed + // NSDMI finishes parsing before this point), so re-establish it from the + // field for the post-initialization profile checks below. + ProfileSuppressScope SuppressScope(*this, FD, /*WalkLexicalParents=*/true); + checkInitProfileUninitWithInitializer(FD->getLocation(), FD->getDeclName(), FD->getType(), FD->getInClassInitializer(), diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 4a981085a5c4a..78b55ca6e92bb 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -102,6 +102,23 @@ struct WithFields { int &r2 = g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} }; +// [[profiles::suppress]] on a data member must cover its initializer's +// finalization checks, not just the initializer's parsing. +struct WithSuppressedFields { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *p1 = &g_uninit; // OK: rule-targeted suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int *p2 = &g_uninit; // OK: whole-profile suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int *p3 [[ref_to_uninit]] = &g_init; // OK: suppressed (marked target, initialized source) +}; + +// A suppress on the enclosing record covers its members' initializers. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] WithClassLevelSuppress { + int *p = &g_uninit; // OK: suppressed by the class-level attribute +}; + [[ref_to_uninit]] int *ret_uninit_ptr_ok() { return &g_uninit; } // OK [[ref_to_uninit]] int *ret_uninit_ptr_bad() { return &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index 692368d189aff..5d8dbb0fb68a7 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -75,6 +75,21 @@ void test_suppress_block() { } } +// [[profiles::suppress]] on a data member covers the uninit_with_initializer +// check that runs when its NSDMI is finalized, not just its parsing. +struct WithSuppressedNSDMI { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] int m [[uninitialized]] = 0; // OK: rule-targeted suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int n [[uninitialized]] = 1; // OK: whole-profile suppress +}; + +// A suppress on the enclosing record covers its members' NSDMIs. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] WithClassLevelSuppressedNSDMI { + int m [[uninitialized]] = 0; // OK: suppressed by the class-level attribute +}; + template void template_marker_with_init() { T x [[uninitialized]] = T{}; // expected-error 2 {{variable 'x' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} From 9d323ae1bcc0dfdd735eb1a1af278de27aad285e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 13:36:51 -0400 Subject: [PATCH 094/289] Resolve std::init suppression from the declaration, not just the parse stack Decl-attached std::init checks (uninit_decl, static_runtime_init, uninit_with_initializer, ref_to_uninit on a variable/member) consulted only the parse-time ProfileSuppressStack, which must be alive at the moment the check fires -- the root cause of the data-member-initializer suppression gap and similar timing-dependent bugs. Add a Decl-aware shouldEmitProfileViolation(name, rule, Loc, const Decl *D) overload that also walks D and its lexical parents for [[profiles::suppress]] (reusing the walk the post-parse Stmt walker already performs), and route the decl-attached checks through it. This subsumes the data-member-initializer finalization scope, which is removed. Expr/statement-level checks keep using the stack as before. --- clang/include/clang/Sema/Sema.h | 10 ++++-- clang/lib/Sema/Sema.cpp | 35 +++++++++++++------ clang/lib/Sema/SemaDecl.cpp | 18 +++++----- clang/lib/Sema/SemaDeclCXX.cpp | 17 ++++----- .../SemaCXX/safety-profile-init-static.cpp | 7 ++++ 5 files changed, 59 insertions(+), 28 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index bad48c60d3dea..3adb4523360b3 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1081,10 +1081,14 @@ class Sema final : public SemaBase { bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName = "") const; + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Decl *D) const; bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, const Stmt *S, AnalysisDeclContext &AC) const; bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc); + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, const Decl *D); bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, const Stmt *UseStmt, AnalysisDeclContext &AC) const; @@ -1100,7 +1104,8 @@ class Sema final : public SemaBase { void checkInitProfileUninitWithInitializer(SourceLocation Loc, DeclarationName Name, QualType DeclType, - const Expr *Init, bool HasMarker); + const Expr *Init, bool HasMarker, + const Decl *D); /// True if default-initialization of \p T would leave at least one scalar /// subobject with an indeterminate value. Shared by the std::init rules @@ -1126,7 +1131,8 @@ class Sema final : public SemaBase { /// target must not. Shared by the variable, data-member, assignment, /// argument, and return check sites; gated by shouldEmitProfileViolation. void checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, - bool IsReference, const Expr *Src); + bool IsReference, const Expr *Src, + const Decl *D = nullptr); class ProfileSuppressScope { Sema &S; diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 2815850c92597..a5d6040adff3d 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3112,6 +3112,19 @@ bool Sema::isProfileSuppressed(StringRef ProfileName, return false; } +bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Decl *D) const { + for (; D;) { + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + const DeclContext *DC = D->getLexicalDeclContext(); + D = DC ? dyn_cast(DC) : nullptr; + } + return false; +} + bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName, const Stmt *S, AnalysisDeclContext &AC) const { @@ -3133,22 +3146,24 @@ bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName, ProfileName, RuleName)) return true; } - for (const Decl *D = AC.getDecl(); D;) { - for (const auto *PSA : D->specific_attrs()) - if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), - ProfileName, RuleName)) - return true; - const DeclContext *DC = D->getLexicalDeclContext(); - D = DC ? dyn_cast(DC) : nullptr; - } - return false; + return isProfileSuppressed(ProfileName, RuleName, AC.getDecl()); } bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc) { + return shouldEmitProfileViolation(ProfileName, RuleName, Loc, /*D=*/nullptr); +} + +bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, const Decl *D) { if (!isProfileEnforced(ProfileName)) return false; - if (isProfileSuppressed(ProfileName, RuleName)) + // Honor [[profiles::suppress]] from the parse-time stack and, when a Decl is + // available, from the declaration and its lexical parents. The latter does + // not depend on a parse-time scope still being active, so finalization checks + // that run after the parse scope is torn down still respect suppression. + if (isProfileSuppressed(ProfileName, RuleName) || + isProfileSuppressed(ProfileName, RuleName, D)) return false; // P3589R2 Section 1.1: "its static semantic effects are as-if applied only // after translation phase 7. It is not possible for a profile to change the diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index e2dd60aa8e8b4..6ccd04720e9c0 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14727,7 +14727,7 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && !Var->hasAttr() && - shouldEmitProfileViolation(Profile, Rule, Var->getLocation()) && + shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && // A definition with no initializer (scalar / pointer / enum, or an // array of them), or a class/aggregate type whose default-init leaves // a scalar subobject indeterminate (its synthesized constructor call @@ -14887,7 +14887,8 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, DeclarationName Name, QualType DeclType, const Expr *Init, - bool HasMarker) { + bool HasMarker, + const Decl *D) { // [[uninitialized]] documents that the entity is intentionally left // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr // is a placeholder for an initialization that already failed (e.g. @@ -14898,7 +14899,7 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "uninit_with_initializer"; // Gate the (possibly recursive) type walk below on enforcement. - if (!shouldEmitProfileViolation(Profile, Rule, Loc)) + if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) return; // A synthesized default-initialization that leaves the object indeterminate // (a trivial or aggregate type, no user-written initializer) is consistent @@ -14977,14 +14978,15 @@ bool Sema::refersToUninitializedMemory(const Expr *E, bool IsReference) const { } void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, - bool IsReference, const Expr *Src) { + bool IsReference, const Expr *Src, + const Decl *D) { // A RecoveryExpr is a placeholder for an initialization that already failed, // not a source the user wrote, so it must not drive this rule. if (!Src || isa(Src->IgnoreParens())) return; static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "ref_to_uninit"; - if (!shouldEmitProfileViolation(Profile, Rule, Loc)) + if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) return; bool SrcUninit = refersToUninitializedMemory(Src, IsReference); unsigned IsRef = IsReference ? 1 : 0; @@ -14999,7 +15001,7 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { checkInitProfileUninitWithInitializer( var->getLocation(), var->getDeclName(), var->getType(), var->getInit(), - var->hasAttr()); + var->hasAttr(), var); // std::init / ref_to_uninit (paper §5): a pointer or reference variable must // be bound consistently with its [[ref_to_uninit]] marking. A dependent type @@ -15007,7 +15009,7 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (QualType VT = var->getType(); !VT->isDependentType() && (VT->isPointerType() || VT->isReferenceType())) checkRefToUninitInit(var->getLocation(), var->hasAttr(), - VT->isReferenceType(), var->getInit()); + VT->isReferenceType(), var->getInit(), var); CUDA().MaybeAddConstantAttr(var); @@ -15259,7 +15261,7 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (checkConstInit()) return false; if (!shouldEmitProfileViolation(Profile, Rule, - var->getLocation())) + var->getLocation(), var)) return false; Diag(var->getLocation(), diag::err_init_static_runtime_init) << Profile << var->getDeclName(); diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 4a6049172adac..c41999c93d17c 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4226,16 +4226,16 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, FD->setInClassInitializer(InitExpr.get()); - // The parse-time [[profiles::suppress]] scope set up while parsing the - // initializer is gone by the time this finalization runs (a late-parsed - // NSDMI finishes parsing before this point), so re-establish it from the - // field for the post-initialization profile checks below. - ProfileSuppressScope SuppressScope(*this, FD, /*WalkLexicalParents=*/true); - + // Pass the field to the post-initialization profile checks so the Decl-aware + // shouldEmitProfileViolation overload resolves [[profiles::suppress]] from + // the field and its lexical parents. This does not depend on a parse-time + // suppress scope still being active (the late-parsed NSDMI finishes parsing + // before this finalization runs). checkInitProfileUninitWithInitializer(FD->getLocation(), FD->getDeclName(), FD->getType(), FD->getInClassInitializer(), - FD->hasAttr()); + FD->hasAttr(), + FD); // std::init / ref_to_uninit (paper §5): a pointer or reference data member // with a default member initializer must be bound consistently with its @@ -4243,7 +4243,8 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, if (QualType FT = FD->getType(); !FT->isDependentType() && (FT->isPointerType() || FT->isReferenceType())) checkRefToUninitInit(FD->getLocation(), FD->hasAttr(), - FT->isReferenceType(), FD->getInClassInitializer()); + FT->isReferenceType(), FD->getInClassInitializer(), + FD); } /// Find the direct and/or virtual base specifiers that diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp index 408cddf1f4271..f70965b0199b4 100644 --- a/clang/test/SemaCXX/safety-profile-init-static.cpp +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -56,3 +56,10 @@ int g_suppressed_all = runtime(); // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::other)]] int g_wrong_suppress = runtime(); // expected-error {{non-local variable 'g_wrong_suppress' requires constant initialization under profile 'std::init'}} + +// A suppress on the enclosing namespace covers its variables (found by the +// declaration's lexical-parent walk). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +namespace [[profiles::suppress(std::init)]] suppressed_ns { +int n_ok = runtime(); // OK: suppressed by the namespace-level attribute +} From 95fef4bfde006f37ae5b1ff1f399f54faab4e057 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 13:42:07 -0400 Subject: [PATCH 095/289] Consolidate finalization profile dispatch onto decl-aware suppression The class- and constructor-finalization callbacks now pass their node to the Decl-aware shouldEmitProfileViolation overload, which resolves [[profiles::suppress]] from the node and its lexical parents. The dispatcher no longer needs to push a ProfileSuppressScope, so that scope is removed. --- clang/lib/Sema/SemaDeclCXX.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index c41999c93d17c..1462ef7cd4f26 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7059,7 +7059,7 @@ template struct FinalizationProfile { void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { if (!S.shouldEmitProfileViolation("test::class_final", /*Rule=*/"", - RD->getLocation())) + RD->getLocation(), RD)) return; S.Diag(RD->getLocation(), diag::err_profile_class_final_test) << "test::class_final" << RD; @@ -7067,7 +7067,7 @@ void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { if (!S.shouldEmitProfileViolation("test::ctor_final", /*Rule=*/"", - Ctor->getLocation())) + Ctor->getLocation(), Ctor)) return; S.Diag(Ctor->getLocation(), diag::err_profile_ctor_final_test) << "test::ctor_final" << Ctor->getParent(); @@ -7101,7 +7101,7 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { if (!S.defaultInitLeavesScalarIndeterminate(F->getType())) continue; if (!S.shouldEmitProfileViolation("std::init", "ctor_uninit_member", - Ctor->getLocation())) + Ctor->getLocation(), Ctor)) continue; S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) << "std::init" << F->getDeclName(); @@ -7122,22 +7122,20 @@ constexpr FinalizationProfile {"std::init", &runStdInitCtorUninitMemberCallback}, }; -// Run the enforced finalization-profile callbacks in Table for D, setting up a -// suppress scope so [[profiles::suppress]] on D or a lexical parent is honored. -// Merges the former per-node dispatchers; the per-node filter (dependent, -// lambda, delegating, ...) stays at each call site. The table is taken by -// reference-to-array, not ArrayRef: deducing Node from a C array against an -// ArrayRef> parameter is not possible (no -// array-to-ArrayRef conversion happens during template argument deduction). +// Run the enforced finalization-profile callbacks in Table for D. Merges the +// former per-node dispatchers; the per-node filter (dependent, lambda, +// delegating, ...) stays at each call site. Each callback passes D to the +// Decl-aware Sema::shouldEmitProfileViolation, which honors [[profiles::suppress]] +// on D or a lexical parent, so the dispatcher needs no suppress scope of its +// own. The table is taken by reference-to-array, not ArrayRef: deducing Node +// from a C array against an ArrayRef> parameter is not +// possible (no array-to-ArrayRef conversion happens during template argument +// deduction). template void dispatchFinalizationProfiles(Sema &S, Node *D, const FinalizationProfile (&Table)[N]) { if (!S.anyProfileEnforced(Table)) return; - // ProfileSuppressScope is profile-agnostic (it pushes every - // [[profiles::suppress]] entry it finds and isProfileSuppressed filters by - // name later), so set it up once for all callbacks. - Sema::ProfileSuppressScope Scope(S, D, /*WalkLexicalParents=*/true); for (const auto &E : Table) if (S.isProfileEnforced(E.Name)) E.Callback(S, D); From 6f947a4321334f882b749a7ffe132e80642105bb Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 14:30:58 -0400 Subject: [PATCH 096/289] Add init-profile attributes to the supported-attributes test list The [[uninitialized]] (CXX11Uninitialized) and [[ref_to_uninit]] (RefToUninit) marker attributes are subject-bearing InheritableAttrs, so clang-tblgen emits them in the #pragma clang attribute supported-attributes list, but the test's expected CHECK list was never updated. Add the two missing entries at the generator's positions. --- clang/test/Misc/pragma-attribute-supported-attributes-list.test | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test index 03b9a77ec1814..649447e800ddb 100644 --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -47,6 +47,7 @@ // CHECK-NEXT: CUDANoCluster (SubjectMatchRule_objc_method, SubjectMatchRule_hasType_functionType) // CHECK-NEXT: CUDAShared (SubjectMatchRule_variable) // CHECK-NEXT: CXX11NoReturn (SubjectMatchRule_function) +// CHECK-NEXT: CXX11Uninitialized (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: CallableWhen (SubjectMatchRule_function_is_member) // CHECK-NEXT: Callback (SubjectMatchRule_function) // CHECK-NEXT: CalledOnce (SubjectMatchRule_variable_is_parameter) @@ -190,6 +191,7 @@ // CHECK-NEXT: RandomizeLayout (SubjectMatchRule_record) // CHECK-NEXT: ReadOnlyPlacement (SubjectMatchRule_record) // CHECK-NEXT: ReentrantCapability (SubjectMatchRule_record, SubjectMatchRule_type_alias) +// CHECK-NEXT: RefToUninit (SubjectMatchRule_variable, SubjectMatchRule_field, SubjectMatchRule_function) // CHECK-NEXT: ReleaseHandle (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: ReqdWorkGroupSize (SubjectMatchRule_function) // CHECK-NEXT: Restrict (SubjectMatchRule_function) From 2ae24906d11de1bb257cbcf47285ef1223589131 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 14:58:05 -0400 Subject: [PATCH 097/289] Recognize a [[ref_to_uninit]] reference as a source of uninitialized memory glvalueDenotesUninitStorage only treated a [[uninitialized]] entity as denoting uninitialized storage, so binding or passing a [[ref_to_uninit]] reference (whose referent is uninitialized) was wrongly rejected -- e.g. 'int &r2 [[ref_to_uninit]] = r1;' where r1 is a [[ref_to_uninit]] reference -- even though the symmetric [[ref_to_uninit]] pointer-copy case is accepted. Treat a named reference marked [[ref_to_uninit]] as denoting uninitialized storage (a marked pointer named as a glvalue still does not, since that is the initialized pointer object). --- clang/lib/Sema/SemaDecl.cpp | 13 ++++++++++--- .../SemaCXX/safety-profile-init-ref-to-uninit.cpp | 13 ++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 6ccd04720e9c0..133cc9f1176f3 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14955,11 +14955,18 @@ static bool glvalueDenotesUninitStorage(const Expr *E) { return false; E = E->IgnoreParenImpCasts(); - // A [[uninitialized]] variable / data member, or a subobject of one. + // A named entity denotes uninitialized storage if it is [[uninitialized]], or + // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, + // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes + // the pointer object itself -- which is initialized -- so it does not count. + auto DeclDenotesUninit = [](const ValueDecl *VD) { + return VD->hasAttr() || + (VD->getType()->isReferenceType() && VD->hasAttr()); + }; if (const auto *DRE = dyn_cast(E)) - return DRE->getDecl()->hasAttr(); + return DeclDenotesUninit(DRE->getDecl()); if (const auto *ME = dyn_cast(E)) - return ME->getMemberDecl()->hasAttr() || + return DeclDenotesUninit(ME->getMemberDecl()) || glvalueDenotesUninitStorage(ME->getBase()); if (const auto *ASE = dyn_cast(E)) return pointerRefersToUninitStorage(ASE->getBase()); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 78b55ca6e92bb..15e766766688d 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -40,7 +40,11 @@ void test_reference_target() { int &r4 = g_init; // OK int *p [[ref_to_uninit]] = &g_uninit; int &r5 [[ref_to_uninit]] = *p; // OK: *p denotes uninitialized storage - (void)r1; (void)r2; (void)r3; (void)r4; (void)r5; (void)p; + // A [[ref_to_uninit]] reference is itself a source of uninitialized storage, + // symmetric to the [[ref_to_uninit]] pointer-copy case. + int &r6 [[ref_to_uninit]] = r1; // OK: r1 refers to uninitialized memory + int &r7 = r1; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)r1; (void)r2; (void)r3; (void)r4; (void)r5; (void)r6; (void)r7; (void)p; } void test_assignment() { @@ -82,6 +86,13 @@ void test_call_arguments() { take_ref(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} take_ref(g_init); // OK + // A [[ref_to_uninit]] reference argument matches a [[ref_to_uninit]] reference + // parameter, and is rejected for an unmarked one. + int &ru [[ref_to_uninit]] = g_uninit; + take_uninit_ref(ru); // OK + take_ref(ru); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)ru; + // The worked example from paper §5. int a1[] = {1, 2, 3}; [[uninitialized]] int a2[3]; From aeaa8c392c267664f1db06cf55e1119230be437a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 16:01:32 -0400 Subject: [PATCH 098/289] Thread a marker-honoring flag through defaultInitLeavesScalarIndeterminate Add a HonorUninitMarkers parameter (default false) to defaultInitLeavesScalarIndeterminate and its recursive helper, and skip a data member marked [[uninitialized]] in the member walk when it is set. With the default, every caller's behavior is unchanged; this only plumbs the knob that the uninit_decl and ctor_uninit_member rules will opt into so a type whose only indeterminate scalars are all marked is reported as determinate (paper 6.2). No functional change. --- clang/include/clang/Sema/Sema.h | 19 ++++++++++++++----- clang/lib/Sema/SemaDecl.cpp | 26 ++++++++++++++++++-------- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 3adb4523360b3..d400017253698 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1109,11 +1109,20 @@ class Sema final : public SemaBase { /// True if default-initialization of \p T would leave at least one scalar /// subobject with an indeterminate value. Shared by the std::init rules - /// uninit_decl (R5, at the variable declaration) and ctor_uninit_member - /// (R6, for a class-typed member). A class with a user-provided default - /// constructor is trusted (that constructor is checked by R6 at its own - /// definition). Dependent and incomplete types are treated as determinate. - bool defaultInitLeavesScalarIndeterminate(QualType T); + /// uninit_decl (at the variable declaration), ctor_uninit_member (for a + /// class-typed member), and uninit_with_initializer. A class with a + /// user-provided default constructor is trusted (that constructor is checked + /// at its own definition). Dependent and incomplete types are treated as + /// determinate. + /// + /// When \p HonorUninitMarkers is true, a data member marked [[uninitialized]] + /// is treated as acknowledged and skipped, so a type whose only indeterminate + /// scalars are all marked is reported as determinate. uninit_decl and + /// ctor_uninit_member pass true (the marker excuses the member, paper §6.2); + /// uninit_with_initializer passes false because it needs the factual answer + /// (whether the default-initialization is genuinely a no-op). + bool defaultInitLeavesScalarIndeterminate(QualType T, + bool HonorUninitMarkers = false); /// std::init / ref_to_uninit (paper §5): true if \p E refers to (for a /// pointer source) or, when \p IsReference, denotes (for a glvalue source) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 133cc9f1176f3..c9c4049d9a24c 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14843,13 +14843,13 @@ void Sema::addLifetimeBoundToImplicitThis(CXXMethodDecl *MD) { } static bool defaultInitLeavesScalarIndeterminateImpl( - ASTContext &Ctx, QualType T, + ASTContext &Ctx, QualType T, bool HonorUninitMarkers, llvm::SmallPtrSetImpl &Visited) { if (T->isDependentType() || T->isIncompleteType()) return false; if (const ArrayType *AT = Ctx.getAsArrayType(T)) - return defaultInitLeavesScalarIndeterminateImpl(Ctx, AT->getElementType(), - Visited); + return defaultInitLeavesScalarIndeterminateImpl( + Ctx, AT->getElementType(), HonorUninitMarkers, Visited); if (T->isReferenceType()) return false; const auto *RD = T->getAsCXXRecordDecl(); @@ -14863,24 +14863,34 @@ static bool defaultInitLeavesScalarIndeterminateImpl( // Break cycles from ill-formed self-containing types (e.g. struct S { S x; }). if (!Visited.insert(RD->getCanonicalDecl()).second) return false; - // Trust a user-provided default constructor: R6 checks at its definition. + // Trust a user-provided default constructor: ctor_uninit_member checks at its + // definition. if (RD->hasUserProvidedDefaultConstructor()) return false; for (const CXXBaseSpecifier &Base : RD->bases()) - if (defaultInitLeavesScalarIndeterminateImpl(Ctx, Base.getType(), Visited)) + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, Base.getType(), + HonorUninitMarkers, Visited)) return true; for (const FieldDecl *F : RD->fields()) { if (F->isUnnamedBitField() || F->hasInClassInitializer()) continue; - if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), Visited)) + // A member the type's author marked [[uninitialized]] is acknowledged as + // intentionally uninitialized, so it does not leave an unacknowledged + // scalar indeterminate (paper §6.2). + if (HonorUninitMarkers && F->hasAttr()) + continue; + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), + HonorUninitMarkers, Visited)) return true; } return false; } -bool Sema::defaultInitLeavesScalarIndeterminate(QualType T) { +bool Sema::defaultInitLeavesScalarIndeterminate(QualType T, + bool HonorUninitMarkers) { llvm::SmallPtrSet Visited; - return defaultInitLeavesScalarIndeterminateImpl(Context, T, Visited); + return defaultInitLeavesScalarIndeterminateImpl(Context, T, + HonorUninitMarkers, Visited); } void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, From 37f3b77e7eecb8b012e35f541543c8e260ef8a8a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 16:01:51 -0400 Subject: [PATCH 099/289] Honor [[uninitialized]] members in the std::init uninit_decl rule The uninit_decl check at a variable definition now passes HonorUninitMarkers=true, so a record whose only indeterminate scalar subobjects are all marked [[uninitialized]] is trusted -- e.g. 'struct A { int x [[uninitialized]]; }; A a;' is accepted, matching the already-implemented direct-field excusal in ctor_uninit_member and the paper's 6.2 model. A mixed type still fires for its unmarked scalars. --- clang/lib/Sema/SemaDecl.cpp | 3 ++- .../test/SemaCXX/safety-profile-init-aggregate.cpp | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index c9c4049d9a24c..1cb639484015b 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14734,7 +14734,8 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { // provides an initializer, so the !getInit() test alone misses it). (!Var->getInit() || (Var->getType()->isRecordType() && - defaultInitLeavesScalarIndeterminate(Var->getType())))) { + defaultInitLeavesScalarIndeterminate( + Var->getType(), /*HonorUninitMarkers=*/true)))) { Diag(Var->getLocation(), diag::err_init_uninit_decl) << Profile << Var->getDeclName(); } diff --git a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp index 25c9149ae7f0f..0cfdf36299a87 100644 --- a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp +++ b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp @@ -10,6 +10,9 @@ struct WithCtor { WithCtor(); int x; }; struct PartlyInit { int x; struct Inner { Inner(); } s; }; struct Nested { Trivial t; }; struct WithBase : Trivial {}; +struct MarkedMember { int x [[uninitialized]]; }; +struct MixedMarked { int a [[uninitialized]]; int b; }; +struct NestedMarked { MarkedMember m; }; void test_aggregate() { Trivial a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} @@ -36,6 +39,16 @@ void test_trusted() { (void)a; (void)b; } +void test_marked_members() { + // A type whose only indeterminate scalars are [[uninitialized]] is trusted; + // those members are acknowledged uninitialized (paper §6.2), even through a + // nesting level. A mixed type still fires for its unmarked scalar. + MarkedMember a; + NestedMarked b; + MixedMarked c; // expected-error {{variable 'c' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + (void)a; (void)b; (void)c; +} + void test_suppress() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "uninit_decl")]] Trivial a; From 7d55ea1568b255db310b5012a080082179bdaab0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 16:02:02 -0400 Subject: [PATCH 100/289] Honor [[uninitialized]] members in the std::init ctor_uninit_member rule The nested-aggregate arm of ctor_uninit_member now passes HonorUninitMarkers=true, so a member whose type's indeterminate scalars are all themselves marked [[uninitialized]] no longer makes the enclosing constructor fire -- e.g. 'struct In { int x [[uninitialized]]; }; struct O { In m; O(){} };'. This makes the nested case consistent with the directly-marked-member case, which was already excused. A member whose type still leaves an unacknowledged scalar indeterminate continues to fire. --- clang/lib/Sema/SemaDeclCXX.cpp | 3 ++- clang/test/SemaCXX/safety-profile-init-ctor.cpp | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 1462ef7cd4f26..acc4d987b94ba 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7098,7 +7098,8 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { if (F->hasAttr() || F->hasInClassInitializer() || Written.count(F)) continue; - if (!S.defaultInitLeavesScalarIndeterminate(F->getType())) + if (!S.defaultInitLeavesScalarIndeterminate(F->getType(), + /*HonorUninitMarkers=*/true)) continue; if (!S.shouldEmitProfileViolation("std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 6ccb24e74a1d5..946c361e84464 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -6,6 +6,8 @@ struct WithCtor { WithCtor(); }; struct Inner { int y; }; +struct InnerMarked { int y [[uninitialized]]; }; +struct InnerMixed { int a [[uninitialized]]; int b; }; struct MissingMember { int x; // expected-note {{member 'x' declared here}} @@ -38,6 +40,19 @@ struct NestedAggregate { NestedAggregate() {} // expected-error {{constructor does not initialize member 'm' under profile 'std::init'}} }; +// A member whose type's only indeterminate scalar is [[uninitialized]] is +// acknowledged (paper §6.2), so the constructor need not initialize it. +struct NestedMarkedMember { + InnerMarked m; + NestedMarkedMember() {} +}; + +// A member whose type still leaves an unacknowledged scalar indeterminate fires. +struct NestedMixedMember { + InnerMixed m; // expected-note {{member 'm' declared here}} + NestedMixedMember() {} // expected-error {{constructor does not initialize member 'm' under profile 'std::init'}} +}; + struct TrustedMemberCtor { WithCtor m; TrustedMemberCtor() {} From fc191c9a2cee0b075cbe1dde3b8c608b2fb5c6ca Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 16 Jun 2026 16:02:11 -0400 Subject: [PATCH 101/289] Document std::init marked-member trust and guard uninit_with_initializer Update the R2/R5 documentation to describe the HonorUninitMarkers walk that skips acknowledged [[uninitialized]] members, and add a guard test ensuring uninit_with_initializer keeps the factual walk (HonorUninitMarkers=false): a marked-member aggregate left default-initialized is a genuine no-op, so 'A a [[uninitialized]];' must not be diagnosed as a marker/initializer contradiction. --- clang/docs/ProfilesFramework.rst | 22 ++++++++++++++----- .../safety-profile-init-with-initializer.cpp | 11 ++++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 538232dbbe035..417fb53b3aefe 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -703,9 +703,13 @@ constructor is trusted; static / thread storage duration is excluded with no initializer (so braced or value initialization such as ``S s = {1};`` and ``S s{};`` is unaffected -- omitted aggregate members are value-initialized). -- The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate``, - which recurses through bases and members, trusts user-provided default - constructors, and excludes unions. +- The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate`` + with ``HonorUninitMarkers=true``, which recurses through bases and members, + trusts user-provided default constructors, excludes unions, and skips data + members marked ``[[uninitialized]]`` (acknowledged uninitialized, paper + §6.2). So a type whose only indeterminate scalars are all marked is trusted + (e.g. ``struct A { int x [[uninitialized]]; }; A a;`` is accepted), while a + mixed type still fires for its unmarked scalars. R3. ``static_runtime_init`` -- pattern 1 ......................................... @@ -738,6 +742,11 @@ contradiction (the marker means "no initialization here"). that actually runs (e.g. ``WithCtor x [[uninitialized]];``), but *not* a no-op trivial/aggregate default-initialization, where the marker is consistent with the object being left uninitialized. +- Unlike R2/R5, this "no-op?" test calls + ``defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=false`` + (the *factual* answer): a type whose members are themselves marked still + default-initializes to a no-op, so the variable marker stays consistent and + the rule must not fire (e.g. ``A a [[uninitialized]];`` for the ``A`` above). R5. ``ctor_uninit_member`` -- pattern 4 ....................................... @@ -745,8 +754,11 @@ R5. ``ctor_uninit_member`` -- pattern 4 A user-provided constructor must initialize every non-static data member via its member-initializer list or an NSDMI, unless the member is marked ``[[uninitialized]]`` (paper §6.1). A plain assignment in the constructor -body does not count. A member whose own default-initialization leaves a -scalar subobject indeterminate (a nested aggregate) is flagged as well. +body does not count. A member whose own default-initialization leaves an +*unacknowledged* scalar subobject indeterminate (a nested aggregate) is +flagged as well; a member whose type's indeterminate scalars are all +themselves marked ``[[uninitialized]]`` is trusted (the same +``HonorUninitMarkers`` walk as R2, paper §6.2). - Diagnostic: ``err_init_ctor_uninit_member`` (with a ``note_init_uninit_member_here`` note at the member). diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index 5d8dbb0fb68a7..ea36f7ce356fe 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -44,6 +44,17 @@ void test_class_synthesized_init() { (void)x; (void)y; } +struct MarkedMemberAgg { int x [[uninitialized]]; }; + +void test_marked_member_agg() { + // R4 stays factual: MarkedMemberAgg's default-initialization is a genuine + // no-op that leaves x indeterminate, so marking the variable too is + // consistent rather than a contradiction. (Honoring the member marker here + // would wrongly fire uninit_with_initializer.) + MarkedMemberAgg a [[uninitialized]]; + (void)a; +} + struct NoDefaultCtor { NoDefaultCtor() = delete; }; // expected-note {{'NoDefaultCtor' has been explicitly marked deleted here}} \ // no-profiles-note {{'NoDefaultCtor' has been explicitly marked deleted here}} From 55604adb60f99d53f4b589859ae279081e0a9208 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 19 Jun 2026 13:24:40 -0400 Subject: [PATCH 102/289] Keep CFG-uninit profiles diagnosing after the first TU error The per-function analysis-based-warnings pass stops once the TU has an uncompilable error: ActOnFinishFunctionBody leaves ActivePolicy null, so IssueWarnings is never invoked for any later function. That silently disabled the profiles that ride the uninitialized-variables analysis (std::init's uninit_read and test::uninit_read) for every function after the first error, so a single earlier error -- profile or otherwise -- made the rest of the TU pass unchecked. When such a profile is enforced, still run the per-function pass for a valid body after an error, but restrict it to profile diagnostics via a ProfileOnly reporter so ordinary -Wuninitialized warnings stay suppressed as before. The new path is gated behind -fprofiles plus an enforced CFG-uninit profile, leaving all other compilations unchanged. Consolidate the two read tests into single multi-violation TUs with a leading unrelated error, replacing the per-violation -DCASE workaround that previously hid the bug. --- .../clang/Sema/AnalysisBasedWarnings.h | 6 ++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 61 +++++++++++++++- clang/lib/Sema/SemaDecl.cpp | 13 +++- .../test/SemaCXX/safety-profile-init-read.cpp | 73 ++++++++----------- .../SemaCXX/safety-profile-uninit-read.cpp | 42 ++++------- 5 files changed, 120 insertions(+), 75 deletions(-) diff --git a/clang/include/clang/Sema/AnalysisBasedWarnings.h b/clang/include/clang/Sema/AnalysisBasedWarnings.h index 0ed61e56825be..ff59eab8618d0 100644 --- a/clang/include/clang/Sema/AnalysisBasedWarnings.h +++ b/clang/include/clang/Sema/AnalysisBasedWarnings.h @@ -117,6 +117,12 @@ class AnalysisBasedWarnings { // Issue warnings that require whole-translation-unit analysis. void IssueWarnings(TranslationUnitDecl *D); + /// True if a profile that rides the uninitialized-variables analysis (see the + /// CFGUninitProfiles table) is enforced. Lets the per-function dispatch keep + /// running that analysis after a TU error so these profiles still diagnose + /// every later function instead of silently stopping at the first error. + bool hasEnforcedCFGUninitProfile() const; + void registerVarDeclWarning(VarDecl *VD, PossiblyUnreachableDiag PUD); void issueWarningsForRegisteredVarDecl(VarDecl *VD); diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index e11317c86af78..f7762a01c6c85 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1704,6 +1704,11 @@ constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; AnalysisDeclContext &AC; + // When set, only the CFGUninitProfiles diagnostics are emitted; the default + // -Wuninitialized reports (self-init and the sorted const-ref/ptr/use paths) + // are skipped. The post-error profile pass sets this so it cannot resurrect + // ordinary warnings that the first TU error is meant to suppress. + bool ProfileOnly; typedef SmallVector UsesVec; typedef llvm::PointerIntPair MappedType; // Prefer using MapVector to DenseMap, so that iteration order will be @@ -1713,7 +1718,9 @@ class UninitValsDiagReporter : public UninitVariablesHandler { UsesMap uses; public: - UninitValsDiagReporter(Sema &S, AnalysisDeclContext &AC) : S(S), AC(AC) {} + UninitValsDiagReporter(Sema &S, AnalysisDeclContext &AC, + bool ProfileOnly = false) + : S(S), AC(AC), ProfileOnly(ProfileOnly) {} ~UninitValsDiagReporter() override { flushDiagnostics(); } MappedType &getUses(const VarDecl *vd) { @@ -1776,6 +1783,11 @@ class UninitValsDiagReporter : public UninitVariablesHandler { } } + // The post-error pass runs purely to keep CFG-uninit profiles diagnosing; + // it must never fall through to the default -Wuninitialized reports. + if (ProfileOnly) + return; + // Specially handle the case where we have uses of an uninitialized // variable, but the root cause is an idiomatic self-init. We want // to report the diagnostic at the self-init since that is the root cause. @@ -2779,6 +2791,10 @@ void sema::AnalysisBasedWarnings::clearOverrides() { PolicyOverrides.enableThreadSafetyAnalysis = false; } +bool sema::AnalysisBasedWarnings::hasEnforcedCFGUninitProfile() const { + return S.anyProfileEnforced(CFGUninitProfiles); +} + static void flushDiagnostics(Sema &S, const sema::FunctionScopeInfo *fscope) { for (const auto &D : fscope->PossiblyUnreachableDiags) S.Diag(D.Loc, D.PD); @@ -2848,6 +2864,41 @@ void sema::AnalysisBasedWarnings::issueWarningsForRegisteredVarDecl( S, AC, std::make_pair(SecondRange.begin(), SecondRange.end())); } +// Pattern-2 profiles (the CFGUninitProfiles table) ride the uninitialized- +// variables analysis, which IssueWarnings otherwise skips once the TU has an +// uncompilable error. Re-run just that analysis for a single function so an +// early TU error does not silently disable the profile for every later +// function. Diagnostics are restricted to the profile via the ProfileOnly +// reporter, and the CFG build options mirror the non-linearized configuration +// of the main pass so the analysis sees the same CFG. +static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { + AnalysisDeclContext AC(/*Mgr=*/nullptr, D); + + AC.getCFGBuildOptions().PruneTriviallyFalseEdges = true; + AC.getCFGBuildOptions().AddEHEdges = false; + AC.getCFGBuildOptions().AddInitializers = true; + AC.getCFGBuildOptions().AddImplicitDtors = true; + AC.getCFGBuildOptions().AddParameterLifetimes = true; + AC.getCFGBuildOptions().AddTemporaryDtors = true; + AC.getCFGBuildOptions().AddCXXNewAllocator = false; + AC.getCFGBuildOptions().AddCXXDefaultInitExprInCtors = true; + AC.getCFGBuildOptions() + .setAlwaysAdd(Stmt::BinaryOperatorClass) + .setAlwaysAdd(Stmt::CompoundAssignOperatorClass) + .setAlwaysAdd(Stmt::BlockExprClass) + .setAlwaysAdd(Stmt::CStyleCastExprClass) + .setAlwaysAdd(Stmt::DeclRefExprClass) + .setAlwaysAdd(Stmt::ImplicitCastExprClass) + .setAlwaysAdd(Stmt::UnaryOperatorClass); + + if (CFG *cfg = AC.getCFG()) { + UninitValsDiagReporter reporter(S, AC, /*ProfileOnly=*/true); + UninitVariablesAnalysisStats stats = {}; + runUninitializedVariablesAnalysis(*cast(D), *cfg, AC, reporter, + stats); + } +} + // An AST Visitor that calls a callback function on each callable DEFINITION // that is NOT in a dependent context: class CallableVisitor : public DynamicRecursiveASTVisitor { @@ -3212,6 +3263,14 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( if (S.hasUncompilableErrorOccurred()) { // Flush out any possibly unreachable diagnostics. flushDiagnostics(S, fscope); + // Pattern-2 profiles ride the uninitialized-variables analysis and must see + // every function even after an earlier TU error; otherwise the first error + // disables them for all later functions. Run only that analysis, only for + // an enforced profile, and only on a valid decl (so the CFG is buildable). + // Other analyses keep the early-out. + if (S.anyProfileEnforced(CFGUninitProfiles) && !D->isInvalidDecl() && + !Diags.hasFatalErrorOccurred()) + runUninitProfileAnalysisAfterError(S, D); return; } diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 1cb639484015b..4673871302863 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -17223,9 +17223,16 @@ Decl *Sema::ActOnFinishFunctionBody(Decl *dcl, Stmt *Body, bool IsInstantiation, getDiagnostics().getSuppressAllDiagnostics()) { DiscardCleanupsInEvaluationContext(); } - if (!hasUncompilableErrorOccurred() && !isa(dcl)) { - // Since the body is valid, issue any analysis-based warnings that are - // enabled. + if (!isa(dcl) && + (!hasUncompilableErrorOccurred() || + (!dcl->isInvalidDecl() && + AnalysisWarnings.hasEnforcedCFGUninitProfile()))) { + // Normally analysis-based warnings only run for a valid body in an + // otherwise error-free TU. CFG-based profiles (e.g. std::init's + // uninit_read) must keep diagnosing later functions even after an + // earlier TU error, so still run the per-function pass for a valid body + // when such a profile is enforced; IssueWarnings restricts that + // post-error pass to profile diagnostics. ActivePolicy = &WP; } diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index 8011a91a1c9d4..f5e05aae4162a 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -1,31 +1,26 @@ -// Each CASE selects one violation test so the analysis-based-warnings early -// exit on first error doesn't hide later cases. -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=0 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=1 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=2 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=3 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=4 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=5 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=6 %s -// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized -DCASE=0 %s - -#if CASE == 0 -// expected-no-diagnostics -#endif +// All violations share one TU with a leading unrelated error: the early error +// disables the analysis-based-warnings pass for later functions, so this also +// verifies that an enforced CFG-uninit profile keeps diagnosing afterwards. +// The DEMOTE run additionally enforces test::uninit_read to exercise profile +// table ordering, which would otherwise change the std::init-only diagnostics. +// RUN: %clang_cc1 -fsyntax-only -verify=expected,common -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=demote,common -fprofiles -std=c++23 -Wno-uninitialized -DDEMOTE %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles,common -std=c++23 -Wno-uninitialized %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(std::init)]]; // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::other)]]; -#if CASE == 4 +#ifdef DEMOTE [[profiles::enforce(test::uninit_read)]]; #endif -// Cases that never diagnose are always compiled. +int leading_unrelated_error = undeclared_identifier; +// common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} -// CASE=4 also enforces test::uninit_read; the always-compiled suppress tests -// suppress both so the function-under-test demonstrates std::init behavior in -// isolation. +// The always-compiled suppress tests suppress both std::init and +// test::uninit_read so the function-under-test demonstrates std::init behavior +// in isolation under either run. // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] @@ -77,17 +72,16 @@ void test_param(int p) { (void)y; } -#if CASE == 1 +#ifndef DEMOTE void test_marker_does_not_excuse_read() { int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; } -#endif -#if CASE == 2 void test_suppress_stmt_outer() { int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] { int y = x; (void)y; @@ -95,9 +89,7 @@ void test_suppress_stmt_outer() { int z = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)z; } -#endif -#if CASE == 3 template T template_uninit() { T x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} @@ -106,36 +98,33 @@ T template_uninit() { void instantiate_template_uninit() { template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} } -#endif -#if CASE == 4 -// When both test::uninit_read and std::init are enforced (the conditional -// enforce above adds test::uninit_read for this case), table order in -// CFGUninitProfiles makes test::uninit_read fire first. Suppressing it at -// the use site lets the std::init diagnostic surface. -void test_demote_test_profile() { - int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} - [[profiles::suppress(test::uninit_read)]] { - int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} - (void)y; - } -} -#endif - -#if CASE == 5 void test_selective_suppress() { int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::other)]] { int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; } } -#endif -#if CASE == 6 void test_decl_suppress_does_not_extend() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; } #endif + +#ifdef DEMOTE +// With both test::uninit_read and std::init enforced, table order makes +// test::uninit_read fire first; suppressing it at the use site lets the +// std::init diagnostic surface. +void test_demote_test_profile() { + int x [[uninitialized]]; // demote-note {{variable 'x' is declared here}} + [[profiles::suppress(test::uninit_read)]] { + int y = x; // demote-error {{variable 'x' is read before initialization under profile 'std::init'}} + (void)y; + } +} +#endif diff --git a/clang/test/SemaCXX/safety-profile-uninit-read.cpp b/clang/test/SemaCXX/safety-profile-uninit-read.cpp index cc5ac810985c4..527eee8a1190b 100644 --- a/clang/test/SemaCXX/safety-profile-uninit-read.cpp +++ b/clang/test/SemaCXX/safety-profile-uninit-read.cpp @@ -1,24 +1,17 @@ -// Each CASE selects one violation test so the analysis-based-warnings early -// exit on first error doesn't hide later cases. -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=0 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=1 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=2 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=3 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=4 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=5 %s -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DCASE=6 %s -// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized -DCASE=0 %s - -#if CASE == 0 -// expected-no-diagnostics -#endif +// All violations share one TU with a leading unrelated error: the early error +// disables the analysis-based-warnings pass for later functions, so this also +// verifies that an enforced CFG-uninit profile keeps diagnosing afterwards. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::uninit_read)]]; // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::other)]]; -// Cases that never diagnose are always compiled. +int leading_unrelated_error = undeclared_identifier; +// expected-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} +// no-profiles-error@-2 {{use of undeclared identifier 'undeclared_identifier'}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::uninit_read)]] @@ -81,17 +74,15 @@ void test_suppress_self_init() { (void)&x; } -#if CASE == 1 void test_violation() { int x; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} (void)y; } -#endif -#if CASE == 2 void test_suppress_stmt_outer() { int x; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::uninit_read)]] { int y = x; (void)y; @@ -99,9 +90,7 @@ void test_suppress_stmt_outer() { int z = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} (void)z; } -#endif -#if CASE == 3 template T template_uninit() { T x; // expected-note {{variable 'x' is declared here}} @@ -110,32 +99,27 @@ T template_uninit() { void instantiate_template_uninit() { template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} } -#endif -#if CASE == 4 void test_self_init_with_use() { int x = x; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} (void)y; } -#endif -#if CASE == 5 void test_selective_suppress() { int x; // expected-note {{variable 'x' is declared here}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::other)]] { int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} (void)y; } } -#endif -#if CASE == 6 -// Suppress on a declaration is token-based: it covers the initializer of -// that declaration but not later uses that live in different declarations. +// Suppress on a declaration is token-based: it covers the initializer of that +// declaration but not later uses that live in different declarations. void test_decl_suppress_does_not_extend() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::uninit_read)]] int x; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} (void)y; } -#endif From 2b51a3c0cf6c127798f375b6bc0009f185749709 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 19 Jun 2026 14:04:23 -0400 Subject: [PATCH 103/289] Dereference the base of an arrow member access in the ref_to_uninit recognizer glvalueDenotesUninitStorage treated a MemberExpr base as a glvalue regardless of access kind, so member access through a [[ref_to_uninit]] pointer (a->m) inspected the pointer object instead of its pointee and was not recognized as uninitialized storage -- letting &a->m flow into an unmarked pointer, parameter, or return while the equivalent &(*a).m was correctly diagnosed. Check the arrow base with pointerRefersToUninitStorage, mirroring the UO_Deref arm. --- clang/lib/Sema/SemaDecl.cpp | 5 ++++- .../SemaCXX/safety-profile-init-ref-to-uninit.cpp | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 4673871302863..b8b2a4aec6584 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14977,8 +14977,11 @@ static bool glvalueDenotesUninitStorage(const Expr *E) { if (const auto *DRE = dyn_cast(E)) return DeclDenotesUninit(DRE->getDecl()); if (const auto *ME = dyn_cast(E)) + // a->m reaches m through the pointer a (object *a); a.m through the + // glvalue a. return DeclDenotesUninit(ME->getMemberDecl()) || - glvalueDenotesUninitStorage(ME->getBase()); + (ME->isArrow() ? pointerRefersToUninitStorage(ME->getBase()) + : glvalueDenotesUninitStorage(ME->getBase())); if (const auto *ASE = dyn_cast(E)) return pointerRefersToUninitStorage(ASE->getBase()); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 15e766766688d..ba6be1373c5c3 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -103,6 +103,18 @@ void test_call_arguments() { [[profiles::suppress(std::init)]] { take_ptr(&g_uninit); } // OK: suppressed } +struct Inner { int m; }; + +// Member access through a [[ref_to_uninit]] pointer denotes uninitialized +// storage. Arrow access (a->m, object *a) and explicit deref ((*a).m) must +// behave identically. +void test_member_through_pointer(Inner *ptr [[ref_to_uninit]]) { + int *q1 = &ptr->m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q2 = &(*ptr).m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q3 [[ref_to_uninit]] = &ptr->m; // OK + (void)q1; (void)q2; (void)q3; +} + struct WithFields { int *p1 [[ref_to_uninit]] = &g_uninit; // OK int *p2 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} From 71023d5f4794588553ca141dc70bda5b8518bfab Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 19 Jun 2026 14:34:20 -0400 Subject: [PATCH 104/289] Correct std::init profile docs and document known gaps --- clang/docs/ProfilesFramework.rst | 49 +++++++++++++++++++++----------- clang/lib/Sema/SemaDeclCXX.cpp | 6 ++-- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 417fb53b3aefe..ec9bfbc2d5a9c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -290,14 +290,14 @@ This pattern needs two pieces, both colocated with the dispatcher. }; The shared ``dispatchFinalizationProfiles`` dispatcher (used by both - patterns 3 and 4) checks ``anyProfileEnforced(Table)``, sets up a - ``ProfileSuppressScope(S, RD, /*WalkLexicalParents=*/true)``, iterates the + patterns 3 and 4) checks ``anyProfileEnforced(Table)``, iterates the table, skips entries whose profile is not enforced, and invokes the - callback. Because the - suppress scope is established by the dispatcher, the callback can use - the location-based ``shouldEmitProfileViolation`` overload and have - ``[[profiles::suppress]]`` on the class or any enclosing lexical - ``Decl`` work correctly. + callback. Each callback passes the finalized ``Decl`` (here the + ``CXXRecordDecl``) to the decl-aware ``shouldEmitProfileViolation`` + overload, which walks the declaration and its lexical parents for a + matching ``[[profiles::suppress]]``, so suppression on the class or any + enclosing lexical ``Decl`` works without the dispatcher establishing a + suppress scope. 2. **Emit diagnostics from the callback via** ``Sema::shouldEmitProfileViolation``. Each callback decides where on @@ -362,11 +362,12 @@ table ``ConstructorFinalizationProfiles`` of the same ``FinalizationProfile`` row (here ``FinalizationProfile``), and a callback that emits via ``Sema::shouldEmitProfileViolation``. The same shared -``dispatchFinalizationProfiles`` dispatcher establishes a -``ProfileSuppressScope(S, Ctor, /*WalkLexicalParents=*/true)`` around each -callback, so ``[[profiles::suppress]]`` on the constructor, the class, or an -enclosing lexical ``Decl`` works. A callback that should only apply to -user-written constructors checks ``Ctor->isUserProvided()``. +``dispatchFinalizationProfiles`` dispatcher invokes each callback, which +passes the ``CXXConstructorDecl`` to the decl-aware +``shouldEmitProfileViolation`` overload; that overload walks the declaration +and its lexical parents, so ``[[profiles::suppress]]`` on the constructor, +the class, or an enclosing lexical ``Decl`` works. A callback that should +only apply to user-written constructors checks ``Ctor->isUserProvided()``. .. _profiles-token-dominion: @@ -584,9 +585,9 @@ Because dependent classes are filtered out by the dispatcher, the diagnostic fires on class template *instantiations* rather than on the primary template. Lambda closures are also skipped. ``[[profiles::suppress(test::class_final)]]`` on the class or any -enclosing lexical ``Decl`` silences the diagnostic via the -``ProfileSuppressScope(*this, RD, /*WalkLexicalParents=*/true)`` the -dispatcher establishes around each callback. +enclosing lexical ``Decl`` silences the diagnostic via the decl-aware +``shouldEmitProfileViolation`` overload, which walks the class and its +lexical parents for a matching suppression. The ``test::ctor_final`` Profile @@ -684,6 +685,11 @@ the table-order priority makes ``test::uninit_read`` fire first. Use ``[[profiles::suppress(test::uninit_read)]]`` to demote it at a use site and surface the ``std::init`` diagnostic. +Known deviation: the initialization profile paper exempts ``std::byte`` from +the uninitialized-read rule, but this slice rides the generic CFG +uninitialized-variables analysis, which does not special-case ``std::byte``, +so a read of an uninitialized ``std::byte`` is currently diagnosed. + R2. ``uninit_decl`` -- pattern 1 ................................. @@ -710,6 +716,11 @@ constructor is trusted; static / thread storage duration is excluded §6.2). So a type whose only indeterminate scalars are all marked is trusted (e.g. ``struct A { int x [[uninitialized]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. +- Known gap: the aggregate branch guards on ``Var->getType()->isRecordType()``, + which does not look through array types, so an automatic array of a class + type that leaves scalar subobjects indeterminate (e.g. ``S arr[3];`` for + ``struct S { int x; };``) is not diagnosed, even though the scalar form + ``S one;`` is. R3. ``static_runtime_init`` -- pattern 1 ......................................... @@ -764,7 +775,13 @@ themselves marked ``[[uninitialized]]`` is trusted (the same ``note_init_uninit_member_here`` note at the member). - Opt-in table: ``ConstructorFinalizationProfiles`` (pattern 4). - Reference and const members keep their existing dedicated diagnostics; - anonymous-aggregate members and bit-fields are conservatively skipped. + anonymous-aggregate members and unnamed bit-fields are skipped (named + bit-fields are checked like any other member). +- Known gaps: base-class subobjects of the constructed class are not checked + -- only direct non-static data members -- so a user-provided constructor + that leaves a base subobject uninitialized (paper §6.1) is not diagnosed. + A const member is skipped here but is treated as indeterminate by + ``defaultInitLeavesScalarIndeterminate`` (R2). R6. ``union_marker`` -- attribute handler ......................................... diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index acc4d987b94ba..446a33652dc8b 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7089,9 +7089,9 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { Written.insert(F); for (const FieldDecl *F : Ctor->getParent()->fields()) { - // Anonymous aggregate members and bit-fields are conservatively skipped in - // this slice; reference and const members already have dedicated - // diagnostics when left uninitialized. + // Anonymous aggregate members and unnamed bit-fields are skipped; a named + // bit-field is checked like any other member. Reference and const members + // already have dedicated diagnostics when left uninitialized. if (F->isUnnamedBitField() || !F->getDeclName() || F->getType()->isReferenceType() || F->getType().isConstQualified()) continue; From 1abd7fcf5ceeb1f1c1c0fd8f62071ef954c0b8bd Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 19 Jun 2026 15:41:30 -0400 Subject: [PATCH 105/289] Gate built-in test:: profiles behind -fprofiles-test-profiles Keeps the framework's test-only profiles inert unless the test suite opts in. --- clang/docs/ProfilesFramework.rst | 20 +++++++++++++++++-- clang/include/clang/Basic/LangOptions.def | 1 + clang/include/clang/Options/Options.td | 4 ++++ clang/lib/Sema/Sema.cpp | 4 ++++ clang/test/PCH/cxx-profiles-enforce.cpp | 6 +++--- .../SemaCXX/safety-profile-class-final.cpp | 2 +- .../SemaCXX/safety-profile-ctor-final.cpp | 2 +- .../safety-profile-framework-modules.cppm | 8 ++++---- .../test/SemaCXX/safety-profile-framework.cpp | 2 +- .../test/SemaCXX/safety-profile-init-read.cpp | 2 +- .../test/SemaCXX/safety-profile-type-cast.cpp | 9 ++++++++- .../SemaCXX/safety-profile-uninit-read.cpp | 2 +- 12 files changed, 47 insertions(+), 15 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index ec9bfbc2d5a9c..9dc3b5171563c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -59,6 +59,16 @@ The framework's parse-time bookkeeping (``ProfileSuppressScope``, attribute custom parsing, etc.) is also no-ops when ``LangOpts.Profiles`` is false, so the flag is the single switch that turns the entire feature on or off. +The built-in ``test::`` profiles (see `Built-in Profiles`_) exist only to +exercise the framework and are additionally gated on the ``-fprofiles-test-profiles`` +flag, which sets ``LangOpts.ProfilesTestProfiles``. This flag is ``-cc1``-only +(not exposed by the driver) and is intended solely for running the test suite. +Under ``-fprofiles`` alone, ``[[profiles::enforce(test::...)]]`` is still +recognized (it is not ``warn_attribute_ignored``) and its designator is still +recorded and exported across modules, but ``Sema::isProfileEnforced`` reports +any ``test::``-prefixed profile as not enforced, so no ``test::`` rule ever +fires. Real profiles such as ``std::init`` are unaffected by this flag. + Attribute Reference =================== @@ -493,7 +503,10 @@ The following parts of P3589R2 are deliberately not implemented: Built-in Profiles ================= -The tree ships five built-in profiles, all gated on ``-fprofiles``: +The tree ships five built-in profiles, all gated on ``-fprofiles``. The four +``test::`` profiles are additionally gated on the ``-cc1``-only +``-fprofiles-test-profiles`` flag (see `Driver Flag`_) and are inert under +``-fprofiles`` alone; ``std::init`` needs only ``-fprofiles``: - ``test::type_cast`` (test-only) -- pattern-1 example. - ``test::uninit_read`` (test-only) -- pattern-2 example riding the existing @@ -511,7 +524,10 @@ By convention: - Real test profiles live under the ``test::`` namespace. Today there are four: ``test::type_cast``, ``test::uninit_read``, ``test::class_final``, - and ``test::ctor_final``. + and ``test::ctor_final``. Because the ``test::`` prefix is what + ``Sema::isProfileEnforced`` keys on to gate them behind + ``-fprofiles-test-profiles``, any new test-only profile must also live + under ``test::``. - The names ``test::other``, ``test::bounds``, ``test::new_profile``, and ``test::not_enforced`` are deliberately *not* implemented and appear only in negative tests as stand-in "some other profile" names. Adding a real diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def index 6e15a22ea272a..1b991275d6f7c 100644 --- a/clang/include/clang/Basic/LangOptions.def +++ b/clang/include/clang/Basic/LangOptions.def @@ -155,6 +155,7 @@ LANGOPT(MathErrno , 1, 1, NotCompatible, "errno in math functions") LANGOPT(Modules , 1, 0, NotCompatible, "modules semantics") LANGOPT(CPlusPlusModules , 1, 0, Compatible, "C++ modules syntax") LANGOPT(Profiles , 1, 0, NotCompatible, "C++ profiles framework") +LANGOPT(ProfilesTestProfiles, 1, 0, Benign, "C++ profiles framework built-in test:: profiles") LANGOPT(SkipODRCheckInGMF , 1, 0, NotCompatible, "Skip ODR checks for decls in the global module fragment") LANGOPT(BuiltinHeadersInSystemModules, 1, 0, NotCompatible, "builtin headers belong to system modules, and _Builtin_ modules are ignored for cstdlib headers") ENUM_LANGOPT(CompilingModule, CompilingModuleKind, 3, CMK_None, Benign, diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index d5c9714cb2f59..34ce80d8cdf8c 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -1701,6 +1701,10 @@ defm profiles : BoolFOption<"profiles", "Enable C++ profiles framework (P3589R2)">, NegFlag>, ShouldParseIf; +def fprofiles_test_profiles : Flag<["-"], "fprofiles-test-profiles">, + Group, Visibility<[CC1Option]>, + HelpText<"Enable the C++ profiles framework's built-in test:: profiles (test suite only)">, + MarshallingInfoFlag>; defm coro_aligned_allocation : BoolFOption<"coro-aligned-allocation", LangOpts<"CoroAlignedAllocation">, DefaultFalse, diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index a5d6040adff3d..39db56411ed52 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -2989,6 +2989,10 @@ Attr *Sema::CreateAnnotationAttr(const ParsedAttr &AL) { bool Sema::isProfileEnforced(StringRef ProfileName) const { if (!getLangOpts().Profiles) return false; + // The built-in test:: profiles only exercise the framework; keep them inert + // unless the test suite opts in via -fprofiles-test-profiles. + if (!getLangOpts().ProfilesTestProfiles && ProfileName.starts_with("test::")) + return false; return getProfileEnforcement(ProfileName) != nullptr; } diff --git a/clang/test/PCH/cxx-profiles-enforce.cpp b/clang/test/PCH/cxx-profiles-enforce.cpp index 73cd3eb14236c..b809aba39d2aa 100644 --- a/clang/test/PCH/cxx-profiles-enforce.cpp +++ b/clang/test/PCH/cxx-profiles-enforce.cpp @@ -1,9 +1,9 @@ // Test this without pch. -// RUN: %clang_cc1 %s -fprofiles -std=c++20 -fsyntax-only -include %s -verify +// RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include %s -verify // Test with pch. -// RUN: %clang_cc1 %s -fprofiles -std=c++20 -emit-pch -o %t -// RUN: %clang_cc1 %s -fprofiles -std=c++20 -fsyntax-only -include-pch %t -verify +// RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -emit-pch -o %t +// RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include-pch %t -verify #ifndef HEADER #define HEADER diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp index 126d1164242c8..7d1e3898c6bfa 100644 --- a/clang/test/SemaCXX/safety-profile-class-final.cpp +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -1,4 +1,4 @@ -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 %s // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} diff --git a/clang/test/SemaCXX/safety-profile-ctor-final.cpp b/clang/test/SemaCXX/safety-profile-ctor-final.cpp index 2e957eb8fcfec..4c6a0263f8682 100644 --- a/clang/test/SemaCXX/safety-profile-ctor-final.cpp +++ b/clang/test/SemaCXX/safety-profile-ctor-final.cpp @@ -1,4 +1,4 @@ -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 %s // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 6a1b5490fc04e..c46b4b665ac5a 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -7,7 +7,7 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_fail.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_mismatch.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_repeated.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify -// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/impl_propagation.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/impl_propagation.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_enforce.cppm -o %t/mod_gmf_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/require_gmf_ok.cpp -fmodule-file=GmfMod=%t/mod_gmf_enforce.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/mod_gmf_only_enforce.cppm -o %t/mod_gmf_only_enforce.pcm -verify @@ -15,9 +15,9 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-module-interface %t/part_iface.cppm -o %t/part_iface.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_ok.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_primary_require_fail.cppm -fmodule-file=PartMod:part=%t/part_iface.pcm -verify -// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_iface_violation.cppm -verify -// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_enforce.cppm -verify -// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_inherit.cppm -fmodule-file=%t/mod_enforced.pcm -Wno-eager-load-cxx-named-modules -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/part_iface_violation.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/part_impl_enforce.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/part_impl_inherit.cppm -fmodule-file=%t/mod_enforced.pcm -Wno-eager-load-cxx-named-modules -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/part_impl_no_inherit.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_no_local_enforce.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_gmf_only_no_leak.cpp -fmodule-file=GmfOnlyMod=%t/mod_gmf_only_enforce.pcm -verify diff --git a/clang/test/SemaCXX/safety-profile-framework.cpp b/clang/test/SemaCXX/safety-profile-framework.cpp index 68dc80e42dc34..c12c29a1a552a 100644 --- a/clang/test/SemaCXX/safety-profile-framework.cpp +++ b/clang/test/SemaCXX/safety-profile-framework.cpp @@ -1,4 +1,4 @@ -// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++20 %s +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -fprofiles-test-profiles -std=c++20 %s // =================================================================== // Enforce on empty-declaration at TU scope: OK diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index f5e05aae4162a..32b72d60c6868 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -4,7 +4,7 @@ // The DEMOTE run additionally enforces test::uninit_read to exercise profile // table ordering, which would otherwise change the std::init-only diagnostics. // RUN: %clang_cc1 -fsyntax-only -verify=expected,common -fprofiles -std=c++23 -Wno-uninitialized %s -// RUN: %clang_cc1 -fsyntax-only -verify=demote,common -fprofiles -std=c++23 -Wno-uninitialized -DDEMOTE %s +// RUN: %clang_cc1 -fsyntax-only -verify=demote,common -fprofiles -fprofiles-test-profiles -std=c++23 -Wno-uninitialized -DDEMOTE %s // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles,common -std=c++23 -Wno-uninitialized %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index f10701bba6ce1..6e4ab740c78b4 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -1,5 +1,12 @@ -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 %s // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-test -fprofiles -std=c++23 %s + +// Under -fprofiles without -fprofiles-test-profiles the built-in test:: profiles +// are inert: the attributes are recognized (no "ignored" warnings) but no rule +// fires anywhere in this file. +// no-test-no-diagnostics + // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} [[profiles::enforce(test::type_cast)]]; // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} diff --git a/clang/test/SemaCXX/safety-profile-uninit-read.cpp b/clang/test/SemaCXX/safety-profile-uninit-read.cpp index 527eee8a1190b..3c480057d77b3 100644 --- a/clang/test/SemaCXX/safety-profile-uninit-read.cpp +++ b/clang/test/SemaCXX/safety-profile-uninit-read.cpp @@ -1,7 +1,7 @@ // All violations share one TU with a leading unrelated error: the early error // disables the analysis-based-warnings pass for later functions, so this also // verifies that an enforced CFG-uninit profile keeps diagnosing afterwards. -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fprofiles-test-profiles -std=c++23 -Wno-uninitialized %s // RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized %s // no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} From ed83740c6d282a476ed3c16a49fd813f83686515 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 19 Jun 2026 17:26:28 -0400 Subject: [PATCH 106/289] Diagnose automatic arrays of class type under std::init uninit_decl The uninit_decl check guarded the recursive walk on Var->getType()->isRecordType(), which is false for an array type, so an automatic array of a class type that leaves a scalar subobject indeterminate (e.g. S arr[3]; for struct S { int x; };) escaped the rule even though the scalar form S one; was diagnosed. Walk the base element type so arrays of class type are covered (paper section 6); the helper already strips array extents internally. --- clang/docs/ProfilesFramework.rst | 5 ----- clang/lib/Sema/SemaDecl.cpp | 9 +++++---- .../SemaCXX/safety-profile-init-aggregate.cpp | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 9dc3b5171563c..1a3baae30a246 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -732,11 +732,6 @@ constructor is trusted; static / thread storage duration is excluded §6.2). So a type whose only indeterminate scalars are all marked is trusted (e.g. ``struct A { int x [[uninitialized]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. -- Known gap: the aggregate branch guards on ``Var->getType()->isRecordType()``, - which does not look through array types, so an automatic array of a class - type that leaves scalar subobjects indeterminate (e.g. ``S arr[3];`` for - ``struct S { int x; };``) is not diagnosed, even though the scalar form - ``S one;`` is. R3. ``static_runtime_init`` -- pattern 1 ......................................... diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index b8b2a4aec6584..6322203fbd686 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14729,11 +14729,12 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { !Var->hasAttr() && shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && // A definition with no initializer (scalar / pointer / enum, or an - // array of them), or a class/aggregate type whose default-init leaves - // a scalar subobject indeterminate (its synthesized constructor call - // provides an initializer, so the !getInit() test alone misses it). + // array of them), or a class/aggregate type -- possibly the element + // type of an array -- whose default-init leaves a scalar subobject + // indeterminate (its synthesized constructor call provides an + // initializer, so the !getInit() test alone misses it). (!Var->getInit() || - (Var->getType()->isRecordType() && + (Context.getBaseElementType(Var->getType())->isRecordType() && defaultInitLeavesScalarIndeterminate( Var->getType(), /*HonorUninitMarkers=*/true)))) { Diag(Var->getLocation(), diag::err_init_uninit_decl) diff --git a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp index 0cfdf36299a87..d521ebd7961e0 100644 --- a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp +++ b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp @@ -49,6 +49,21 @@ void test_marked_members() { (void)a; (void)b; (void)c; } +void test_arrays() { + // An automatic array of a class type whose default-init leaves a scalar + // subobject indeterminate is diagnosed via the base element type (paper + // section 6). + Trivial a[3]; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Trivial b[2][3]; // expected-error {{variable 'b' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + int c[5]; // expected-error {{variable 'c' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Trivial d[2] = {}; + Trivial e[2] = {{1}, {2}}; + [[uninitialized]] Trivial f[3]; + WithCtor g[3]; + AllInit h[3]; + (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g; (void)h; +} + void test_suppress() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "uninit_decl")]] Trivial a; From 16c24a480a7fe983e73dd1493c14488ff00f12e5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 19 Jun 2026 17:43:41 -0400 Subject: [PATCH 107/289] Diagnose uninitialized unions under std::init and exempt union constructors Two union defects: an uninitialized union object/member was silently accepted (the indeterminate-scalar helper returned false for all unions), and a union constructor false-positived ctor_uninit_member because the "initialize every member" rule was applied to a union whose members are mutually exclusive. Treat a union as indeterminate (paper section 6.5) unless it has no members, a user-provided default constructor, or a default member initializer, so an uninitialized union variable (err_init_uninit_union, a message that does not suggest the banned marker) and an uninitialized union data member (ctor_uninit_member) are diagnosed. Skip a union's own constructor in ctor_uninit_member. Retain the banned [[uninitialized]] marker on a union after diagnosing union_marker so the entity is not re-diagnosed by uninit_decl. --- clang/docs/ProfilesFramework.rst | 20 +++++++++++-- .../clang/Basic/DiagnosticSemaKinds.td | 2 ++ clang/lib/Sema/SemaDecl.cpp | 23 ++++++++++++--- clang/lib/Sema/SemaDeclAttr.cpp | 9 +++--- clang/lib/Sema/SemaDeclCXX.cpp | 6 ++++ .../SemaCXX/safety-profile-init-union.cpp | 29 +++++++++++++++++++ 6 files changed, 78 insertions(+), 11 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 1a3baae30a246..2536830948cde 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -727,9 +727,9 @@ constructor is trusted; static / thread storage duration is excluded are value-initialized). - The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=true``, which recurses through bases and members, - trusts user-provided default constructors, excludes unions, and skips data - members marked ``[[uninitialized]]`` (acknowledged uninitialized, paper - §6.2). So a type whose only indeterminate scalars are all marked is trusted + trusts user-provided default constructors, and skips data members marked + ``[[uninitialized]]`` (acknowledged uninitialized, paper §6.2). So a type + whose only indeterminate scalars are all marked is trusted (e.g. ``struct A { int x [[uninitialized]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. @@ -788,6 +788,10 @@ themselves marked ``[[uninitialized]]`` is trusted (the same - Reference and const members keep their existing dedicated diagnostics; anonymous-aggregate members and unnamed bit-fields are skipped (named bit-fields are checked like any other member). +- A union's own constructor is exempt from this rule -- its members are + mutually exclusive, so a constructor initializes at most one (paper §6.5; + see R6). A union *data member* of a non-union class is still checked, and + must be initialized via the member-initializer list. - Known gaps: base-class subobjects of the constructed class are not checked -- only direct non-static data members -- so a user-provided constructor that leaves a base subobject uninitialized (paper §6.1) is not diagnosed. @@ -807,6 +811,16 @@ assignment when compiled without the profile. structured-binding rejections, which are unconditional, this is gated on enforcement -- a union may legitimately carry the marker without the profile. +- The banned marker is retained on the declaration after it is diagnosed, so + the ``uninit_decl`` / ``ctor_uninit_member`` rules treat the entity as + acknowledged and do not emit a second, contradictory diagnostic. + +An *unmarked* union left uninitialized is itself the error (paper §6.5): +``Sema::defaultInitLeavesScalarIndeterminate`` reports a union as indeterminate +unless it has no members, a user-provided default constructor, or a default +member initializer. A uninitialized union variable is therefore diagnosed by +``uninit_decl`` (with the union-specific ``err_init_uninit_union``) and an +uninitialized union data member by ``ctor_uninit_member``. R7. ``ref_to_uninit`` -- pattern 1 .................................. diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index cbbb413f892a0..cf11778f957ed 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14148,6 +14148,8 @@ def err_init_uninit_read : ProfileRuleError< def err_init_uninit_decl : ProfileRuleError< "variable %1 must be initialized or marked '[[uninitialized]]' under " "profile '%0'">; +def err_init_uninit_union : ProfileRuleError< + "variable %1 of union type must be initialized under profile '%0'">; def err_init_static_runtime_init : ProfileRuleError< "non-local variable %1 requires constant initialization under " "profile '%0'">; diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 6322203fbd686..4aa4b15189370 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14737,7 +14737,12 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { (Context.getBaseElementType(Var->getType())->isRecordType() && defaultInitLeavesScalarIndeterminate( Var->getType(), /*HonorUninitMarkers=*/true)))) { - Diag(Var->getLocation(), diag::err_init_uninit_decl) + // A union variable cannot carry [[uninitialized]] (union_marker bans it), + // so it must be initialized; use a message that does not suggest the + // marker as a remedy. + bool IsUnion = Context.getBaseElementType(Var->getType())->isUnionType(); + Diag(Var->getLocation(), + IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) << Profile << Var->getDeclName(); } @@ -14858,10 +14863,20 @@ static bool defaultInitLeavesScalarIndeterminateImpl( if (!RD) // Scalars, pointers, and enums are left indeterminate by default-init. return T->isScalarType(); - // A union is handled by the union_marker rule, not here; its members are - // mutually exclusive so the member walk below does not apply. - if (RD->isUnion() || RD->isInvalidDecl()) + if (RD->isInvalidDecl()) return false; + // A union's members are mutually exclusive, so the per-member walk below does + // not apply. Default-initialization leaves it without an initialized member + // (paper §6.5) unless it has no members, has a user-provided default + // constructor (trusted), or a default member initializer initializes one. + if (RD->isUnion()) { + if (RD->field_empty() || RD->hasUserProvidedDefaultConstructor()) + return false; + for (const FieldDecl *F : RD->fields()) + if (F->hasInClassInitializer()) + return false; + return true; + } // Break cycles from ill-formed self-containing types (e.g. struct S { S x; }). if (!Visited.insert(RD->getCanonicalDecl()).second) return false; diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 17d490732a46f..f5cdb9d9f4bdc 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6975,12 +6975,13 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, bool UnionMember = isa(D) && cast(D)->getParent()->isUnion(); if ((UnionVar || UnionMember) && - S.shouldEmitProfileViolation("std::init", "union_marker", AL.getLoc())) { + S.shouldEmitProfileViolation("std::init", "union_marker", AL.getLoc())) + // Diagnose the banned placement but retain the marker (fall through to + // addAttr) so uninit_decl / ctor_uninit_member treat the entity as + // acknowledged and do not re-diagnose it with a second, contradictory + // error. S.Diag(AL.getLoc(), diag::err_init_union_marker) << "std::init" << (UnionMember ? 1 : 0); - AL.setInvalid(); - return; - } D->addAttr(::new (S.Context) CXX11UninitializedAttr(S.Context, AL)); } diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 446a33652dc8b..f5a03b72360b9 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7081,6 +7081,12 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { if (!Ctor->isUserProvided()) return; + // A union's members are mutually exclusive; a constructor initializes at most + // one, so the "every member" rule does not apply (paper §6.5). Whether the + // active member is set is a constructor-body flow question, deferred. + if (Ctor->getParent()->isUnion()) + return; + // Members given a written member-initializer by this constructor. llvm::SmallPtrSet Written; for (const CXXCtorInitializer *Init : Ctor->inits()) diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index 0452048caa804..9274804e4c11b 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -31,3 +31,32 @@ union MarkedMember { struct NotUnion { int x [[uninitialized]]; }; + +union WithNSDMI { int x = 0; float y; }; +// Defining a union constructor must not fire ctor_uninit_member for the other +// members (they are mutually exclusive). +union WithUserCtor { int x; float y; WithUserCtor() : x(0) {} }; + +void test_uninit_union_object() { + U a; // expected-error {{variable 'a' of union type must be initialized under profile 'std::init'}} + U b = {1}; + U c{}; + WithNSDMI d; // OK: a default member initializer initializes a member + WithUserCtor e; // OK: a user-provided default constructor is trusted + (void)a; (void)b; (void)c; (void)d; (void)e; +} + +void test_uninit_union_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] U a; + (void)a; +} + +// A union data member that a constructor leaves uninitialized is diagnosed; one +// initialized via its member-initializer is accepted. +struct HasUnionMember { + U u; // expected-note {{member 'u' declared here}} + int z; + HasUnionMember() : z(0) {} // expected-error {{constructor does not initialize member 'u' under profile 'std::init'}} + HasUnionMember(int) : u{1}, z(0) {} +}; From 9b1d2cefe72fb3b6c9252297e572e01458d5cecc Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 11:45:02 -0400 Subject: [PATCH 108/289] Reject [[uninitialized]] on pointers under std::init Paper section 4.1: "a reference cannot be uninitialized. The initialization profile requires the same for pointers." The handler already rejected the marker on a reference but accepted it on a pointer. Add a pointer_marker rule that diagnoses [[uninitialized]] on a pointer (variable or data member) and points the user at initializing it (e.g. to nullptr). Like union_marker, the check is gated on enforcement -- a pointer may carry the marker without the profile -- and the marker is retained after the diagnostic so uninit_decl does not emit a second error. --- clang/docs/ProfilesFramework.rst | 22 ++++++++++++++++++- .../clang/Basic/DiagnosticSemaKinds.td | 3 +++ clang/lib/Sema/SemaDeclAttr.cpp | 10 +++++++++ .../test/SemaCXX/safety-profile-init-decl.cpp | 16 +++++++++++++- 4 files changed, 49 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 2536830948cde..3adaa6b569bfb 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -665,7 +665,10 @@ regardless of ``-fprofiles``; its profile rules carry weight only when (e.g. ``WithCtor x [[uninitialized]];``). A trivial/aggregate type whose default-initialization is a no-op is *not* such an initializer, so the marker is accepted there (the object is genuinely left uninitialized). - - Is banned on a union object or union member by ``union_marker``. + - Is banned on a pointer by ``pointer_marker`` (a pointer must be + initialized, paper §4.1), and on a union object or member by + ``union_marker``. Both are gated on enforcement, and the marker is retained + after the diagnostic so ``uninit_decl`` does not re-diagnose the entity. ``[[ref_to_uninit]]`` (also a standard C++11 attribute) marks a pointer, reference, or pointer/reference-returning function as referring to @@ -848,6 +851,23 @@ else is treated as initialized (the trust model). (``Sema::BuildReturnStmt``). A dependent target type defers to instantiation. +R8. ``pointer_marker`` -- attribute handler +........................................... + +``[[uninitialized]]`` on a pointer is banned (paper §4.1): "a reference cannot +be uninitialized. The initialization profile requires the same for pointers." +A pointer must instead be initialized (e.g. to ``nullptr``). + +- Diagnostic: ``err_init_uninit_pointer_marker``. +- Check site: the ``CXX11Uninitialized`` handler in + ``clang/lib/Sema/SemaDeclAttr.cpp``, alongside ``union_marker``. Like that + rule it is gated on enforcement -- a pointer may legitimately carry the marker + without the profile -- and the marker is retained after the diagnostic so + ``uninit_decl`` does not also fire. +- A pointer parameter is rejected earlier and unconditionally (the marker is + meaningless on a parameter); a pointer-to-member is not a pointer type and is + out of scope. + Diagnostic suppression ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index cf11778f957ed..09d9749155775 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14165,6 +14165,9 @@ def err_ref_to_uninit_attr_invalid_type : Error< def err_init_union_marker : ProfileRuleError< "'[[uninitialized]]' cannot be applied to %select{a variable of union type|" "a union member}1 under profile '%0'">; +def err_init_uninit_pointer_marker : ProfileRuleError< + "'[[uninitialized]]' cannot be applied to a pointer under profile '%0'; " + "initialize the pointer (for example to 'nullptr')">; def err_init_ctor_uninit_member : ProfileRuleError< "constructor does not initialize member %1 under profile '%0'">; def note_init_uninit_member_here : Note< diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index f5cdb9d9f4bdc..19e915f9a6395 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6982,6 +6982,16 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, // error. S.Diag(AL.getLoc(), diag::err_init_union_marker) << "std::init" << (UnionMember ? 1 : 0); + else if (cast(D)->getType()->isPointerType() && + S.shouldEmitProfileViolation("std::init", "pointer_marker", + AL.getLoc())) + // std::init / pointer_marker (paper §4.1): "a reference cannot be + // uninitialized. The initialization profile requires the same for + // pointers." A pointer must be initialized (e.g. to nullptr), so the marker + // is rejected -- but, like the union case, retained to avoid a redundant + // uninit_decl. Gated on enforcement: a pointer may carry the marker without + // the profile. + S.Diag(AL.getLoc(), diag::err_init_uninit_pointer_marker) << "std::init"; D->addAttr(::new (S.Context) CXX11UninitializedAttr(S.Context, AL)); } diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index 844e4ec4c7de4..a208eb0eaa9d7 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -21,10 +21,24 @@ void test_scalars() { void test_pointer() { int* p; // expected-error {{variable 'p' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} int* q = nullptr; - int* r [[uninitialized]]; + // A pointer cannot be left uninitialized (paper section 4.1); the marker is + // rejected rather than excusing it. + int* r [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} (void)q; (void)r; } +struct PtrMember { + int* p [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +}; + +void test_pointer_marker_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int* p [[uninitialized]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "pointer_marker")]] int* q [[uninitialized]]; + (void)p; (void)q; +} + enum E { E0, E1 }; void test_enum() { E x; // expected-error {{variable 'x' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} From 01e6f15fbb1e9f42c30430bd946ce4e29d543e52 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 11:52:06 -0400 Subject: [PATCH 109/289] Exempt std::byte from the std::init uninitialized rules The paper exempts std::byte from the read/write rules ("Reading or writing uninitialized memory is an error (except std::byte)"), but the slice diagnosed a std::byte object at its declaration and on read. Exempt std::byte from uninit_decl (the variable check and the indeterminate-scalar helper, so a std::byte subobject does not make a record indeterminate) and from uninit_read (a per-entry ExemptStdByte flag on CFGUninitProfileEntry, so the exemption applies to std::init but not the generic test::uninit_read profile). A mixed record still fires for its non-byte members. --- clang/docs/ProfilesFramework.rst | 13 +++++++------ clang/lib/Sema/AnalysisBasedWarnings.cpp | 13 +++++++++++-- clang/lib/Sema/SemaDecl.cpp | 14 ++++++++++---- .../test/SemaCXX/safety-profile-init-decl.cpp | 14 ++++++++++++++ .../test/SemaCXX/safety-profile-init-read.cpp | 18 ++++++++++++++++++ 5 files changed, 60 insertions(+), 12 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 3adaa6b569bfb..0d57536ddea8f 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -695,8 +695,10 @@ existing ``CFGUninitProfiles`` table beside ``test::uninit_read``: .. code-block:: c++ constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { - {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read}, - {"std::init", "uninit_read", diag::err_init_uninit_read}, + {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read, + /*ExemptStdByte=*/false}, + {"std::init", "uninit_read", diag::err_init_uninit_read, + /*ExemptStdByte=*/true}, }; If both ``test::uninit_read`` and ``std::init`` are enforced in the same TU, @@ -704,10 +706,9 @@ the table-order priority makes ``test::uninit_read`` fire first. Use ``[[profiles::suppress(test::uninit_read)]]`` to demote it at a use site and surface the ``std::init`` diagnostic. -Known deviation: the initialization profile paper exempts ``std::byte`` from -the uninitialized-read rule, but this slice rides the generic CFG -uninitialized-variables analysis, which does not special-case ``std::byte``, -so a read of an uninitialized ``std::byte`` is currently diagnosed. +A read of an uninitialized ``std::byte`` is not diagnosed (paper §4 exempts +``std::byte``). The exemption is per-entry via ``CFGUninitProfileEntry`` so it +applies to ``std::init`` but not the generic ``test::uninit_read`` profile. R2. ``uninit_decl`` -- pattern 1 ................................. diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index f7762a01c6c85..ac133679bd1ab 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1695,10 +1695,15 @@ struct CFGUninitProfileEntry { StringRef Name; StringRef Rule; unsigned DiagID; + // std::byte may be read while uninitialized (paper §4); the initialization + // profile exempts it, while the generic test profile does not. + bool ExemptStdByte; }; constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { - {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read}, - {"std::init", "uninit_read", diag::err_init_uninit_read}, + {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read, + /*ExemptStdByte=*/false}, + {"std::init", "uninit_read", diag::err_init_uninit_read, + /*ExemptStdByte=*/true}, }; class UninitValsDiagReporter : public UninitVariablesHandler { @@ -1773,6 +1778,10 @@ class UninitValsDiagReporter : public UninitVariablesHandler { // and skip the default warning path entirely. for (const auto &U : *vec) { for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + // std::byte may be read while uninitialized (paper §4). + if (E.ExemptStdByte && + S.Context.getBaseElementType(vd->getType())->isStdByteType()) + continue; if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) continue; S.Diag(U.getUser()->getBeginLoc(), E.DiagID) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 4aa4b15189370..5bc41916c12f9 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14724,9 +14724,13 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { // The enforcement check gates the (possibly recursive) type walk below so // it runs only under the profile, not on every default-initialized // variable. + QualType BaseTy = Context.getBaseElementType(Var->getType()); if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && !Var->hasAttr() && + // std::byte may be left uninitialized (paper §4), so it -- and arrays + // of it -- are exempt from this rule. + !BaseTy->isStdByteType() && shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && // A definition with no initializer (scalar / pointer / enum, or an // array of them), or a class/aggregate type -- possibly the element @@ -14734,13 +14738,13 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { // indeterminate (its synthesized constructor call provides an // initializer, so the !getInit() test alone misses it). (!Var->getInit() || - (Context.getBaseElementType(Var->getType())->isRecordType() && + (BaseTy->isRecordType() && defaultInitLeavesScalarIndeterminate( Var->getType(), /*HonorUninitMarkers=*/true)))) { // A union variable cannot carry [[uninitialized]] (union_marker bans it), // so it must be initialized; use a message that does not suggest the // marker as a remedy. - bool IsUnion = Context.getBaseElementType(Var->getType())->isUnionType(); + bool IsUnion = BaseTy->isUnionType(); Diag(Var->getLocation(), IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) << Profile << Var->getDeclName(); @@ -14861,8 +14865,10 @@ static bool defaultInitLeavesScalarIndeterminateImpl( return false; const auto *RD = T->getAsCXXRecordDecl(); if (!RD) - // Scalars, pointers, and enums are left indeterminate by default-init. - return T->isScalarType(); + // Scalars, pointers, and enums are left indeterminate by default-init, + // except std::byte, which the profile permits to be uninitialized + // (paper §4), so a std::byte subobject does not make a record indeterminate. + return T->isScalarType() && !T->isStdByteType(); if (RD->isInvalidDecl()) return false; // A union's members are mutually exclusive, so the per-member walk below does diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index a208eb0eaa9d7..a008d1864e1e0 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -6,8 +6,22 @@ int sink(int); +namespace std { enum class byte : unsigned char {}; } + struct Trivial { int m; }; struct WithCtor { WithCtor(); int m; }; +struct ByteWrap { std::byte b; }; +struct ByteAndInt { std::byte b; int x; }; + +void test_byte() { + // std::byte may be left uninitialized (paper section 4), as may arrays of it + // and records whose only members are std::byte. + std::byte a; + std::byte buf[8]; + ByteWrap w; + ByteAndInt m; // expected-error {{variable 'm' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + (void)a; (void)buf; (void)w; (void)m; +} void test_scalars() { int a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index 32b72d60c6868..f307e0e8498bc 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -15,6 +15,8 @@ [[profiles::enforce(test::uninit_read)]]; #endif +namespace std { enum class byte : unsigned char {}; } + int leading_unrelated_error = undeclared_identifier; // common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} @@ -114,6 +116,14 @@ void test_decl_suppress_does_not_extend() { int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; } + +// std::byte may be read while uninitialized (paper section 4), so std::init +// does not diagnose a read of an uninitialized std::byte. +void test_byte_read_exempt() { + std::byte b [[uninitialized]]; + std::byte c = b; + (void)c; +} #endif #ifdef DEMOTE @@ -127,4 +137,12 @@ void test_demote_test_profile() { (void)y; } } + +// The std::byte exemption is std::init-only: test::uninit_read still diagnoses +// a read of an uninitialized std::byte. +void test_byte_not_exempt_under_test_profile() { + std::byte b [[uninitialized]]; // demote-note {{variable 'b' is declared here}} + std::byte c = b; // demote-error {{variable 'b' is read before initialization under profile 'test::uninit_read'}} + (void)c; +} #endif From c8d7e570a06e682f6a5e125625642d572835f225 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 13:51:18 -0400 Subject: [PATCH 110/289] Exclude thread-locals from the std::init static_runtime_init rule --- clang/lib/Sema/SemaDecl.cpp | 5 +++++ clang/test/SemaCXX/safety-profile-init-static.cpp | 2 ++ 2 files changed, 7 insertions(+) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 5bc41916c12f9..85554591a2fdd 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15291,6 +15291,11 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { Diag(it.first, it.second); var->setInvalidDecl(); } else if (IsGlobal && [&] { + // Thread-locals have thread (not static) storage duration; + // paper §3 scopes this rule to non-local *static* objects + // (uninit_decl likewise excludes thread storage). + if (var->getTLSKind() != VarDecl::TLS_None) + return false; // std::init / static_runtime_init: paper says non-local // statics must be initialized at compile or link time. // checkConstInit() permits trivial default initialization diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp index f70965b0199b4..3dc73d223fd85 100644 --- a/clang/test/SemaCXX/safety-profile-init-static.cpp +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -24,6 +24,8 @@ Trivial g_trivial_braced = {}; WithDtor g_with_dtor; WithCtor g_with_ctor; // expected-error {{non-local variable 'g_with_ctor' requires constant initialization under profile 'std::init'}} +thread_local int t_ns = runtime(); // OK: thread storage duration, not static + constinit int g_ci = 0; // The constinit hard error fires regardless of -fprofiles. constinit int g_ci_runtime = runtime(); From 08e6d7c42704d93b552da34b5f926550c7c715fa Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 14:13:56 -0400 Subject: [PATCH 111/289] Propagate [[ref_to_uninit]] through explicit pointer casts under std::init Paper section 4.3: a [[ref_to_uninit]] void* cast to another pointer type is itself [[ref_to_uninit]]. The ref_to_uninit recognizer only stripped implicit casts (IgnoreParenImpCasts), so an explicit pointer cast laundered the uninitialized-ness past the check. Look through an explicit pointer-to-pointer cast in pointerRefersToUninitStorage, recursing into the operand. The operand must itself be a pointer, so a pointer manufactured from an integer (reinterpret_cast(0xdeadbeef)) stays trusted. Deref-of-cast is fixed transitively through the existing UO_Deref arm. --- clang/include/clang/Sema/Sema.h | 6 ++-- clang/lib/Sema/SemaDecl.cpp | 8 +++++ .../safety-profile-init-ref-to-uninit.cpp | 32 +++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index d400017253698..2eb211e7a8aa8 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1129,9 +1129,9 @@ class Sema final : public SemaBase { /// uninitialized storage. Recognized purely locally from the expression's /// syntactic form -- the address of, or a subobject of, a [[uninitialized]] /// entity; a value of a [[ref_to_uninit]] pointer/reference or array; a - /// dereference of such a pointer; or a call to a [[ref_to_uninit]]-returning - /// function. Anything else is treated as initialized (the trust model; no - /// flow analysis). + /// dereference of such a pointer; a cast of such a pointer to another pointer + /// type; or a call to a [[ref_to_uninit]]-returning function. Anything else + /// is treated as initialized (the trust model; no flow analysis). bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; /// std::init / ref_to_uninit (paper §5): check that the initialization of a diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 85554591a2fdd..89389ed900b35 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14979,6 +14979,14 @@ static bool pointerRefersToUninitStorage(const Expr *E) { if (const FunctionDecl *FD = CE->getDirectCallee()) return FD->hasAttr(); + // Paper §4.3: a [[ref_to_uninit]] pointer cast to another pointer type is + // itself [[ref_to_uninit]]. Implicit casts were already stripped above, so + // this only looks through an explicit pointer-to-pointer cast; a pointer + // manufactured from an integer (operand not a pointer) is not propagated. + if (const auto *CE = dyn_cast(E)) + if (CE->getSubExpr()->getType()->isPointerType()) + return pointerRefersToUninitStorage(CE->getSubExpr()); + return false; } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index ba6be1373c5c3..be7e7b7eff1ad 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -12,6 +12,7 @@ int g_init = 0; [[uninitialized]] int g_uninit; [[uninitialized]] int g_uninit_arr[3]; [[ref_to_uninit]] int *allocate(int n); +[[ref_to_uninit]] void *alloc_void(); void test_pointer_target() { int *p1 [[ref_to_uninit]] = &g_uninit; // OK @@ -33,6 +34,37 @@ void test_pointer_sources() { (void)bad_from_array; (void)bad_from_call; } +// An explicit pointer-to-pointer cast of a [[ref_to_uninit]] pointer is itself +// [[ref_to_uninit]] (paper §4.3); the cast does not launder the marking. +void test_pointer_casts() { + void *vp [[ref_to_uninit]] = &g_uninit; + int *c1 [[ref_to_uninit]] = (int *)vp; // OK + int *c2 = (int *)vp; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + int *sc1 [[ref_to_uninit]] = static_cast(vp); // OK + int *sc2 = static_cast(vp); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rc1 [[ref_to_uninit]] = reinterpret_cast(vp); // OK + int *rc2 = reinterpret_cast(vp); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // An initialized pointer round-tripped through a cast stays initialized. + void *vi = &g_init; + int *ci1 = (int *)vi; // OK + int *ci2 [[ref_to_uninit]] = (int *)vi; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + // Casting a [[ref_to_uninit]]-returning call result propagates the marking. + int *cc1 [[ref_to_uninit]] = (int *)alloc_void(); // OK + int *cc2 = (int *)alloc_void(); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // Trust model: a pointer manufactured from an integer is not propagated. + int *ti = reinterpret_cast(0xdeadbeef); // OK + + // Deref of a cast routes back through the pointer recognizer. + int &dr [[ref_to_uninit]] = *(int *)vp; // OK + + (void)c1; (void)c2; (void)sc1; (void)sc2; (void)rc1; (void)rc2; + (void)vi; (void)ci1; (void)ci2; (void)cc1; (void)cc2; (void)ti; (void)dr; +} + void test_reference_target() { int &r1 [[ref_to_uninit]] = g_uninit; // OK int &r2 [[ref_to_uninit]] = g_init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} From 7e9f78e946d1fbfe92d2a9d3f208beae750a71c0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 14:21:16 -0400 Subject: [PATCH 112/289] Propagate [[ref_to_uninit]] through reference casts and calls under std::init Close the symmetric reference-side gap left by the pointer-cast fix. The glvalue recognizer matched DeclRefExpr/MemberExpr/ArraySubscriptExpr and a pointer dereference, but had no cast arm and no call arm, so an explicit reference cast (int &r = (int &)x;) laundered the uninitialized-ness past the check and a [[ref_to_uninit]]-returning reference call was missed where the pointer equivalent was not. Add an ExplicitCastExpr arm guarded on a glvalue operand and a CallExpr arm (direct callee has RefToUninitAttr) to glvalueDenotesUninitStorage, making the two recognizers symmetric. These forms follow from the profile's guarantee (no use of uninitialized objects) rather than an explicit paper rule. --- clang/include/clang/Sema/Sema.h | 5 +++-- clang/lib/Sema/SemaDecl.cpp | 11 +++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 2eb211e7a8aa8..673f26f4d8595 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1130,8 +1130,9 @@ class Sema final : public SemaBase { /// syntactic form -- the address of, or a subobject of, a [[uninitialized]] /// entity; a value of a [[ref_to_uninit]] pointer/reference or array; a /// dereference of such a pointer; a cast of such a pointer to another pointer - /// type; or a call to a [[ref_to_uninit]]-returning function. Anything else - /// is treated as initialized (the trust model; no flow analysis). + /// type, or of such a glvalue to another reference; or a call to a + /// [[ref_to_uninit]]-returning function. Anything else is treated as + /// initialized (the trust model; no flow analysis). bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; /// std::init / ref_to_uninit (paper §5): check that the initialization of a diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 89389ed900b35..8cfa09a356709 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15012,6 +15012,11 @@ static bool glvalueDenotesUninitStorage(const Expr *E) { return DeclDenotesUninit(ME->getMemberDecl()) || (ME->isArrow() ? pointerRefersToUninitStorage(ME->getBase()) : glvalueDenotesUninitStorage(ME->getBase())); + // A call to a [[ref_to_uninit]]-returning reference function: the referent + // it returns is uninitialized. Mirrors the pointer recognizer's call arm. + if (const auto *CE = dyn_cast(E)) + if (const FunctionDecl *FD = CE->getDirectCallee()) + return FD->hasAttr(); if (const auto *ASE = dyn_cast(E)) return pointerRefersToUninitStorage(ASE->getBase()); @@ -15020,6 +15025,12 @@ static bool glvalueDenotesUninitStorage(const Expr *E) { if (UO->getOpcode() == UO_Deref) return pointerRefersToUninitStorage(UO->getSubExpr()); + // A reference cast (an explicit cast yielding a glvalue) denotes the same + // storage as its operand; propagate. Symmetric to the pointer-cast arm. + if (const auto *CE = dyn_cast(E)) + if (CE->getSubExpr()->isGLValue()) + return glvalueDenotesUninitStorage(CE->getSubExpr()); + return false; } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index be7e7b7eff1ad..e6220d8a7c6fc 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -13,6 +13,7 @@ int g_init = 0; [[uninitialized]] int g_uninit_arr[3]; [[ref_to_uninit]] int *allocate(int n); [[ref_to_uninit]] void *alloc_void(); +[[ref_to_uninit]] int &get_uninit_ref(); void test_pointer_target() { int *p1 [[ref_to_uninit]] = &g_uninit; // OK @@ -79,6 +80,23 @@ void test_reference_target() { (void)r1; (void)r2; (void)r3; (void)r4; (void)r5; (void)r6; (void)r7; (void)p; } +// A reference cast (an explicit cast yielding a glvalue) denotes the same +// storage as its operand, and a [[ref_to_uninit]]-returning reference call +// denotes uninitialized storage. Symmetric to the pointer side. +void test_reference_casts() { + int &cr1 [[ref_to_uninit]] = static_cast(g_uninit); // OK + int &cr2 = static_cast(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &cr3 [[ref_to_uninit]] = (int &)g_init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + int &gr1 [[ref_to_uninit]] = get_uninit_ref(); // OK + int &gr2 = get_uninit_ref(); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // Address of a reference cast routes back through the pointer recognizer. + int *p [[ref_to_uninit]] = &(int &)g_uninit; // OK + + (void)cr1; (void)cr2; (void)cr3; (void)gr1; (void)gr2; (void)p; +} + void test_assignment() { int *p [[ref_to_uninit]] = &g_uninit; p = &g_uninit; // OK From fd06ee9c06ed76e2665aff9d4ec0c7c595ef5425 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 14:22:53 -0400 Subject: [PATCH 113/289] Document ref_to_uninit cast propagation in the profiles framework docs Update the R7 recognizer list to record that the recognizer looks through an explicit pointer-to-pointer cast (paper section 4.3) and, symmetrically, a reference cast and a [[ref_to_uninit]]-returning reference call. The reference forms are noted as following from the profile's guarantee rather than an explicit paper rule. --- clang/docs/ProfilesFramework.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 0d57536ddea8f..99468b08ce6dd 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -836,8 +836,13 @@ initialized memory. "Refers to uninitialized memory" is recognised purely locally from the source expression's syntactic form (no flow analysis): the address of, or a subobject of, a ``[[uninitialized]]`` entity; a value of a ``[[ref_to_uninit]]`` pointer/reference or array; a dereference of such a -pointer; or a call to a ``[[ref_to_uninit]]``-returning function. Anything -else is treated as initialized (the trust model). +pointer; a cast of such a pointer to another pointer type (paper §4.3), or of +such a glvalue to another reference; or a call to a +``[[ref_to_uninit]]``-returning function. Anything else is treated as +initialized (the trust model). The reference cast and the +``[[ref_to_uninit]]``-returning reference call are not spelled out by the +paper but follow from the profile's guarantee that uninitialized objects are +not used, and keep the pointer and reference recognizers symmetric. - Diagnostics: ``err_init_ref_to_uninit_requires_uninit`` (marked target, initialized source) and ``err_init_uninit_requires_ref_to_uninit`` (unmarked From cb3c0dc43b803465d948767f50671801ffe60566 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 14:50:14 -0400 Subject: [PATCH 114/289] Rename the [[uninitialized]] init-profile marker to [[uninit]] Matches P4222R1.1, which shortened the spelling. --- clang/docs/ProfilesFramework.rst | 44 +++++++++---------- clang/include/clang/Basic/Attr.td | 6 +-- clang/include/clang/Basic/AttrDocs.td | 10 ++--- .../clang/Basic/DiagnosticSemaKinds.td | 12 ++--- clang/include/clang/Sema/Sema.h | 6 +-- clang/lib/Sema/SemaDecl.cpp | 20 ++++----- clang/lib/Sema/SemaDeclAttr.cpp | 13 +++--- clang/lib/Sema/SemaDeclCXX.cpp | 6 +-- ...a-attribute-supported-attributes-list.test | 2 +- .../SemaCXX/safety-profile-init-aggregate.cpp | 26 +++++------ .../test/SemaCXX/safety-profile-init-ctor.cpp | 8 ++-- .../test/SemaCXX/safety-profile-init-decl.cpp | 26 +++++------ .../safety-profile-init-field-marker.cpp | 40 ++++++++--------- .../test/SemaCXX/safety-profile-init-read.cpp | 26 +++++------ .../safety-profile-init-ref-to-uninit.cpp | 6 +-- .../SemaCXX/safety-profile-init-union.cpp | 10 ++--- .../safety-profile-init-with-initializer.cpp | 36 +++++++-------- 17 files changed, 148 insertions(+), 149 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 99468b08ce6dd..fd97cf3368cca 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -393,7 +393,7 @@ For a variable declaration that range covers the initializer expression (so subsequent uses; those uses appear in different declarations or statements and are checked normally at their own source location. Profiles that need per-object "opt-out of this check everywhere this value is used" semantics -(for example, the proposed ``[[uninitialized]]`` attribute of the +(for example, the proposed ``[[uninit]]`` attribute of the initialization profile) must introduce their own, separate, decl-scoped marker. @@ -631,9 +631,9 @@ The ``std::init`` Profile (initial slice) A slice of the proposed initialization profile. It does not yet implement classes that expose uninitialized memory to users (paper §6.2) or random-access initialization of uninitialized arrays (paper §6.4); and the constructor-body -flow check that would let a ``[[uninitialized]]`` member be initialized by +flow check that would let a ``[[uninit]]`` member be initialized by assignment in the body (the dynamic half of paper §6.1) is deferred to a future -CFG-based pass. Until it lands, a ``[[uninitialized]]`` data member is trusted, +CFG-based pass. Until it lands, a ``[[uninit]]`` data member is trusted, and writes *through* a ``[[ref_to_uninit]]`` pointer/reference are not yet verified (the paper relegates them to ``construct_at`` or suppression). @@ -642,13 +642,13 @@ The slice introduces two marker attributes and the rules below. Marker attributes ~~~~~~~~~~~~~~~~~ -``[[uninitialized]]`` (a standard C++11 attribute, distinct from the Clang +``[[uninit]]`` (a standard C++11 attribute, distinct from the Clang vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` or ``FieldDecl`` as intentionally left uninitialized. Recognised by Clang regardless of ``-fprofiles``; its profile rules carry weight only when ``std::init`` is enforced. -- TableGen def: ``CXX11Uninitialized`` in ``clang/include/clang/Basic/Attr.td``, +- TableGen def: ``Uninit`` in ``clang/include/clang/Basic/Attr.td``, with a custom handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. - Subjects: ``Var`` and ``Field``. The handler rejects placement where the marker is meaningless -- a reference, a function parameter, or a structured @@ -662,7 +662,7 @@ regardless of ``-fprofiles``; its profile rules carry weight only when ill-formed. - Triggers ``uninit_with_initializer`` when combined with an initializer, including a language-synthesized one from a constructor that actually runs - (e.g. ``WithCtor x [[uninitialized]];``). A trivial/aggregate type whose + (e.g. ``WithCtor x [[uninit]];``). A trivial/aggregate type whose default-initialization is a no-op is *not* such an initializer, so the marker is accepted there (the object is genuinely left uninitialized). - Is banned on a pointer by ``pointer_marker`` (a pointer must be @@ -715,7 +715,7 @@ R2. ``uninit_decl`` -- pattern 1 An automatic-storage variable definition whose default-initialization leaves it (or a scalar subobject) indeterminate must either carry -``[[uninitialized]]`` or be initialized. This covers a scalar / pointer / +``[[uninit]]`` or be initialized. This covers a scalar / pointer / enum with no initializer, and -- per paper §6 ("classes without constructors") -- an aggregate or trivially-default-constructible class type whose default-initialization leaves a scalar subobject indeterminate (e.g. @@ -732,9 +732,9 @@ constructor is trusted; static / thread storage duration is excluded - The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=true``, which recurses through bases and members, trusts user-provided default constructors, and skips data members marked - ``[[uninitialized]]`` (acknowledged uninitialized, paper §6.2). So a type + ``[[uninit]]`` (acknowledged uninitialized, paper §6.2). So a type whose only indeterminate scalars are all marked is trusted - (e.g. ``struct A { int x [[uninitialized]]; }; A a;`` is accepted), while a + (e.g. ``struct A { int x [[uninit]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. R3. ``static_runtime_init`` -- pattern 1 @@ -754,7 +754,7 @@ be ``constinit`` (which is already a hard error from the existing R4. ``uninit_with_initializer`` -- pattern 1 ............................................ -``[[uninitialized]]`` and an initializer on the same declaration is a +``[[uninit]]`` and an initializer on the same declaration is a contradiction (the marker means "no initialization here"). - Diagnostic: ``err_init_uninit_with_initializer``. @@ -765,25 +765,25 @@ contradiction (the marker means "no initialization here"). - A ``RecoveryExpr`` placeholder (from a failed initialization) is not a user-written initializer and does not trigger the rule. - The "initializer" includes a language-synthesized one from a constructor - that actually runs (e.g. ``WithCtor x [[uninitialized]];``), but *not* a + that actually runs (e.g. ``WithCtor x [[uninit]];``), but *not* a no-op trivial/aggregate default-initialization, where the marker is consistent with the object being left uninitialized. - Unlike R2/R5, this "no-op?" test calls ``defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=false`` (the *factual* answer): a type whose members are themselves marked still default-initializes to a no-op, so the variable marker stays consistent and - the rule must not fire (e.g. ``A a [[uninitialized]];`` for the ``A`` above). + the rule must not fire (e.g. ``A a [[uninit]];`` for the ``A`` above). R5. ``ctor_uninit_member`` -- pattern 4 ....................................... A user-provided constructor must initialize every non-static data member via its member-initializer list or an NSDMI, unless the member is marked -``[[uninitialized]]`` (paper §6.1). A plain assignment in the constructor +``[[uninit]]`` (paper §6.1). A plain assignment in the constructor body does not count. A member whose own default-initialization leaves an *unacknowledged* scalar subobject indeterminate (a nested aggregate) is flagged as well; a member whose type's indeterminate scalars are all -themselves marked ``[[uninitialized]]`` is trusted (the same +themselves marked ``[[uninit]]`` is trusted (the same ``HonorUninitMarkers`` walk as R2, paper §6.2). - Diagnostic: ``err_init_ctor_uninit_member`` (with a @@ -805,12 +805,12 @@ themselves marked ``[[uninitialized]]`` is trusted (the same R6. ``union_marker`` -- attribute handler ......................................... -``[[uninitialized]]`` on a union object or a union member is banned (paper +``[[uninit]]`` on a union object or a union member is banned (paper §6.5): delayed initialization by assigning a member would be an erroneous assignment when compiled without the profile. - Diagnostic: ``err_init_union_marker``. -- Check site: the ``CXX11Uninitialized`` handler in +- Check site: the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. Unlike the reference / parameter / structured-binding rejections, which are unconditional, this is gated on enforcement -- a union may legitimately carry the marker without the @@ -834,7 +834,7 @@ A pointer or reference must be bound consistently with its refer to uninitialized memory, and an unmarked one may only refer to initialized memory. "Refers to uninitialized memory" is recognised purely locally from the source expression's syntactic form (no flow analysis): the -address of, or a subobject of, a ``[[uninitialized]]`` entity; a value of a +address of, or a subobject of, a ``[[uninit]]`` entity; a value of a ``[[ref_to_uninit]]`` pointer/reference or array; a dereference of such a pointer; a cast of such a pointer to another pointer type (paper §4.3), or of such a glvalue to another reference; or a call to a @@ -860,12 +860,12 @@ not used, and keep the pointer and reference recognizers symmetric. R8. ``pointer_marker`` -- attribute handler ........................................... -``[[uninitialized]]`` on a pointer is banned (paper §4.1): "a reference cannot +``[[uninit]]`` on a pointer is banned (paper §4.1): "a reference cannot be uninitialized. The initialization profile requires the same for pointers." A pointer must instead be initialized (e.g. to ``nullptr``). - Diagnostic: ``err_init_uninit_pointer_marker``. -- Check site: the ``CXX11Uninitialized`` handler in +- Check site: the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp``, alongside ``union_marker``. Like that rule it is gated on enforcement -- a pointer may legitimately carry the marker without the profile -- and the marker is retained after the diagnostic so @@ -933,7 +933,7 @@ profiles. When changing the framework, run them all with defaulted skips, suppression, and the without-``-fprofiles`` path. - ``clang/test/SemaCXX/safety-profile-init-decl.cpp`` -- the ``std::init`` profile's ``uninit_decl`` rule for scalars / pointers / enums: require an - initializer or ``[[uninitialized]]``; statics / thread-locals are + initializer or ``[[uninit]]``; statics / thread-locals are excluded; class types with a user-provided default constructor are trusted. - ``clang/test/SemaCXX/safety-profile-init-aggregate.cpp`` -- the @@ -947,11 +947,11 @@ profiles. When changing the framework, run them all with regardless of ``-fprofiles``. - ``clang/test/SemaCXX/safety-profile-init-with-initializer.cpp`` -- the ``std::init`` profile's ``uninit_with_initializer`` rule: every - combination of ``[[uninitialized]]`` placement (prefix / postfix) with + combination of ``[[uninit]]`` placement (prefix / postfix) with every initializer form (``= e``, ``{}``, ``(e)``), plus the synthesized-initializer and RecoveryExpr cases. - ``clang/test/SemaCXX/safety-profile-init-field-marker.cpp`` -- placement - of ``[[uninitialized]]`` on data members, the marker / NSDMI + of ``[[uninit]]`` on data members, the marker / NSDMI contradiction, and rejection on references, parameters, and structured bindings. - ``clang/test/SemaCXX/safety-profile-init-ctor.cpp`` -- the ``std::init`` diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 97f5f16f83124..cf6df4dddb9a0 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1721,10 +1721,10 @@ def CXX11NoReturn : InheritableAttr { let Documentation = [CXX11NoReturnDocs]; } -def CXX11Uninitialized : InheritableAttr { - let Spellings = [CXX11<"", "uninitialized", 202602>]; +def Uninit : InheritableAttr { + let Spellings = [CXX11<"", "uninit", 202602>]; let Subjects = SubjectList<[Var, Field], ErrorDiag>; - let Documentation = [CXX11UninitializedDocs]; + let Documentation = [UninitDocs]; } def RefToUninit : InheritableAttr { diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 7b315d0203d2b..e88d952c4a1a2 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -875,11 +875,11 @@ migration for code using ``[[noreturn]]`` after including ````. }]; } -def CXX11UninitializedDocs : Documentation { +def UninitDocs : Documentation { let Category = DocCatVariable; - let Heading = "uninitialized"; + let Heading = "uninit"; let Content = [{ -The ``[[uninitialized]]`` attribute marks a variable or non-static data +The ``[[uninit]]`` attribute marks a variable or non-static data member definition as intentionally left uninitialized. It is purely informational: it has no effect on code generation and the entity still has the same indeterminate value it would have without the attribute. @@ -887,13 +887,13 @@ has the same indeterminate value it would have without the attribute. The attribute exists to support the initialization profile (see :doc:`ProfilesFramework`). Under that profile, a definition such as ``int x;`` is diagnosed because it leaves ``x`` with an indeterminate -value; writing ``int x [[uninitialized]];`` instead suppresses that +value; writing ``int x [[uninit]];`` instead suppresses that declaration-site diagnostic and documents that the lack of an initializer is intentional. The attribute does **not** suppress diagnostics that fire when the variable is later read before being assigned; the read-before-init rule still applies. -A declaration or non-static data member with both ``[[uninitialized]]`` +A declaration or non-static data member with both ``[[uninit]]`` and an initializer is ill-formed under the initialization profile (the marker contradicts the explicit initialization). diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 09d9749155775..1af0060035d85 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14146,7 +14146,7 @@ def err_profile_ctor_final_test : ProfileRuleError< def err_init_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; def err_init_uninit_decl : ProfileRuleError< - "variable %1 must be initialized or marked '[[uninitialized]]' under " + "variable %1 must be initialized or marked '[[uninit]]' under " "profile '%0'">; def err_init_uninit_union : ProfileRuleError< "variable %1 of union type must be initialized under profile '%0'">; @@ -14154,19 +14154,19 @@ def err_init_static_runtime_init : ProfileRuleError< "non-local variable %1 requires constant initialization under " "profile '%0'">; def err_init_uninit_with_initializer : ProfileRuleError< - "variable %1 cannot be both '[[uninitialized]]' and have an initializer " + "variable %1 cannot be both '[[uninit]]' and have an initializer " "under profile '%0'">; -def err_uninitialized_attr_invalid_subject : Error< - "'uninitialized' attribute cannot be applied to %select{a reference|" +def err_uninit_attr_invalid_subject : Error< + "'uninit' attribute cannot be applied to %select{a reference|" "a function parameter|a structured binding}0">; def err_ref_to_uninit_attr_invalid_type : Error< "'ref_to_uninit' attribute only applies to pointers, references, and " "functions returning them">; def err_init_union_marker : ProfileRuleError< - "'[[uninitialized]]' cannot be applied to %select{a variable of union type|" + "'[[uninit]]' cannot be applied to %select{a variable of union type|" "a union member}1 under profile '%0'">; def err_init_uninit_pointer_marker : ProfileRuleError< - "'[[uninitialized]]' cannot be applied to a pointer under profile '%0'; " + "'[[uninit]]' cannot be applied to a pointer under profile '%0'; " "initialize the pointer (for example to 'nullptr')">; def err_init_ctor_uninit_member : ProfileRuleError< "constructor does not initialize member %1 under profile '%0'">; diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 673f26f4d8595..9c729d85cc540 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1096,7 +1096,7 @@ class Sema final : public SemaBase { SourceLocation Loc, unsigned DiagID); /// std::init / uninit_with_initializer (R4): diagnose an entity that is both - /// marked [[uninitialized]] and has an initializer. Shared by the variable + /// marked [[uninit]] and has an initializer. Shared by the variable /// (\c CheckCompleteVariableDeclaration) and non-static data member /// (\c ActOnFinishCXXInClassMemberInitializer) paths. \p Init is the /// (possibly null) initializer; a RecoveryExpr placeholder for a failed @@ -1115,7 +1115,7 @@ class Sema final : public SemaBase { /// at its own definition). Dependent and incomplete types are treated as /// determinate. /// - /// When \p HonorUninitMarkers is true, a data member marked [[uninitialized]] + /// When \p HonorUninitMarkers is true, a data member marked [[uninit]] /// is treated as acknowledged and skipped, so a type whose only indeterminate /// scalars are all marked is reported as determinate. uninit_decl and /// ctor_uninit_member pass true (the marker excuses the member, paper §6.2); @@ -1127,7 +1127,7 @@ class Sema final : public SemaBase { /// std::init / ref_to_uninit (paper §5): true if \p E refers to (for a /// pointer source) or, when \p IsReference, denotes (for a glvalue source) /// uninitialized storage. Recognized purely locally from the expression's - /// syntactic form -- the address of, or a subobject of, a [[uninitialized]] + /// syntactic form -- the address of, or a subobject of, a [[uninit]] /// entity; a value of a [[ref_to_uninit]] pointer/reference or array; a /// dereference of such a pointer; a cast of such a pointer to another pointer /// type, or of such a glvalue to another reference; or a call to a diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 8cfa09a356709..0df95df0e0a9f 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14716,7 +14716,7 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { } // std::init / uninit_decl: a definition without any initializer (after - // attempted default-initialization) must either carry [[uninitialized]] or + // attempted default-initialization) must either carry [[uninit]] or // be initialized by a language rule. Static / thread storage duration is // excluded -- those are zero-initialized; runtime-init concerns are R3's. static constexpr StringRef Profile = "std::init"; @@ -14727,7 +14727,7 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { QualType BaseTy = Context.getBaseElementType(Var->getType()); if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && - !Var->hasAttr() && + !Var->hasAttr() && // std::byte may be left uninitialized (paper §4), so it -- and arrays // of it -- are exempt from this rule. !BaseTy->isStdByteType() && @@ -14741,7 +14741,7 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { (BaseTy->isRecordType() && defaultInitLeavesScalarIndeterminate( Var->getType(), /*HonorUninitMarkers=*/true)))) { - // A union variable cannot carry [[uninitialized]] (union_marker bans it), + // A union variable cannot carry [[uninit]] (union_marker bans it), // so it must be initialized; use a message that does not suggest the // marker as a remedy. bool IsUnion = BaseTy->isUnionType(); @@ -14897,10 +14897,10 @@ static bool defaultInitLeavesScalarIndeterminateImpl( for (const FieldDecl *F : RD->fields()) { if (F->isUnnamedBitField() || F->hasInClassInitializer()) continue; - // A member the type's author marked [[uninitialized]] is acknowledged as + // A member the type's author marked [[uninit]] is acknowledged as // intentionally uninitialized, so it does not leave an unacknowledged // scalar indeterminate (paper §6.2). - if (HonorUninitMarkers && F->hasAttr()) + if (HonorUninitMarkers && F->hasAttr()) continue; if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), HonorUninitMarkers, Visited)) @@ -14922,7 +14922,7 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, const Expr *Init, bool HasMarker, const Decl *D) { - // [[uninitialized]] documents that the entity is intentionally left + // [[uninit]] documents that the entity is intentionally left // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr // is a placeholder for an initialization that already failed (e.g. // default-init of a const scalar), not an initializer the user wrote, so it @@ -14950,7 +14950,7 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, // std::init / ref_to_uninit (paper §5). Two mutually-recursive local // recognizers over the syntactic form of a source expression -- no flow // analysis and no type-system tracking. Uninitialized storage is only ever -// introduced by an explicit [[uninitialized]] / [[ref_to_uninit]] marker; +// introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker; // anything unrecognized is treated as initialized (the trust model). static bool glvalueDenotesUninitStorage(const Expr *E); @@ -14996,12 +14996,12 @@ static bool glvalueDenotesUninitStorage(const Expr *E) { return false; E = E->IgnoreParenImpCasts(); - // A named entity denotes uninitialized storage if it is [[uninitialized]], or + // A named entity denotes uninitialized storage if it is [[uninit]], or // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes // the pointer object itself -- which is initialized -- so it does not count. auto DeclDenotesUninit = [](const ValueDecl *VD) { - return VD->hasAttr() || + return VD->hasAttr() || (VD->getType()->isReferenceType() && VD->hasAttr()); }; if (const auto *DRE = dyn_cast(E)) @@ -15063,7 +15063,7 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { checkInitProfileUninitWithInitializer( var->getLocation(), var->getDeclName(), var->getType(), var->getInit(), - var->hasAttr(), var); + var->hasAttr(), var); // std::init / ref_to_uninit (paper §5): a pointer or reference variable must // be bound consistently with its [[ref_to_uninit]] marking. A dependent type diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 19e915f9a6395..e3262d447c75e 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6941,8 +6941,7 @@ static void handleUninitializedAttr(Sema &S, Decl *D, const ParsedAttr &AL) { D->addAttr(::new (S.Context) UninitializedAttr(S.Context, AL)); } -static void handleCXX11UninitializedAttr(Sema &S, Decl *D, - const ParsedAttr &AL) { +static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // The SubjectList has already restricted D to a variable or non-static data // member. Reject the subjects for which "leave uninitialized" is // meaningless: a reference (must bind when declared), a function parameter @@ -6959,7 +6958,7 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, Invalid = Reference; if (Invalid) { - S.Diag(AL.getLoc(), diag::err_uninitialized_attr_invalid_subject) + S.Diag(AL.getLoc(), diag::err_uninit_attr_invalid_subject) << static_cast(*Invalid); AL.setInvalid(); return; @@ -6993,14 +6992,14 @@ static void handleCXX11UninitializedAttr(Sema &S, Decl *D, // the profile. S.Diag(AL.getLoc(), diag::err_init_uninit_pointer_marker) << "std::init"; - D->addAttr(::new (S.Context) CXX11UninitializedAttr(S.Context, AL)); + D->addAttr(::new (S.Context) UninitAttr(S.Context, AL)); } static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // The SubjectList restricts D to a variable, non-static data member, or // function. "Refers to uninitialized memory" is only meaningful for a // pointer or reference (for a function, its return value), so reject any - // other type. Like the [[uninitialized]] subject checks, this is not + // other type. Like the [[uninit]] subject checks, this is not // profile policy and so fires regardless of -fprofiles. QualType T; if (const auto *FD = dyn_cast(D)) @@ -8283,8 +8282,8 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, handleUninitializedAttr(S, D, AL); break; - case ParsedAttr::AT_CXX11Uninitialized: - handleCXX11UninitializedAttr(S, D, AL); + case ParsedAttr::AT_Uninit: + handleUninitAttr(S, D, AL); break; case ParsedAttr::AT_RefToUninit: diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index f5a03b72360b9..29bc00ad9a419 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4234,7 +4234,7 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, checkInitProfileUninitWithInitializer(FD->getLocation(), FD->getDeclName(), FD->getType(), FD->getInClassInitializer(), - FD->hasAttr(), + FD->hasAttr(), FD); // std::init / ref_to_uninit (paper §5): a pointer or reference data member @@ -7076,7 +7076,7 @@ void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { // Paper §6.1: a user-provided constructor must initialize every member via // its member-initializer list or an NSDMI, unless the member is marked - // [[uninitialized]] (whose body initialization is the deferred R7 check). + // [[uninit]] (whose body initialization is the deferred R7 check). // A plain assignment in the constructor body does not count. if (!Ctor->isUserProvided()) return; @@ -7101,7 +7101,7 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { if (F->isUnnamedBitField() || !F->getDeclName() || F->getType()->isReferenceType() || F->getType().isConstQualified()) continue; - if (F->hasAttr() || F->hasInClassInitializer() || + if (F->hasAttr() || F->hasInClassInitializer() || Written.count(F)) continue; if (!S.defaultInitLeavesScalarIndeterminate(F->getType(), diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test index 649447e800ddb..049498a8f6b82 100644 --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -47,7 +47,6 @@ // CHECK-NEXT: CUDANoCluster (SubjectMatchRule_objc_method, SubjectMatchRule_hasType_functionType) // CHECK-NEXT: CUDAShared (SubjectMatchRule_variable) // CHECK-NEXT: CXX11NoReturn (SubjectMatchRule_function) -// CHECK-NEXT: CXX11Uninitialized (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: CallableWhen (SubjectMatchRule_function_is_member) // CHECK-NEXT: Callback (SubjectMatchRule_function) // CHECK-NEXT: CalledOnce (SubjectMatchRule_variable_is_parameter) @@ -225,6 +224,7 @@ // CHECK-NEXT: TargetVersion (SubjectMatchRule_function) // CHECK-NEXT: TestTypestate (SubjectMatchRule_function_is_member) // CHECK-NEXT: TrivialABI (SubjectMatchRule_record) +// CHECK-NEXT: Uninit (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: Uninitialized (SubjectMatchRule_variable_is_local) // CHECK-NEXT: UnsafeBufferUsage (SubjectMatchRule_function, SubjectMatchRule_field) // CHECK-NEXT: UseHandle (SubjectMatchRule_variable_is_parameter) diff --git a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp index d521ebd7961e0..4b6d4ea6a39f4 100644 --- a/clang/test/SemaCXX/safety-profile-init-aggregate.cpp +++ b/clang/test/SemaCXX/safety-profile-init-aggregate.cpp @@ -10,23 +10,23 @@ struct WithCtor { WithCtor(); int x; }; struct PartlyInit { int x; struct Inner { Inner(); } s; }; struct Nested { Trivial t; }; struct WithBase : Trivial {}; -struct MarkedMember { int x [[uninitialized]]; }; -struct MixedMarked { int a [[uninitialized]]; int b; }; +struct MarkedMember { int x [[uninit]]; }; +struct MixedMarked { int a [[uninit]]; int b; }; struct NestedMarked { MarkedMember m; }; void test_aggregate() { - Trivial a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Trivial a; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} Trivial b = {1}; Trivial c = {}; Trivial d{}; - Trivial e [[uninitialized]]; + Trivial e [[uninit]]; (void)a; (void)b; (void)c; (void)d; (void)e; } void test_nested_and_base() { - PartlyInit a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} - Nested b; // expected-error {{variable 'b' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} - WithBase c; // expected-error {{variable 'c' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + PartlyInit a; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + Nested b; // expected-error {{variable 'b' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + WithBase c; // expected-error {{variable 'c' must be initialized or marked '[[uninit]]' under profile 'std::init'}} (void)a; (void)b; (void)c; } @@ -40,12 +40,12 @@ void test_trusted() { } void test_marked_members() { - // A type whose only indeterminate scalars are [[uninitialized]] is trusted; + // A type whose only indeterminate scalars are [[uninit]] is trusted; // those members are acknowledged uninitialized (paper §6.2), even through a // nesting level. A mixed type still fires for its unmarked scalar. MarkedMember a; NestedMarked b; - MixedMarked c; // expected-error {{variable 'c' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + MixedMarked c; // expected-error {{variable 'c' must be initialized or marked '[[uninit]]' under profile 'std::init'}} (void)a; (void)b; (void)c; } @@ -53,12 +53,12 @@ void test_arrays() { // An automatic array of a class type whose default-init leaves a scalar // subobject indeterminate is diagnosed via the base element type (paper // section 6). - Trivial a[3]; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} - Trivial b[2][3]; // expected-error {{variable 'b' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} - int c[5]; // expected-error {{variable 'c' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Trivial a[3]; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + Trivial b[2][3]; // expected-error {{variable 'b' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + int c[5]; // expected-error {{variable 'c' must be initialized or marked '[[uninit]]' under profile 'std::init'}} Trivial d[2] = {}; Trivial e[2] = {{1}, {2}}; - [[uninitialized]] Trivial f[3]; + [[uninit]] Trivial f[3]; WithCtor g[3]; AllInit h[3]; (void)a; (void)b; (void)c; (void)d; (void)e; (void)f; (void)g; (void)h; diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 946c361e84464..7f20ae8bcf9de 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -6,8 +6,8 @@ struct WithCtor { WithCtor(); }; struct Inner { int y; }; -struct InnerMarked { int y [[uninitialized]]; }; -struct InnerMixed { int a [[uninitialized]]; int b; }; +struct InnerMarked { int y [[uninit]]; }; +struct InnerMixed { int a [[uninit]]; int b; }; struct MissingMember { int x; // expected-note {{member 'x' declared here}} @@ -25,7 +25,7 @@ struct DefaultMemberInit { }; struct Marked { - int x [[uninitialized]]; + int x [[uninit]]; Marked() {} }; @@ -40,7 +40,7 @@ struct NestedAggregate { NestedAggregate() {} // expected-error {{constructor does not initialize member 'm' under profile 'std::init'}} }; -// A member whose type's only indeterminate scalar is [[uninitialized]] is +// A member whose type's only indeterminate scalar is [[uninit]] is // acknowledged (paper §6.2), so the constructor need not initialize it. struct NestedMarkedMember { InnerMarked m; diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index a008d1864e1e0..20ab9fab1798c 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -19,45 +19,45 @@ void test_byte() { std::byte a; std::byte buf[8]; ByteWrap w; - ByteAndInt m; // expected-error {{variable 'm' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + ByteAndInt m; // expected-error {{variable 'm' must be initialized or marked '[[uninit]]' under profile 'std::init'}} (void)a; (void)buf; (void)w; (void)m; } void test_scalars() { - int a; // expected-error {{variable 'a' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + int a; // expected-error {{variable 'a' must be initialized or marked '[[uninit]]' under profile 'std::init'}} int b = 0; - int c [[uninitialized]]; + int c [[uninit]]; int d{}; int e = sink(1); (void)b; (void)c; (void)d; (void)e; } void test_pointer() { - int* p; // expected-error {{variable 'p' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + int* p; // expected-error {{variable 'p' must be initialized or marked '[[uninit]]' under profile 'std::init'}} int* q = nullptr; // A pointer cannot be left uninitialized (paper section 4.1); the marker is // rejected rather than excusing it. - int* r [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + int* r [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} (void)q; (void)r; } struct PtrMember { - int* p [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + int* p [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} }; void test_pointer_marker_suppressed() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} - [[profiles::suppress(std::init)]] int* p [[uninitialized]]; + [[profiles::suppress(std::init)]] int* p [[uninit]]; // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} - [[profiles::suppress(std::init, rule: "pointer_marker")]] int* q [[uninitialized]]; + [[profiles::suppress(std::init, rule: "pointer_marker")]] int* q [[uninit]]; (void)p; (void)q; } enum E { E0, E1 }; void test_enum() { - E x; // expected-error {{variable 'x' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + E x; // expected-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} E y = E0; - E z [[uninitialized]]; + E z [[uninit]]; (void)y; (void)z; } @@ -75,7 +75,7 @@ void test_class_trivial() { // member indeterminate is diagnosed by R5 (uninit_decl), the §6 // "classes without constructors" rule. (Detailed coverage lives in // safety-profile-init-aggregate.cpp.) - Trivial t; // expected-error {{variable 't' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + Trivial t; // expected-error {{variable 't' must be initialized or marked '[[uninit]]' under profile 'std::init'}} (void)t; } @@ -94,7 +94,7 @@ void test_param(int p) { } void test_marker_then_assign() { - int x [[uninitialized]]; + int x [[uninit]]; x = 7; (void)x; } @@ -116,7 +116,7 @@ void test_suppress_block() { template void template_uninit() { - T x; // expected-error {{variable 'x' must be initialized or marked '[[uninitialized]]' under profile 'std::init'}} + T x; // expected-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} (void)x; } template void template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 64f904ee9b3e2..c3d4c7cecf0c9 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -5,58 +5,58 @@ [[profiles::enforce(std::init)]]; struct PlainField { - int m [[uninitialized]]; + int m [[uninit]]; }; struct PlainFieldPrefix { - [[uninitialized]] int m; + [[uninit]] int m; }; struct FieldWithNSDMI { - int m [[uninitialized]] = 0; // expected-error {{variable 'm' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + int m [[uninit]] = 0; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; struct FieldWithNSDMIPrefix { - [[uninitialized]] int m = 0; // expected-error {{variable 'm' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + [[uninit]] int m = 0; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; struct WithStaticDataMember { - static int s [[uninitialized]]; - [[uninitialized]] static int t; + static int s [[uninit]]; + [[uninit]] static int t; }; int WithStaticDataMember::s; int WithStaticDataMember::t; struct MultipleFields { - int a [[uninitialized]]; + int a [[uninit]]; int b = 0; - int c [[uninitialized]] = 0; // expected-error {{variable 'c' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + int c [[uninit]] = 0; // expected-error {{variable 'c' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; template struct DependentField { - T m [[uninitialized]]; + T m [[uninit]]; }; template struct DependentField; -// expected-error@+2 {{'uninitialized' attribute only applies to variables and non-static data members}} -// no-profiles-error@+1 {{'uninitialized' attribute only applies to variables and non-static data members}} -[[uninitialized]] void f(); +// expected-error@+2 {{'uninit' attribute only applies to variables and non-static data members}} +// no-profiles-error@+1 {{'uninit' attribute only applies to variables and non-static data members}} +[[uninit]] void f(); // Subjects on which "leave uninitialized" is meaningless are rejected // regardless of -fprofiles. struct ReferenceField { - int &r [[uninitialized]]; // expected-error {{'uninitialized' attribute cannot be applied to a reference}} \ - // no-profiles-error {{'uninitialized' attribute cannot be applied to a reference}} + int &r [[uninit]]; // expected-error {{'uninit' attribute cannot be applied to a reference}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a reference}} }; -void test_invalid_subjects(int p [[uninitialized]]) { // expected-error {{'uninitialized' attribute cannot be applied to a function parameter}} \ - // no-profiles-error {{'uninitialized' attribute cannot be applied to a function parameter}} +void test_invalid_subjects(int p [[uninit]]) { // expected-error {{'uninit' attribute cannot be applied to a function parameter}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a function parameter}} int n = 0; - int &lr [[uninitialized]] = n; // expected-error {{'uninitialized' attribute cannot be applied to a reference}} \ - // no-profiles-error {{'uninitialized' attribute cannot be applied to a reference}} + int &lr [[uninit]] = n; // expected-error {{'uninit' attribute cannot be applied to a reference}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a reference}} int arr[2] = {1, 2}; - [[uninitialized]] auto [a, b] = arr; // expected-error {{'uninitialized' attribute cannot be applied to a structured binding}} \ - // no-profiles-error {{'uninitialized' attribute cannot be applied to a structured binding}} + [[uninit]] auto [a, b] = arr; // expected-error {{'uninit' attribute cannot be applied to a structured binding}} \ + // no-profiles-error {{'uninit' attribute cannot be applied to a structured binding}} (void)p; (void)lr; (void)a; (void)b; } diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index f307e0e8498bc..635accd27f92b 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -27,13 +27,13 @@ int leading_unrelated_error = undeclared_identifier; // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] void test_suppress_decl() { - int x [[uninitialized]]; + int x [[uninit]]; int y = x; (void)y; } void test_suppress_stmt_inner() { - int x [[uninitialized]]; + int x [[uninit]]; // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] { @@ -43,7 +43,7 @@ void test_suppress_stmt_inner() { } void test_suppress_var_init() { - int x [[uninitialized]]; + int x [[uninit]]; // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] [[profiles::suppress(test::uninit_read)]] @@ -52,7 +52,7 @@ void test_suppress_var_init() { } void test_suppress_rule_targeted() { - int x [[uninitialized]]; + int x [[uninit]]; // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} // no-profiles-warning@+2 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "uninit_read")]] @@ -63,7 +63,7 @@ void test_suppress_rule_targeted() { } void test_marker_then_write_then_read() { - int x [[uninitialized]]; + int x [[uninit]]; x = 7; int y = x; (void)y; @@ -76,13 +76,13 @@ void test_param(int p) { #ifndef DEMOTE void test_marker_does_not_excuse_read() { - int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + int x [[uninit]]; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; } void test_suppress_stmt_outer() { - int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + int x [[uninit]]; // expected-note {{variable 'x' is declared here}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] { int y = x; @@ -94,7 +94,7 @@ void test_suppress_stmt_outer() { template T template_uninit() { - T x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + T x [[uninit]]; // expected-note {{variable 'x' is declared here}} return x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} } void instantiate_template_uninit() { @@ -102,7 +102,7 @@ void instantiate_template_uninit() { } void test_selective_suppress() { - int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + int x [[uninit]]; // expected-note {{variable 'x' is declared here}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::other)]] { int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} @@ -112,7 +112,7 @@ void test_selective_suppress() { void test_decl_suppress_does_not_extend() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} - [[profiles::suppress(std::init)]] int x [[uninitialized]]; // expected-note {{variable 'x' is declared here}} + [[profiles::suppress(std::init)]] int x [[uninit]]; // expected-note {{variable 'x' is declared here}} int y = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; } @@ -120,7 +120,7 @@ void test_decl_suppress_does_not_extend() { // std::byte may be read while uninitialized (paper section 4), so std::init // does not diagnose a read of an uninitialized std::byte. void test_byte_read_exempt() { - std::byte b [[uninitialized]]; + std::byte b [[uninit]]; std::byte c = b; (void)c; } @@ -131,7 +131,7 @@ void test_byte_read_exempt() { // test::uninit_read fire first; suppressing it at the use site lets the // std::init diagnostic surface. void test_demote_test_profile() { - int x [[uninitialized]]; // demote-note {{variable 'x' is declared here}} + int x [[uninit]]; // demote-note {{variable 'x' is declared here}} [[profiles::suppress(test::uninit_read)]] { int y = x; // demote-error {{variable 'x' is read before initialization under profile 'std::init'}} (void)y; @@ -141,7 +141,7 @@ void test_demote_test_profile() { // The std::byte exemption is std::init-only: test::uninit_read still diagnoses // a read of an uninitialized std::byte. void test_byte_not_exempt_under_test_profile() { - std::byte b [[uninitialized]]; // demote-note {{variable 'b' is declared here}} + std::byte b [[uninit]]; // demote-note {{variable 'b' is declared here}} std::byte c = b; // demote-error {{variable 'b' is read before initialization under profile 'test::uninit_read'}} (void)c; } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index e6220d8a7c6fc..7a4e92764be78 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -9,8 +9,8 @@ [[profiles::enforce(std::init)]]; int g_init = 0; -[[uninitialized]] int g_uninit; -[[uninitialized]] int g_uninit_arr[3]; +[[uninit]] int g_uninit; +[[uninit]] int g_uninit_arr[3]; [[ref_to_uninit]] int *allocate(int n); [[ref_to_uninit]] void *alloc_void(); [[ref_to_uninit]] int &get_uninit_ref(); @@ -145,7 +145,7 @@ void test_call_arguments() { // The worked example from paper §5. int a1[] = {1, 2, 3}; - [[uninitialized]] int a2[3]; + [[uninit]] int a2[3]; uninitialized_fill(a1, 10); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} uninitialized_fill(a2, 10); // OK diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index 9274804e4c11b..cfaf9038d9acd 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -6,21 +6,21 @@ union U { int x; float y; }; -U g_union [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a variable of union type under profile 'std::init'}} +U g_union [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} void test_union_var() { - U a [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a variable of union type under profile 'std::init'}} + U a [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} (void)a; } void test_union_var_suppressed() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} - [[profiles::suppress(std::init)]] U a [[uninitialized]]; + [[profiles::suppress(std::init)]] U a [[uninit]]; (void)a; } union MarkedMember { - int x [[uninitialized]]; // expected-error {{'[[uninitialized]]' cannot be applied to a union member under profile 'std::init'}} + int x [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a union member under profile 'std::init'}} float y; }; @@ -29,7 +29,7 @@ union MarkedMember { // A non-union class member may carry the marker (it is not banned here). struct NotUnion { - int x [[uninitialized]]; + int x [[uninit]]; }; union WithNSDMI { int x = 0; float y; }; diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index ea36f7ce356fe..e2f4069123216 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -5,27 +5,27 @@ [[profiles::enforce(std::init)]]; void test_postfix_eq() { - int c [[uninitialized]] = 0; // expected-error {{variable 'c' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + int c [[uninit]] = 0; // expected-error {{variable 'c' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)c; } void test_prefix_eq() { - [[uninitialized]] int d = 7; // expected-error {{variable 'd' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + [[uninit]] int d = 7; // expected-error {{variable 'd' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)d; } void test_brace_init() { - int e [[uninitialized]] {}; // expected-error {{variable 'e' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + int e [[uninit]] {}; // expected-error {{variable 'e' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)e; } void test_paren_init() { - int f [[uninitialized]] (3); // expected-error {{variable 'f' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + int f [[uninit]] (3); // expected-error {{variable 'f' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)f; } void test_marker_alone() { - int x [[uninitialized]]; + int x [[uninit]]; (void)x; } @@ -39,19 +39,19 @@ struct WithCtor { WithCtor(); }; void test_class_synthesized_init() { // The implicit default-constructor call is the initializer, so the marker // contradicts it even though there is no explicit initializer. - WithCtor x [[uninitialized]]; // expected-error {{variable 'x' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} - [[uninitialized]] WithCtor y; // expected-error {{variable 'y' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + WithCtor x [[uninit]]; // expected-error {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + [[uninit]] WithCtor y; // expected-error {{variable 'y' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)x; (void)y; } -struct MarkedMemberAgg { int x [[uninitialized]]; }; +struct MarkedMemberAgg { int x [[uninit]]; }; void test_marked_member_agg() { // R4 stays factual: MarkedMemberAgg's default-initialization is a genuine // no-op that leaves x indeterminate, so marking the variable too is // consistent rather than a contradiction. (Honoring the member marker here // would wrongly fire uninit_with_initializer.) - MarkedMemberAgg a [[uninitialized]]; + MarkedMemberAgg a [[uninit]]; (void)a; } @@ -63,25 +63,25 @@ void test_failed_init_no_double_diag() { // user-written initializer, so R4 must not pile a spurious diagnostic on top // of the real error (the absence of an extra '...have an initializer...' // diagnostic here is the assertion). - NoDefaultCtor z [[uninitialized]]; // expected-error {{call to deleted constructor of 'NoDefaultCtor'}} \ + NoDefaultCtor z [[uninit]]; // expected-error {{call to deleted constructor of 'NoDefaultCtor'}} \ // no-profiles-error {{call to deleted constructor of 'NoDefaultCtor'}} (void)z; } -int g_marker_with_init [[uninitialized]] = 42; // expected-error {{variable 'g_marker_with_init' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} +int g_marker_with_init [[uninit]] = 42; // expected-error {{variable 'g_marker_with_init' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} void test_suppress() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] - int x [[uninitialized]] = 0; + int x [[uninit]] = 0; (void)x; } void test_suppress_block() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] { - int a [[uninitialized]] = 0; - int b [[uninitialized]] = 1; + int a [[uninit]] = 0; + int b [[uninit]] = 1; (void)a; (void)b; } } @@ -90,20 +90,20 @@ void test_suppress_block() { // check that runs when its NSDMI is finalized, not just its parsing. struct WithSuppressedNSDMI { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} - [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] int m [[uninitialized]] = 0; // OK: rule-targeted suppress + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] int m [[uninit]] = 0; // OK: rule-targeted suppress // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} - [[profiles::suppress(std::init)]] int n [[uninitialized]] = 1; // OK: whole-profile suppress + [[profiles::suppress(std::init)]] int n [[uninit]] = 1; // OK: whole-profile suppress }; // A suppress on the enclosing record covers its members' NSDMIs. // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} struct [[profiles::suppress(std::init)]] WithClassLevelSuppressedNSDMI { - int m [[uninitialized]] = 0; // OK: suppressed by the class-level attribute + int m [[uninit]] = 0; // OK: suppressed by the class-level attribute }; template void template_marker_with_init() { - T x [[uninitialized]] = T{}; // expected-error 2 {{variable 'x' cannot be both '[[uninitialized]]' and have an initializer under profile 'std::init'}} + T x [[uninit]] = T{}; // expected-error 2 {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)x; } template void template_marker_with_init(); // expected-note {{in instantiation of function template specialization 'template_marker_with_init' requested here}} From 1ef46308ed0cb606d175f5ddbfc0d076af48b86f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 15:43:50 -0400 Subject: [PATCH 115/289] Document the new-expression and static [[uninit]] gaps in the std::init docs The std::init slice does not diagnose uninitialized free-store objects (e.g. new int) because there is no new-expression check site, and it silently accepts a [[uninit]] marker on a non-local static or thread-local variable even though such an object is zero-initialized (contradicting paper section 4.2). Record both as known gaps. --- clang/docs/ProfilesFramework.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index fd97cf3368cca..05e294b3642f3 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -637,6 +637,12 @@ CFG-based pass. Until it lands, a ``[[uninit]]`` data member is trusted, and writes *through* a ``[[ref_to_uninit]]`` pointer/reference are not yet verified (the paper relegates them to ``construct_at`` or suppression). +Dynamically-created objects are also not covered: a ``new`` expression whose +default-initialization leaves a scalar subobject indeterminate (e.g. +``new int``, paper §1.2 / §2) has no check site, so only automatic-storage +(``uninit_decl``, R2) and non-local static (``static_runtime_init``, R3) +definitions are checked -- the dynamic-storage analogue is not yet implemented. + The slice introduces two marker attributes and the rules below. Marker attributes @@ -736,6 +742,13 @@ constructor is trusted; static / thread storage duration is excluded whose only indeterminate scalars are all marked is trusted (e.g. ``struct A { int x [[uninit]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. +- Known gap: because static / thread storage is excluded here and + ``uninit_with_initializer`` (R4) sees no initializer for a zero-initialized + object, a ``[[uninit]]`` marker on a non-local static or thread-local + variable (e.g. ``static int x [[uninit]];``) is silently accepted -- even + though such an object is zero-initialized by language rule, so marking it + ``[[uninit]]`` contradicts paper §4.2 ("an initialized object marked + ``[[uninit]]`` is an error"). R3. ``static_runtime_init`` -- pattern 1 ......................................... From ba0955646d4cff774a7791b34f7177dd0c273f03 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 16:13:27 -0400 Subject: [PATCH 116/289] Recognize an uninitialized new-expression as a source of uninitialized memory under std::init Paper section 1.2 / 4.3: a new-expression that default-initializes its allocated object and leaves a scalar subobject indeterminate (e.g. new int, new int[n]) yields uninitialized free-store memory, grouped with malloc. The ref_to_uninit (R7) recognizer did not treat it as such, so binding it to a pointer was unchecked -- a gap relative to automatic (R2) and non-local static (R3) storage. Add a CXXNewExpr arm to pointerRefersToUninitStorage that reuses defaultInitLeavesScalarIndeterminate, threading ASTContext through the two mutually-recursive recognizer helpers. The arm gates on the new-initialization style being None (default-initialization) rather than hasInitializer(): default-initializing a class synthesizes a (possibly trivial) constructor call, so hasInitializer() is true even for an indeterminate aggregate like new Agg. new T(...) / new T{} are value- or list-initialized, and a user-provided default constructor is trusted, so neither is recognized. The std::byte exemption and the HonorUninitMarkers object-vs-member model are inherited from the shared helper. All five R7 check sites and both diagnostics are reused; no new rule or diagnostic. --- clang/include/clang/Sema/Sema.h | 8 +- clang/lib/Sema/SemaDecl.cpp | 42 +++++++--- .../safety-profile-init-ref-to-uninit.cpp | 83 +++++++++++++++++++ 3 files changed, 117 insertions(+), 16 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 9c729d85cc540..3192c5fffc02d 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1130,9 +1130,11 @@ class Sema final : public SemaBase { /// syntactic form -- the address of, or a subobject of, a [[uninit]] /// entity; a value of a [[ref_to_uninit]] pointer/reference or array; a /// dereference of such a pointer; a cast of such a pointer to another pointer - /// type, or of such a glvalue to another reference; or a call to a - /// [[ref_to_uninit]]-returning function. Anything else is treated as - /// initialized (the trust model; no flow analysis). + /// type, or of such a glvalue to another reference; a call to a + /// [[ref_to_uninit]]-returning function; or a new-expression whose + /// default-initialization leaves the allocated object indeterminate (e.g. + /// new int). Anything else is treated as initialized (the trust model; no + /// flow analysis). bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; /// std::init / ref_to_uninit (paper §5): check that the initialization of a diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 0df95df0e0a9f..229302e771197 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14952,22 +14952,22 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, // analysis and no type-system tracking. Uninitialized storage is only ever // introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker; // anything unrecognized is treated as initialized (the trust model). -static bool glvalueDenotesUninitStorage(const Expr *E); +static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E); // \p E is a pointer prvalue. True if it points to uninitialized storage. -static bool pointerRefersToUninitStorage(const Expr *E) { +static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E) { if (!E) return false; E = E->IgnoreParenImpCasts(); // Array-to-pointer decay has been stripped above, leaving the array glvalue. if (E->getType()->isArrayType()) - return glvalueDenotesUninitStorage(E); + return glvalueDenotesUninitStorage(Ctx, E); // &G, where G denotes uninitialized storage. if (const auto *UO = dyn_cast(E)) if (UO->getOpcode() == UO_AddrOf) - return glvalueDenotesUninitStorage(UO->getSubExpr()); + return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr()); // A value of a [[ref_to_uninit]] pointer, or a call to a // [[ref_to_uninit]]-returning function. @@ -14979,19 +14979,35 @@ static bool pointerRefersToUninitStorage(const Expr *E) { if (const FunctionDecl *FD = CE->getDirectCallee()) return FD->hasAttr(); + // A default-initialized new-expression (none init style: no initializer + // written) whose allocated type's default-initialization leaves a scalar + // subobject indeterminate produces uninitialized free-store memory (paper + // §1.2/§4.3), like a [[ref_to_uninit]] allocator. The style gates this rather + // than hasInitializer(), which is also true for new Agg -- default- + // initializing a class synthesizes a (possibly trivial) constructor call. + // new T(...) / new T{} are value- or list-initialized; a user-provided + // default constructor is trusted by defaultInitLeavesScalarIndeterminate. + if (const auto *NE = dyn_cast(E)) { + if (NE->getInitializationStyle() != CXXNewInitializationStyle::None) + return false; + llvm::SmallPtrSet Visited; + return defaultInitLeavesScalarIndeterminateImpl( + Ctx, NE->getAllocatedType(), /*HonorUninitMarkers=*/true, Visited); + } + // Paper §4.3: a [[ref_to_uninit]] pointer cast to another pointer type is // itself [[ref_to_uninit]]. Implicit casts were already stripped above, so // this only looks through an explicit pointer-to-pointer cast; a pointer // manufactured from an integer (operand not a pointer) is not propagated. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->getType()->isPointerType()) - return pointerRefersToUninitStorage(CE->getSubExpr()); + return pointerRefersToUninitStorage(Ctx, CE->getSubExpr()); return false; } // \p E is a glvalue. True if it denotes uninitialized storage. -static bool glvalueDenotesUninitStorage(const Expr *E) { +static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E) { if (!E) return false; E = E->IgnoreParenImpCasts(); @@ -15010,33 +15026,33 @@ static bool glvalueDenotesUninitStorage(const Expr *E) { // a->m reaches m through the pointer a (object *a); a.m through the // glvalue a. return DeclDenotesUninit(ME->getMemberDecl()) || - (ME->isArrow() ? pointerRefersToUninitStorage(ME->getBase()) - : glvalueDenotesUninitStorage(ME->getBase())); + (ME->isArrow() ? pointerRefersToUninitStorage(Ctx, ME->getBase()) + : glvalueDenotesUninitStorage(Ctx, ME->getBase())); // A call to a [[ref_to_uninit]]-returning reference function: the referent // it returns is uninitialized. Mirrors the pointer recognizer's call arm. if (const auto *CE = dyn_cast(E)) if (const FunctionDecl *FD = CE->getDirectCallee()) return FD->hasAttr(); if (const auto *ASE = dyn_cast(E)) - return pointerRefersToUninitStorage(ASE->getBase()); + return pointerRefersToUninitStorage(Ctx, ASE->getBase()); // *p, where p points to uninitialized storage. if (const auto *UO = dyn_cast(E)) if (UO->getOpcode() == UO_Deref) - return pointerRefersToUninitStorage(UO->getSubExpr()); + return pointerRefersToUninitStorage(Ctx, UO->getSubExpr()); // A reference cast (an explicit cast yielding a glvalue) denotes the same // storage as its operand; propagate. Symmetric to the pointer-cast arm. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->isGLValue()) - return glvalueDenotesUninitStorage(CE->getSubExpr()); + return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr()); return false; } bool Sema::refersToUninitializedMemory(const Expr *E, bool IsReference) const { - return IsReference ? glvalueDenotesUninitStorage(E) - : pointerRefersToUninitStorage(E); + return IsReference ? glvalueDenotesUninitStorage(Context, E) + : pointerRefersToUninitStorage(Context, E); } void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 7a4e92764be78..379f68466f4a9 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -217,3 +217,86 @@ void template_bad() { (void)p; } template void template_bad(); // expected-note {{in instantiation of function template specialization 'template_bad' requested here}} + +// A default-initialized new-expression that leaves a scalar subobject +// indeterminate (e.g. new int, new int[n], paper §1.2 / §4.3) is a source of +// uninitialized free-store memory; new T(...), new T{...}, and a type with a +// user-provided default constructor are initialized. +namespace std { enum class byte : unsigned char {}; } + +struct NewAgg { int x; }; +struct NewWithCtor { NewWithCtor(); int x; }; + +void test_new_scalar() { + int *n1 [[ref_to_uninit]] = new int; // OK + int *n2 = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *n3 = new int(5); // OK + int *n4 = new int(); // OK + int *n5 = new int{}; // OK + int *n6 [[ref_to_uninit]] = new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *n7 [[ref_to_uninit]] = new int(); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)n1; (void)n2; (void)n3; (void)n4; (void)n5; (void)n6; (void)n7; +} + +void test_new_array(int n) { + int *a1 [[ref_to_uninit]] = new int[10]; // OK + int *a2 = new int[10]; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *a3 [[ref_to_uninit]] = new int[n]; // OK + int *a4 = new int[n]; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a1; (void)a2; (void)a3; (void)a4; +} + +void test_new_class() { + NewWithCtor *c1 = new NewWithCtor; // OK: user-provided default ctor trusted + NewWithCtor *c2 [[ref_to_uninit]] = new NewWithCtor; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + NewAgg *a1 = new NewAgg; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + NewAgg *a2 [[ref_to_uninit]] = new NewAgg; // OK + std::byte *b1 = new std::byte; // OK: std::byte exemption inherited + std::byte *b2 [[ref_to_uninit]] = new std::byte; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)c1; (void)c2; (void)a1; (void)a2; (void)b1; (void)b2; +} + +struct NewInFields { + int *p1 [[ref_to_uninit]] = new int; // OK + int *p2 = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p3 = new int(5); // OK +}; + +void test_new_assignment() { + int *p [[ref_to_uninit]] = new int; + p = new int; // OK + p = new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *q = new int(0); + q = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + q = new int(0); // OK + (void)p; (void)q; +} + +void test_new_call_arguments() { + take_uninit_ptr(new int); // OK + take_uninit_ptr(new int(5)); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_ptr(new int); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ptr(new int(5)); // OK +} + +[[ref_to_uninit]] int *ret_new_uninit_ok() { return new int; } // OK +[[ref_to_uninit]] int *ret_new_uninit_bad() { + return new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +int *ret_new_ptr_bad() { + return new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +int *ret_new_ptr_ok() { return new int(0); } // OK + +void test_new_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *s = new int; // OK: suppressed + (void)s; +} + +template +void template_new_bad() { + T *p = new T; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_new_bad(); // expected-note {{in instantiation of function template specialization 'template_new_bad' requested here}} From aa61881ee41d9003c9ae3a3a124c7b5a628397d3 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 22 Jun 2026 16:15:14 -0400 Subject: [PATCH 117/289] Document new-expression ref_to_uninit recognition in the profiles framework docs The std::init slice now recognizes a default-initialized new-expression that leaves a scalar subobject indeterminate (e.g. new int, new int[n]) as a source of uninitialized memory. Replace the slice-intro gap note with a statement that dynamically-created objects are covered when bound, keeping a note that an unbound new whose result is discarded is still unchecked because R7 fires only at binding sites. Extend the R7 recognizer's "refers to uninitialized memory" list with the new-expression form (paper section 1.2 / 4.3) and add a bullet recording that recognition gates on default-initialization (none init style), reuses Sema::defaultInitLeavesScalarIndeterminate (trusting a user-provided default constructor and inheriting the std::byte exemption), and handles array new via getAllocatedType. --- clang/docs/ProfilesFramework.rst | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 05e294b3642f3..9f8be4c72a487 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -637,11 +637,12 @@ CFG-based pass. Until it lands, a ``[[uninit]]`` data member is trusted, and writes *through* a ``[[ref_to_uninit]]`` pointer/reference are not yet verified (the paper relegates them to ``construct_at`` or suppression). -Dynamically-created objects are also not covered: a ``new`` expression whose -default-initialization leaves a scalar subobject indeterminate (e.g. -``new int``, paper §1.2 / §2) has no check site, so only automatic-storage -(``uninit_decl``, R2) and non-local static (``static_runtime_init``, R3) -definitions are checked -- the dynamic-storage analogue is not yet implemented. +Dynamically-created objects are covered when bound: a ``new`` expression that +default-initializes its allocated object and leaves a scalar subobject +indeterminate (e.g. ``new int``, ``new int[n]``, paper §1.2 / §4.3) is +recognised as a source of uninitialized memory by ``ref_to_uninit`` (R7). An +*unbound* ``new`` whose result is discarded (``new int;``) is still unchecked, +because R7 fires only at binding sites. The slice introduces two marker attributes and the rules below. @@ -850,9 +851,11 @@ locally from the source expression's syntactic form (no flow analysis): the address of, or a subobject of, a ``[[uninit]]`` entity; a value of a ``[[ref_to_uninit]]`` pointer/reference or array; a dereference of such a pointer; a cast of such a pointer to another pointer type (paper §4.3), or of -such a glvalue to another reference; or a call to a -``[[ref_to_uninit]]``-returning function. Anything else is treated as -initialized (the trust model). The reference cast and the +such a glvalue to another reference; a call to a +``[[ref_to_uninit]]``-returning function; or a ``new`` expression that +default-initializes its allocated object and leaves a scalar subobject +indeterminate (e.g. ``new int``, ``new int[n]``, paper §1.2 / §4.3). Anything +else is treated as initialized (the trust model). The reference cast and the ``[[ref_to_uninit]]``-returning reference call are not spelled out by the paper but follow from the profile's guarantee that uninitialized objects are not used, and keep the pointer and reference recognizers symmetric. @@ -862,6 +865,14 @@ not used, and keep the pointer and reference recognizers symmetric. target, uninitialized source). - Recognizer + shared check: ``Sema::refersToUninitializedMemory`` and ``Sema::checkRefToUninitInit`` in ``clang/lib/Sema/SemaDecl.cpp``. +- A ``new`` expression is recognised only when it default-initializes its + object (none init style, no written initializer); ``new T(...)`` and + ``new T{...}`` are value- or list-initialized and excluded. Whether the + allocated type leaves a scalar indeterminate reuses + ``Sema::defaultInitLeavesScalarIndeterminate`` (R2), so a ``T`` with a + user-provided default constructor is trusted and the ``std::byte`` exemption + is inherited. ``getAllocatedType`` yields the element type, so array + ``new`` (``new int[n]``) is handled uniformly. - Check sites: variable initialization (``Sema::CheckCompleteVariableDeclaration``), default member initializers (``Sema::ActOnFinishCXXInClassMemberInitializer``), pointer assignment From c278f23e35e3e583964a551ca33500cadf52ea73 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 12:28:07 -0400 Subject: [PATCH 118/289] Document pointer_marker and union_marker usage in the profiles framework docs --- clang/docs/ProfilesFramework.rst | 34 ++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 9f8be4c72a487..6c652958411cc 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -674,8 +674,9 @@ regardless of ``-fprofiles``; its profile rules carry weight only when marker is accepted there (the object is genuinely left uninitialized). - Is banned on a pointer by ``pointer_marker`` (a pointer must be initialized, paper §4.1), and on a union object or member by - ``union_marker``. Both are gated on enforcement, and the marker is retained - after the diagnostic so ``uninit_decl`` does not re-diagnose the entity. + ``union_marker`` (see R6 / R8 below for usage examples). Both are gated on + enforcement, and the marker is retained after the diagnostic so + ``uninit_decl`` does not re-diagnose the entity. ``[[ref_to_uninit]]`` (also a standard C++11 attribute) marks a pointer, reference, or pointer/reference-returning function as referring to @@ -823,6 +824,22 @@ R6. ``union_marker`` -- attribute handler §6.5): delayed initialization by assigning a member would be an erroneous assignment when compiled without the profile. +.. code-block:: c++ + + union U { int x; float y; }; + + U a [[uninit]]; // error: [[uninit]] on a union variable (union_marker) + U b; // error: a union must be initialized (uninit_decl / err_init_uninit_union) + U c = {1}; // OK + U d{}; // OK + + union M { + int x [[uninit]]; // error: [[uninit]] on a union member (union_marker) + float y; + }; + + [[profiles::suppress(std::init)]] U e [[uninit]]; // OK + - Diagnostic: ``err_init_union_marker``. - Check site: the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. Unlike the reference / parameter / @@ -888,6 +905,19 @@ R8. ``pointer_marker`` -- attribute handler be uninitialized. The initialization profile requires the same for pointers." A pointer must instead be initialized (e.g. to ``nullptr``). +.. code-block:: c++ + + int *p; // error: must be initialized or marked [[uninit]] (uninit_decl) + int *q = nullptr; // OK: the prescribed fix + int *r [[uninit]]; // error: [[uninit]] cannot be applied to a pointer (pointer_marker) + + struct S { + int *p [[uninit]]; // error: also fires on a pointer data member + }; + + // Opt out if genuinely required: + [[profiles::suppress(std::init, rule: "pointer_marker")]] int *x [[uninit]]; // OK + - Diagnostic: ``err_init_uninit_pointer_marker``. - Check site: the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp``, alongside ``union_marker``. Like that From 89c7817e555518dceb5b64921f5095b9ab91724b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 15:58:40 -0400 Subject: [PATCH 119/289] Evaluate std::init parse-time rules on instantiations, not template patterns Skip the Decl-based profile checks for templated entities in shouldEmitProfileViolation so they fire once at instantiation instead of also firing on the template pattern (P3589R2: effects apply after phase 7). --- clang/lib/Sema/Sema.cpp | 10 ++++++ .../safety-profile-init-with-initializer.cpp | 32 ++++++++++++++++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 39db56411ed52..bd0f58e80af4c 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3174,6 +3174,16 @@ bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, // outcome of overload resolution or template instantiation, nor is it // possible to 'SFINAE out' failure of a program to satisfy a profile // requirement." + // + // A templated entity is not yet a phase-7 entity, so a profile rule must fire + // only on its instantiation -- where D is the instantiated, non-templated + // declaration -- not on the template pattern. Checking the pattern too would + // diagnose never-instantiated templates and double-fire (once when the + // pattern is parsed and again at each instantiation). Decl-less check sites + // (the [[uninit]] / [[ref_to_uninit]] markers and casts) pass D == nullptr; + // they run once at parse time and are unaffected. + if (D && D->isTemplated()) + return false; if (isUnevaluatedContext()) return false; if (currentEvaluationContext().isDiscardedStatementContext()) diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index e2f4069123216..763906b227244 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -101,9 +101,39 @@ struct [[profiles::suppress(std::init)]] WithClassLevelSuppressedNSDMI { int m [[uninit]] = 0; // OK: suppressed by the class-level attribute }; +// A profile rule fires on the instantiation, not on the template pattern, so a +// dependent [[uninit]]-with-initializer is diagnosed exactly once. template void template_marker_with_init() { - T x [[uninit]] = T{}; // expected-error 2 {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + T x [[uninit]] = T{}; // expected-error {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} (void)x; } template void template_marker_with_init(); // expected-note {{in instantiation of function template specialization 'template_marker_with_init' requested here}} + +// A *non-dependent* declaration inside a template body is likewise diagnosed +// once -- at instantiation -- not on the pattern. +template +void template_nondependent_with_init() { + int x [[uninit]] = 0; // expected-error {{variable 'x' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)x; +} +template void template_nondependent_with_init(); // expected-note {{in instantiation of function template specialization 'template_nondependent_with_init' requested here}} + +// An uninstantiated template pattern is not yet a phase-7 entity, so no profile +// rule fires on it (no expected diagnostic here). +template +void template_never_instantiated() { + int x [[uninit]] = 0; + (void)x; +} + +// An NSDMI in a class template is checked once, when its initializer is +// instantiated (here forced by initializing the specialization). +template +struct WithTemplatedNSDMI { + T m [[uninit]] = T{}; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; +void use_templated_nsdmi() { + WithTemplatedNSDMI w = {}; // expected-note {{in instantiation of default member initializer 'WithTemplatedNSDMI::m' requested here}} + (void)w; +} From b9b29b53f89c3f32a01893fdddf1c0314323bc06 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 15:58:56 -0400 Subject: [PATCH 120/289] Add template regression tests for std::init parse-time rules Cover non-dependent declarations inside instantiated templates for the uninit_decl and ref_to_uninit rules, which previously double-fired. --- clang/test/SemaCXX/safety-profile-init-decl.cpp | 16 ++++++++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index 20ab9fab1798c..8decee1d1e67f 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -120,3 +120,19 @@ void template_uninit() { (void)x; } template void template_uninit(); // expected-note {{in instantiation of function template specialization 'template_uninit' requested here}} + +// A *non-dependent* uninitialized local in a template body is diagnosed once -- +// at instantiation -- not on the template pattern (no double-fire). +template +void template_uninit_nondependent() { + int x; // expected-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)x; +} +template void template_uninit_nondependent(); // expected-note {{in instantiation of function template specialization 'template_uninit_nondependent' requested here}} + +// An uninstantiated template pattern never reaches phase 7, so no rule fires. +template +void template_uninit_never_instantiated() { + int x; + (void)x; +} diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 379f68466f4a9..b7064edd4d9ce 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -218,6 +218,15 @@ void template_bad() { } template void template_bad(); // expected-note {{in instantiation of function template specialization 'template_bad' requested here}} +// A *non-dependent* pointer bound to uninitialized memory inside a template +// body is diagnosed once, at instantiation, not on the pattern (no double-fire). +template +void template_nondependent_bad() { + int *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_nondependent_bad(); // expected-note {{in instantiation of function template specialization 'template_nondependent_bad' requested here}} + // A default-initialized new-expression that leaves a scalar subobject // indeterminate (e.g. new int, new int[n], paper §1.2 / §4.3) is a source of // uninitialized free-store memory; new T(...), new T{...}, and a type with a From 690e9e244b44c27f23b91be2527cccd3429536e4 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 16:26:59 -0400 Subject: [PATCH 121/289] Diagnose uninitialized base subobjects under std::init ctor_uninit_member A user-provided constructor that leaves a direct non-virtual base-class subobject's scalar indeterminate is now diagnosed, since the std::init guarantee is over the complete object and a base cannot carry an [[uninit]] marker. Virtual bases remain deferred (most-derived responsibility). --- clang/docs/ProfilesFramework.rst | 24 ++++++++--- .../clang/Basic/DiagnosticSemaKinds.td | 4 ++ clang/lib/Sema/SemaDeclCXX.cpp | 41 +++++++++++++++++-- 3 files changed, 60 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 6c652958411cc..85dc83515e60a 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -799,10 +799,19 @@ body does not count. A member whose own default-initialization leaves an *unacknowledged* scalar subobject indeterminate (a nested aggregate) is flagged as well; a member whose type's indeterminate scalars are all themselves marked ``[[uninit]]`` is trusted (the same -``HonorUninitMarkers`` walk as R2, paper §6.2). +``HonorUninitMarkers`` walk as R2, paper §6.2). A direct non-virtual +base-class subobject left indeterminate is flagged the same way: the +guarantee is over the *complete object* (paper §5.1, §7.1), and -- unlike a +member -- a base cannot carry an ``[[uninit]]`` marker, so it must always be +initialized. A written base-initializer (``: Base(...)`` / ``: Base{}``) or +a base with a user-provided default constructor is trusted. - Diagnostic: ``err_init_ctor_uninit_member`` (with a - ``note_init_uninit_member_here`` note at the member). + ``note_init_uninit_member_here`` note at the member); for a base subobject, + ``err_init_ctor_uninit_base`` (with a ``note_init_uninit_base_here`` note). + Both share the ``ctor_uninit_member`` rule name, so one + ``[[profiles::suppress(std::init, rule: "ctor_uninit_member")]]`` covers + members and bases alike. - Opt-in table: ``ConstructorFinalizationProfiles`` (pattern 4). - Reference and const members keep their existing dedicated diagnostics; anonymous-aggregate members and unnamed bit-fields are skipped (named @@ -811,10 +820,13 @@ themselves marked ``[[uninit]]`` is trusted (the same mutually exclusive, so a constructor initializes at most one (paper §6.5; see R6). A union *data member* of a non-union class is still checked, and must be initialized via the member-initializer list. -- Known gaps: base-class subobjects of the constructed class are not checked - -- only direct non-static data members -- so a user-provided constructor - that leaves a base subobject uninitialized (paper §6.1) is not diagnosed. - A const member is skipped here but is treated as indeterminate by +- Known gaps: *virtual* base-class subobjects are not checked. A virtual + base is initialized by the most-derived constructor, which is not a local + property of the constructor being checked, so flagging an intermediate + constructor would push a redundant (and possibly surprising) ``: V()`` onto + code that correctly relies on the most-derived class; under-diagnosing here + is the safer, paper-consistent default. Direct non-virtual bases are + checked. A const member is skipped here but is treated as indeterminate by ``defaultInitLeavesScalarIndeterminate`` (R2). R6. ``union_marker`` -- attribute handler diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 1af0060035d85..337a5ff0e7358 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14172,6 +14172,10 @@ def err_init_ctor_uninit_member : ProfileRuleError< "constructor does not initialize member %1 under profile '%0'">; def note_init_uninit_member_here : Note< "member %0 declared here">; +def err_init_ctor_uninit_base : ProfileRuleError< + "constructor does not initialize base class %1 under profile '%0'">; +def note_init_uninit_base_here : Note< + "base class %0 declared here">; def err_init_ref_to_uninit_requires_uninit : ProfileRuleError< "%select{pointer|reference}1 marked '[[ref_to_uninit]]' must refer to " "uninitialized memory under profile '%0'">; diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 29bc00ad9a419..2de8ba7c13c35 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7087,12 +7087,21 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { if (Ctor->getParent()->isUnion()) return; - // Members given a written member-initializer by this constructor. + // Members and direct bases given a written initializer by this constructor. llvm::SmallPtrSet Written; - for (const CXXCtorInitializer *Init : Ctor->inits()) - if (Init->isWritten() && Init->isAnyMemberInitializer()) + llvm::SmallPtrSet WrittenBases; + for (const CXXCtorInitializer *Init : Ctor->inits()) { + if (!Init->isWritten()) + continue; + if (Init->isAnyMemberInitializer()) { if (const FieldDecl *F = Init->getAnyMember()) Written.insert(F); + } else if (Init->isBaseInitializer()) { + if (const Type *T = Init->getBaseClass()) + WrittenBases.insert( + S.Context.getCanonicalType(QualType(T, 0)).getTypePtr()); + } + } for (const FieldDecl *F : Ctor->getParent()->fields()) { // Anonymous aggregate members and unnamed bit-fields are skipped; a named @@ -7115,6 +7124,32 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { S.Diag(F->getLocation(), diag::note_init_uninit_member_here) << F->getDeclName(); } + + // The guarantee is over the complete object (paper §5.1, §7.1), so a + // direct base-class subobject left indeterminate is as much a violation as a + // member. A base cannot carry an [[uninit]] marker (the attribute's subjects + // are Var/Field), so an indeterminate base must always be initialized -- there + // is no marker escape. Virtual bases are the most-derived constructor's + // responsibility, not a local property of this constructor, so they are + // deferred. A written base-initializer initializes the base; an implicit + // (non-written) one is default-init, handled by the indeterminate check. + for (const CXXBaseSpecifier &Base : Ctor->getParent()->bases()) { + if (Base.isVirtual()) + continue; + if (WrittenBases.count( + S.Context.getCanonicalType(Base.getType()).getTypePtr())) + continue; + if (!S.defaultInitLeavesScalarIndeterminate(Base.getType(), + /*HonorUninitMarkers=*/true)) + continue; + if (!S.shouldEmitProfileViolation("std::init", "ctor_uninit_member", + Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_base) + << "std::init" << Base.getType(); + S.Diag(Base.getBeginLoc(), diag::note_init_uninit_base_here) + << Base.getType(); + } } // Class-finalization opt-in table (pattern 3). From d2f4c50034690192e828abe66fdff88c2ac185c6 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 16:27:08 -0400 Subject: [PATCH 122/289] Test std::init ctor_uninit_member coverage of base subobjects Cover uninitialized/initialized/trusted/marked bases, mixed base+member, multiple bases, out-of-line and template constructors, rule suppression, and the deferred virtual-base case. --- .../test/SemaCXX/safety-profile-init-ctor.cpp | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 7f20ae8bcf9de..15d4e91087dac 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -102,3 +102,64 @@ struct [[profiles::suppress(std::init)]] SuppressedClass { int x; SuppressedClass() {} }; + +struct Base { int b; }; + +struct UninitBase : Base { // expected-note {{base class 'Base' declared here}} + UninitBase() {} // expected-error {{constructor does not initialize base class 'Base' under profile 'std::init'}} +}; + +struct BaseInitParen : Base { + BaseInitParen() : Base() {} +}; + +struct BaseInitBraces : Base { + BaseInitBraces() : Base{} {} +}; + +// A base with a user-provided default constructor is trusted. +struct TrustedBaseCtor : WithCtor { + TrustedBaseCtor() {} +}; + +// A base whose only indeterminate scalar is [[uninit]]-marked is trusted. +struct MarkedBaseSub : InnerMarked { + MarkedBaseSub() {} +}; + +struct MixedBaseMember : Base { // expected-note {{base class 'Base' declared here}} + int x; // expected-note {{member 'x' declared here}} + MixedBaseMember() {} + // expected-error@-1 {{constructor does not initialize member 'x' under profile 'std::init'}} + // expected-error@-2 {{constructor does not initialize base class 'Base' under profile 'std::init'}} +}; + +struct MultipleBases : Base, Inner { + // expected-note@-1 {{base class 'Base' declared here}} + // expected-note@-2 {{base class 'Inner' declared here}} + MultipleBases() {} + // expected-error@-1 {{constructor does not initialize base class 'Base' under profile 'std::init'}} + // expected-error@-2 {{constructor does not initialize base class 'Inner' under profile 'std::init'}} +}; + +struct OutOfLineBase : Base { // expected-note {{base class 'Base' declared here}} + OutOfLineBase(); +}; +OutOfLineBase::OutOfLineBase() {} // expected-error {{constructor does not initialize base class 'Base' under profile 'std::init'}} + +template +struct TmplBase : T { // expected-note {{base class 'Base' declared here}} + TmplBase() {} // expected-error {{constructor does not initialize base class 'Base' under profile 'std::init'}} +}; +template struct TmplBase; // expected-note {{in instantiation of member function 'TmplBase::TmplBase' requested here}} + +struct SuppressedBaseByRule : Base { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] SuppressedBaseByRule() {} +}; + +// A virtual base is the most-derived constructor's responsibility, so leaving +// it uninitialized here is deferred: no diagnostic. +struct VirtualBase : virtual Base { + VirtualBase() {} +}; From 5ef242a2465a1fe97ec6929f4321aa37272b32ab Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 17:27:43 -0400 Subject: [PATCH 123/289] Reject [[uninit]] on a static or thread-local under std::init static_marker A static or thread-local is zero-initialized by language rule, so marking it [[uninit]] contradicts the paper (an initialized object cannot be marked [[uninit]]). The check runs in ActOnUninitializedDecl beside uninit_decl (R2); the with-initializer case stays uninit_with_initializer (R4), and unions and pointers stay with union_marker / pointer_marker so no second diagnostic fires. --- .../clang/Basic/DiagnosticSemaKinds.td | 3 ++ clang/lib/Sema/SemaDecl.cpp | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 337a5ff0e7358..00cff378741ac 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14168,6 +14168,9 @@ def err_init_union_marker : ProfileRuleError< def err_init_uninit_pointer_marker : ProfileRuleError< "'[[uninit]]' cannot be applied to a pointer under profile '%0'; " "initialize the pointer (for example to 'nullptr')">; +def err_init_uninit_static_marker : ProfileRuleError< + "'[[uninit]]' cannot be applied to variable %1 with %select{static|thread}2 " + "storage duration under profile '%0'; it is zero-initialized">; def err_init_ctor_uninit_member : ProfileRuleError< "constructor does not initialize member %1 under profile '%0'">; def note_init_uninit_member_here : Note< diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 229302e771197..17ccac937fca9 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14750,6 +14750,35 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { << Profile << Var->getDeclName(); } + // std::init / static_marker: a variable with static or thread storage + // duration is zero-initialized by language rule (paper §3), so it is an + // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an + // initialized object marked [[uninit]] is an error"). The case with a real + // initializer -- explicit, or a constructor that actually runs -- is + // already caught by uninit_with_initializer (R4, in + // CheckCompleteVariableDeclaration below); this covers the zero-initialized, + // no-real-initializer case R4 treats as a consistent no-op. The factual + // (HonorUninitMarkers=false) walk matches R4: a static object whose only + // indeterminate scalars are themselves marked is still zero-initialized. + if (!Var->isInvalidDecl() && + (Var->getStorageDuration() == SD_Static || + Var->getStorageDuration() == SD_Thread) && + Var->hasAttr() && + // A union or pointer object marked [[uninit]] is already rejected by + // union_marker / pointer_marker (regardless of storage duration), and + // they retain the marker; do not pile a second diagnostic on top. + !Var->getType()->isUnionType() && !Var->getType()->isPointerType() && + shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), + Var) && + (!Var->getInit() || + (BaseTy->isRecordType() && + defaultInitLeavesScalarIndeterminate( + Var->getType(), /*HonorUninitMarkers=*/false)))) { + bool IsThread = Var->getStorageDuration() == SD_Thread; + Diag(Var->getLocation(), diag::err_init_uninit_static_marker) + << Profile << Var->getDeclName() << IsThread; + } + CheckCompleteVariableDeclaration(Var); } } From afa0eacf615ab70378346b6d0b1c3d16d4918500 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 17:27:44 -0400 Subject: [PATCH 124/289] Test std::init static_marker coverage of static and thread-local markers Also update the field-marker and ref-to-uninit fixtures, whose namespace-scope [[uninit]] declarations the now-closed gap correctly diagnoses. --- .../safety-profile-init-field-marker.cpp | 7 +- .../safety-profile-init-ref-to-uninit.cpp | 11 ++- .../SemaCXX/safety-profile-init-static.cpp | 81 +++++++++++++++++++ 3 files changed, 95 insertions(+), 4 deletions(-) diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index c3d4c7cecf0c9..98159f0043b56 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -20,12 +20,15 @@ struct FieldWithNSDMIPrefix { [[uninit]] int m = 0; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; +// A static data member is a zero-initialized static object, so its definition +// is rejected by static_marker (paper section 4.2), even though the marker +// attaches to the declaration; the error fires at the out-of-line definition. struct WithStaticDataMember { static int s [[uninit]]; [[uninit]] static int t; }; -int WithStaticDataMember::s; -int WithStaticDataMember::t; +int WithStaticDataMember::s; // expected-error {{'[[uninit]]' cannot be applied to variable 's' with static storage duration under profile 'std::init'; it is zero-initialized}} +int WithStaticDataMember::t; // expected-error {{'[[uninit]]' cannot be applied to variable 't' with static storage duration under profile 'std::init'; it is zero-initialized}} struct MultipleFields { int a [[uninit]]; diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index b7064edd4d9ce..d89f5ac4f7090 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -9,8 +9,15 @@ [[profiles::enforce(std::init)]]; int g_init = 0; -[[uninit]] int g_uninit; -[[uninit]] int g_uninit_arr[3]; +// Static fixtures supplying uninitialized memory for the pointer/reference +// tests below. A static [[uninit]] is rejected by static_marker (paper section +// 4.2), so suppress that rule here: the test deliberately creates uninitialized +// static storage, and suppression keeps the marker (the source stays +// "uninitialized memory" for the ref_to_uninit checks). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit_arr[3]; [[ref_to_uninit]] int *allocate(int n); [[ref_to_uninit]] void *alloc_void(); [[ref_to_uninit]] int &get_uninit_ref(); diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp index 3dc73d223fd85..9a49f776c8dea 100644 --- a/clang/test/SemaCXX/safety-profile-init-static.cpp +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -65,3 +65,84 @@ int g_wrong_suppress = runtime(); // expected-error {{non-local variable ' namespace [[profiles::suppress(std::init)]] suppressed_ns { int n_ok = runtime(); // OK: suppressed by the namespace-level attribute } + +// std::init / static_marker: a static or thread-local is zero-initialized by +// language rule, so marking it [[uninit]] is a contradiction (paper section +// 4.2: "an initialized object marked [[uninit]] is an error"). The +// with-initializer case stays uninit_with_initializer's (a real initializer +// already contradicts the marker); this rule covers the zero-initialized, +// no-initializer case. + +namespace std { enum class byte : unsigned char {}; } + +// The paper's section 4.2 'int glob2 [[uninit]]' example, plus the explicit +// 'static' and 'thread_local' spellings. +int g_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} +static int g_static_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_static_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} +thread_local int g_thread_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_thread_marked' with thread storage duration under profile 'std::init'; it is zero-initialized}} + +// std::byte fires too: a static std::byte is zero-initialized, unlike an +// automatic one (which uninit_decl exempts because it may be left +// indeterminate). +std::byte g_byte_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_byte_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} + +// A trivial aggregate at static storage: default-init is a no-op, but the +// object is still zero-initialized, so the marker fires. +Trivial g_agg_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 'g_agg_marked' with static storage duration under profile 'std::init'; it is zero-initialized}} + +// A static pointer / union is owned by pointer_marker / union_marker (they fire +// regardless of storage duration), not static_marker -- exactly one diagnostic. +union U { int x; float y; }; +static int *g_ptr_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +static U g_union_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +// Unmarked statics / thread-locals are fine (zero-initialized, nothing to mark). +int g_unmarked; +static int g_static_unmarked; +thread_local int g_thread_unmarked; +std::byte g_byte_unmarked; + +void test_local_statics_marked() { + static int s [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 's' with static storage duration under profile 'std::init'; it is zero-initialized}} + thread_local int t [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 't' with thread storage duration under profile 'std::init'; it is zero-initialized}} + static int s_ok; + thread_local int t_ok; + // Automatic storage is uninit_decl's domain, not static_marker: the marker + // is the accepted way to leave an automatic variable uninitialized. + int a [[uninit]]; + (void)s; (void)t; (void)s_ok; (void)t_ok; (void)a; +} + +// A static [[uninit]] *with* an initializer is uninit_with_initializer's (R4); +// static_marker must not add a second diagnostic (one error on the line). +int g_marked_with_init [[uninit]] = 0; // expected-error {{variable 'g_marked_with_init' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + +void test_local_static_running_ctor() { + // The synthesized constructor call is a real initializer, so this stays + // uninit_with_initializer (R4); static_marker stays silent (one error). + static WithCtor w [[uninit]]; // expected-error {{variable 'w' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)w; +} + +// Suppression: rule-targeted and whole-profile. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] int g_marker_suppressed_rule [[uninit]]; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] int g_marker_suppressed_all [[uninit]]; + +// A profile rule fires on the instantiation, not the template pattern: a +// dependent static is diagnosed once, at instantiation. +template +void template_static_marked() { + static T s [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to variable 's' with static storage duration under profile 'std::init'; it is zero-initialized}} + (void)s; +} +template void template_static_marked(); // expected-note {{in instantiation of function template specialization 'template_static_marked' requested here}} + +// An uninstantiated template pattern is not yet a phase-7 entity, so no rule +// fires on it (no expected diagnostic here). +template +void template_static_never_instantiated() { + static T s [[uninit]]; + (void)s; +} From f811b1e04c74cd4ffeddb182e41585973e27d36e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 17:27:44 -0400 Subject: [PATCH 125/289] Document std::init static_marker and remove the closed static gap --- clang/docs/ProfilesFramework.rst | 63 +++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 85dc83515e60a..0c0ffaaeb46a9 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -677,6 +677,9 @@ regardless of ``-fprofiles``; its profile rules carry weight only when ``union_marker`` (see R6 / R8 below for usage examples). Both are gated on enforcement, and the marker is retained after the diagnostic so ``uninit_decl`` does not re-diagnose the entity. + - Is banned on a variable with static or thread storage duration by + ``static_marker`` (such a variable is zero-initialized, so the marker + contradicts paper §4.2; see R9 below). ``[[ref_to_uninit]]`` (also a standard C++11 attribute) marks a pointer, reference, or pointer/reference-returning function as referring to @@ -729,7 +732,8 @@ constructors") -- an aggregate or trivially-default-constructible class type whose default-initialization leaves a scalar subobject indeterminate (e.g. ``struct S { int x; }; S s;``). A class type with a user-provided default constructor is trusted; static / thread storage duration is excluded -(zero-initialized by language rule). +(zero-initialized by language rule -- a ``[[uninit]]`` marker on such a +variable is instead rejected by ``static_marker`` (R9)). - Diagnostic: ``err_init_uninit_decl``. - Check site: ``Sema::ActOnUninitializedDecl`` in @@ -744,13 +748,6 @@ constructor is trusted; static / thread storage duration is excluded whose only indeterminate scalars are all marked is trusted (e.g. ``struct A { int x [[uninit]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. -- Known gap: because static / thread storage is excluded here and - ``uninit_with_initializer`` (R4) sees no initializer for a zero-initialized - object, a ``[[uninit]]`` marker on a non-local static or thread-local - variable (e.g. ``static int x [[uninit]];``) is silently accepted -- even - though such an object is zero-initialized by language rule, so marking it - ``[[uninit]]`` contradicts paper §4.2 ("an initialized object marked - ``[[uninit]]`` is an error"). R3. ``static_runtime_init`` -- pattern 1 ......................................... @@ -940,6 +937,50 @@ A pointer must instead be initialized (e.g. to ``nullptr``). meaningless on a parameter); a pointer-to-member is not a pointer type and is out of scope. +R9. ``static_marker`` -- pattern 1 +.................................. + +A variable with static or thread storage duration is zero-initialized by +language rule (paper §3), so it is an initialized object; marking it +``[[uninit]]`` contradicts paper §4.2 ("an initialized object marked +``[[uninit]]`` is an error"). A pointer must be initialized; a static is +*already* initialized. + +.. code-block:: c++ + + int glob; // OK: zero-initialized + int glob2 [[uninit]]; // error: zero-initialized (static_marker) + static int s [[uninit]]; // error: also on an explicit static + thread_local int t [[uninit]]; // error: thread storage is zero-initialized too + + void f() { + static int ls [[uninit]]; // error: also on a block-scope static + } + + // Opt out if genuinely required: + [[profiles::suppress(std::init, rule: "static_marker")]] int x [[uninit]]; // OK + +- Diagnostic: ``err_init_uninit_static_marker`` (a ``%select`` distinguishes + static from thread storage). +- Check site: ``Sema::ActOnUninitializedDecl`` in + ``clang/lib/Sema/SemaDecl.cpp``, beside the ``uninit_decl`` (R2) check -- + *not* the ``Uninit`` attribute handler that hosts ``union_marker`` / + ``pointer_marker``. The decl site is reached only for a definition with no + written initializer (so a non-defining ``extern`` declaration, handled + earlier, is excluded), and passing the ``VarDecl`` to + ``shouldEmitProfileViolation`` makes the rule fire on instantiations rather + than template patterns, like every other Decl-based rule. +- Unlike ``uninit_decl``, ``std::byte`` is *not* exempt here: a static + ``std::byte`` is zero-initialized (it cannot be left indeterminate the way an + automatic one can), so the marker is still contradictory. +- Partition with ``uninit_with_initializer`` (R4): a static ``[[uninit]]`` with + a real initializer -- an explicit one, or a constructor that actually runs + (e.g. ``static WithCtor w [[uninit]];``) -- is R4's; ``static_marker`` covers + only the zero-initialized, no-real-initializer case R4 treats as a consistent + no-op (it reuses ``defaultInitLeavesScalarIndeterminate`` with + ``HonorUninitMarkers=false``, R4's factual choice). Exactly one diagnostic + fires in every case. + Diagnostic suppression ~~~~~~~~~~~~~~~~~~~~~~ @@ -1010,7 +1051,11 @@ profiles. When changing the framework, run them all with profile's ``static_runtime_init`` rule: non-local vars need a constant initializer; locals / static-locals / thread-locals are excluded; ``constinit`` failures still produce the existing hard error - regardless of ``-fprofiles``. + regardless of ``-fprofiles``. Also the ``static_marker`` rule: a + ``[[uninit]]`` marker on a zero-initialized static or thread-local + variable (including ``std::byte``) is rejected, while the with-initializer + case stays ``uninit_with_initializer``, plus suppression and template + instantiation. - ``clang/test/SemaCXX/safety-profile-init-with-initializer.cpp`` -- the ``std::init`` profile's ``uninit_with_initializer`` rule: every combination of ``[[uninit]]`` placement (prefix / postfix) with From aca47e215c2105bca1af95d4b638cea7a99c0167 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 17:58:51 -0400 Subject: [PATCH 126/289] Diagnose [[uninit]] member reads in ctor bodies under std::init uninit_read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A [[uninit]] scalar data member must not be read before it is given a value (paper §7.1 "initialized ... before use"; §4.5 makes reading an uninitialized object, except std::byte, erroneous). The constructor is deliberately not required to initialize such a member (§5.1 excepts members with an uninitialized indicator, §5.3 leaves them for users), so there is no constructor-exit requirement -- only a read-before-assignment one. checkInitProfileCtorBody runs a forward definite-assignment dataflow over the constructor-body CFG: a built-in scalar member is assigned by a plain m = e (§4.5), and is definitely assigned at a point only if assigned on every path reaching it (§1.3, intersection meet). A value read of a member that is not definitely assigned is reported via err_init_member_read_before_init. It reuses the uninit_read rule (and its std::byte exemption), runs from IssueWarnings and the post-error path, and is complementary to ctor_uninit_member (R5), which structurally excuses a marked member. --- clang/docs/ProfilesFramework.rst | 52 +++- .../clang/Basic/DiagnosticSemaKinds.td | 2 + clang/lib/Sema/AnalysisBasedWarnings.cpp | 222 ++++++++++++++++++ 3 files changed, 270 insertions(+), 6 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 0c0ffaaeb46a9..c1a4f4776e2a8 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -630,12 +630,18 @@ The ``std::init`` Profile (initial slice) A slice of the proposed initialization profile. It does not yet implement classes that expose uninitialized memory to users (paper §6.2) or random-access -initialization of uninitialized arrays (paper §6.4); and the constructor-body -flow check that would let a ``[[uninit]]`` member be initialized by -assignment in the body (the dynamic half of paper §6.1) is deferred to a future -CFG-based pass. Until it lands, a ``[[uninit]]`` data member is trusted, -and writes *through* a ``[[ref_to_uninit]]`` pointer/reference are not yet -verified (the paper relegates them to ``construct_at`` or suppression). +initialization of uninitialized arrays (paper §6.4). A read of a scalar +``[[uninit]]`` data member before it is assigned in a constructor body *is* +diagnosed by R1 via a CFG-based definite-assignment pass over the body (paper +§7.1 "initialized ... before use"). Class-type and array members (which need +``construct_at`` flow modeling), ``construct_at``/``destroy_at`` flow, and +double-initialization / double-destruction detection remain deferred. The +constructor is *not* required to initialize a ``[[uninit]]`` member (paper §5.1 +excepts members with an uninitialized indicator, and §5.3 leaves such a member +for the user): the obligation is keyed on a read before assignment, not on the +constructor's end. Writes *through* a ``[[ref_to_uninit]]`` pointer/reference +are not yet verified (the paper relegates them to ``construct_at`` or +suppression). Dynamically-created objects are covered when bound: a ``new`` expression that default-initializes its allocated object and leaves a scalar subobject @@ -721,6 +727,40 @@ A read of an uninitialized ``std::byte`` is not diagnosed (paper §4 exempts ``std::byte``). The exemption is per-entry via ``CFGUninitProfileEntry`` so it applies to ``std::init`` but not the generic ``test::uninit_read`` profile. +The same ``std::init`` guarantee also covers a ``[[uninit]]`` scalar *data +member* read before it is assigned in a constructor body. +``checkInitProfileCtorBody`` in ``clang/lib/Sema/AnalysisBasedWarnings.cpp`` +runs a forward definite-assignment dataflow over the constructor-body CFG: a +member is assigned by a plain ``m = e`` (for a built-in type a write is its +initialization, paper §4.5) and is *definitely assigned* at a point only if +assigned on every path reaching it (the meet is intersection, paper §1.3 +"consider all branches ... executed"). A value read (an lvalue-to-rvalue load, +including the RHS of an assignment or a compound assignment) of a member that is +not yet definitely assigned is reported via ``err_init_member_read_before_init`` +at the first such read, with a ``note_init_uninit_member_here`` note. Details: + +- It runs from ``IssueWarnings`` for an enforced ``std::init`` constructor, + reusing the CFG built for the uninitialized-variables analysis, and also from + the post-error path (``runUninitProfileAnalysisAfterError``) so an earlier TU + error does not silently disable it. +- It reuses the ``uninit_read`` rule name (it enforces the same "no read of an + uninitialized object" guarantee), so + ``[[profiles::suppress(std::init, rule: "uninit_read")]]`` -- checked at the + read site via the ``Stmt``/``AnalysisDeclContext`` suppression overload -- + covers it, and the ``std::byte`` exemption applies. +- Target members are ``[[uninit]]`` built-in scalar (arithmetic or enum) + members; a member given a value by the *written* member-initializer list is + assigned at body entry (no false positive and no spurious "marker + list-init" + contradiction). +- There is **no** constructor-exit requirement: a ``[[uninit]]`` member that is + simply never read is left as-is (paper §5.1/§5.3), exactly as R5 structurally + excuses a marked member -- the two checks are complementary. +- Out of scope here (deferred, conservative omissions, not extensions): + class-type and array members, ``construct_at`` flow, and double-init/destroy + detection. Taking the address of a member or binding a reference to it is R7 + (``ref_to_uninit``) territory and is treated as neither a read nor an + initialization by this pass. + R2. ``uninit_decl`` -- pattern 1 ................................. diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 00cff378741ac..1b3d9cd6e94bf 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14145,6 +14145,8 @@ def err_profile_ctor_final_test : ProfileRuleError< "profile '%0'">; def err_init_uninit_read : ProfileRuleError< "variable %1 is read before initialization under profile '%0'">; +def err_init_member_read_before_init : ProfileRuleError< + "member %1 is read before initialization under profile '%0'">; def err_init_uninit_decl : ProfileRuleError< "variable %1 must be initialized or marked '[[uninit]]' under " "profile '%0'">; diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index ac133679bd1ab..b1fda461c2bb9 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1706,6 +1706,217 @@ constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { /*ExemptStdByte=*/true}, }; +// If E names a non-static data member of the current object (an implicit or +// explicit `this->m`), return that field; otherwise null. Access through any +// other object (e.g. `other.m`) is not the current object's member. +static const FieldDecl *getCurrentObjectMember(const Expr *E) { + const auto *ME = dyn_cast(E->IgnoreParenImpCasts()); + if (!ME || !isa(ME->getBase()->IgnoreParenImpCasts())) + return nullptr; + return dyn_cast(ME->getMemberDecl()); +} + +// std::init constructor-body check (paper §7.1 "initialized ... before use"). +// +// A [[uninit]] scalar data member is deliberately *not* required to be +// initialized by the constructor (paper §5.1 excepts members with an +// uninitialized indicator; §5.3 leaves them for users). What is required is +// that such a member is not *read* before it is given a value (§4.5: reading an +// uninitialized object, except std::byte, is erroneous). This is the member +// analog of the R1 rule for [[uninit]] locals. +// +// A forward definite-assignment dataflow over the constructor body: a scalar +// member is "assigned" by a plain `m = e` (for a built-in type a write is its +// initialization, §4.5) and a member is definitely assigned at a point only if +// assigned on every path reaching it (§1.3: all branches are considered +// executed). A value read of a member that is not definitely assigned there is +// the violation. There is no constructor-exit requirement: a member that is +// simply never read is left as-is, exactly as the structural ctor_uninit_member +// check (R5) excuses a marked member. +static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, + AnalysisDeclContext &AC) { + CFG *cfg = AC.getCFG(); + if (!cfg) + return; + + // Target members: [[uninit]] built-in scalar (arithmetic or enum) members + // whose assignment counts as initialization (§4.5). std::byte is exempt + // (§4.5), matching R1/R2. Class-type and array members (which would need + // construct_at flow modeling) and pointers (banned with [[uninit]] by R8) are + // out of scope here. + SmallVector Members; + llvm::DenseMap Index; + for (const FieldDecl *F : Ctor->getParent()->fields()) { + if (!F->hasAttr() || !F->getDeclName() || + F->hasInClassInitializer()) + continue; + QualType T = F->getType(); + if (!T->isIntegralOrEnumerationType() && !T->isFloatingType()) + continue; + if (S.Context.getBaseElementType(T)->isStdByteType()) + continue; + Index[F] = Members.size(); + Members.push_back(F); + } + if (Members.empty()) + return; + const unsigned N = Members.size(); + + // Statements lexically in the constructor body. Member-initializer-list + // expressions are excluded so the check concerns body reads only; a member + // initialized in the written init list instead seeds the entry state below. + llvm::SmallPtrSet BodyStmts; + if (const Stmt *Body = Ctor->getBody()) { + SmallVector Stack(1, Body); + while (!Stack.empty()) { + const Stmt *Cur = Stack.pop_back_val(); + if (!Cur || !BodyStmts.insert(Cur).second) + continue; + for (const Stmt *Child : Cur->children()) + Stack.push_back(Child); + } + } + + // A member given a value by the written member-initializer list is assigned + // at body entry, so a later body read is fine and no "marker + list-init" + // contradiction is introduced (that is not specified by the paper). + llvm::BitVector Seed(N, false); + for (const CXXCtorInitializer *Init : Ctor->inits()) { + if (!Init->isWritten() || !Init->isAnyMemberInitializer()) + continue; + if (const FieldDecl *F = Init->getAnyMember()) { + auto It = Index.find(F); + if (It != Index.end()) + Seed.set(It->second); + } + } + + // Per-block ordered events recovered from the linearized CFG: a member load + // (an lvalue-to-rvalue conversion of `this->m`) is a Read; `m = e` is a Write + // that marks m assigned after the RHS is evaluated; a compound assignment + // `m op= e` both reads and then writes m. + enum EventKind { Read, Write, ReadWrite }; + struct Event { + EventKind Kind; + unsigned Idx; + const Expr *E; + }; + const unsigned NumBlocks = cfg->getNumBlockIDs(); + std::vector> Events(NumBlocks); + std::vector Gen(NumBlocks, llvm::BitVector(N, false)); + for (const CFGBlock *B : *cfg) { + auto &BlockEvents = Events[B->getBlockID()]; + for (const CFGElement &Elem : *B) { + auto OptStmt = Elem.getAs(); + if (!OptStmt) + continue; + const Stmt *St = OptStmt->getStmt(); + if (!BodyStmts.count(St)) + continue; + if (const auto *ICE = dyn_cast(St)) { + if (ICE->getCastKind() != CK_LValueToRValue) + continue; + const FieldDecl *F = getCurrentObjectMember(ICE->getSubExpr()); + if (!F) + continue; + auto It = Index.find(F); + if (It != Index.end()) + BlockEvents.push_back({Read, It->second, ICE}); + } else if (const auto *BO = dyn_cast(St)) { + if (!BO->isAssignmentOp()) + continue; + const FieldDecl *F = getCurrentObjectMember(BO->getLHS()); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back( + {BO->isCompoundAssignmentOp() ? ReadWrite : Write, It->second, BO}); + Gen[B->getBlockID()].set(It->second); + } + } + } + + // Forward "definitely assigned" dataflow: entry state is the seed; a block's + // entry is the intersection over its predecessors' exits (a member is + // definitely assigned only if assigned on every incoming path); a block's + // exit adds the members it assigns. Unprocessed (unreachable) predecessors + // keep the all-assigned top, so unreachable code is never flagged. + std::vector EntryState(NumBlocks, llvm::BitVector(N, true)); + std::vector ExitState(NumBlocks, llvm::BitVector(N, true)); + const CFGBlock &CFGEntry = cfg->getEntry(); + ForwardDataflowWorklist Worklist(*cfg, AC); + Worklist.enqueueBlock(&CFGEntry); + while (const CFGBlock *B = Worklist.dequeue()) { + llvm::BitVector In(N, true); + if (B == &CFGEntry) { + In = Seed; + } else { + bool First = true; + for (const CFGBlock *Pred : B->preds()) { + if (!Pred) + continue; + if (First) { + In = ExitState[Pred->getBlockID()]; + First = false; + } else { + In &= ExitState[Pred->getBlockID()]; + } + } + } + EntryState[B->getBlockID()] = In; + In |= Gen[B->getBlockID()]; + if (In != ExitState[B->getBlockID()]) { + ExitState[B->getBlockID()] = In; + Worklist.enqueueSuccessors(B); + } + } + + // Replay each block from its fixpoint entry state and collect reads of a + // member that is not yet definitely assigned at that point. + std::vector> Offending(N); + for (const CFGBlock *B : *cfg) { + llvm::BitVector Assigned = EntryState[B->getBlockID()]; + for (const Event &Ev : Events[B->getBlockID()]) { + switch (Ev.Kind) { + case Read: + if (!Assigned.test(Ev.Idx)) + Offending[Ev.Idx].push_back(Ev.E); + break; + case Write: + Assigned.set(Ev.Idx); + break; + case ReadWrite: + if (!Assigned.test(Ev.Idx)) + Offending[Ev.Idx].push_back(Ev.E); + Assigned.set(Ev.Idx); + break; + } + } + } + + // Report at the first offending read (in source order) that is not + // suppressed, once per member, mirroring the local-variable reporter. + for (unsigned I = 0; I != N; ++I) { + if (Offending[I].empty()) + continue; + llvm::sort(Offending[I], [&](const Expr *A, const Expr *Bx) { + return S.SourceMgr.isBeforeInTranslationUnit(A->getBeginLoc(), + Bx->getBeginLoc()); + }); + for (const Expr *R : Offending[I]) { + if (!S.shouldEmitProfileViolation("std::init", "uninit_read", R, AC)) + continue; + S.Diag(R->getBeginLoc(), diag::err_init_member_read_before_init) + << "std::init" << Members[I]->getDeclName(); + S.Diag(Members[I]->getLocation(), diag::note_init_uninit_member_here) + << Members[I]->getDeclName(); + break; + } + } +} + class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; AnalysisDeclContext &AC; @@ -2905,6 +3116,11 @@ static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { UninitVariablesAnalysisStats stats = {}; runUninitializedVariablesAnalysis(*cast(D), *cfg, AC, reporter, stats); + // Keep the constructor-body read-before-init check alive after a TU error, + // for parity with the normal path. + if (S.isProfileEnforced("std::init")) + if (const auto *Ctor = dyn_cast(D)) + checkInitProfileCtorBody(S, Ctor, AC); } } @@ -3428,6 +3644,12 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( } } + // std::init: diagnose a read of a [[uninit]] scalar member before it is + // assigned in the constructor body, reusing the CFG built above. + if (S.isProfileEnforced("std::init")) + if (const auto *Ctor = dyn_cast(D)) + checkInitProfileCtorBody(S, Ctor, AC); + // TODO: Enable lifetime safety analysis for other languages once it is // stable. if (EnableLifetimeSafetyAnalysis && S.getLangOpts().CPlusPlus) { From 350d66aea226f976771f1b06989f717443c71f06 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 17:59:05 -0400 Subject: [PATCH 127/289] Test std::init read-before-init of [[uninit]] members in constructor bodies Covers read-before vs read-after assignment, self-read on the RHS, compound assignment, one-branch vs both-branches and loop-body definiteness, never-read members (no constructor-exit requirement), the std::byte exemption, marker plus member-initializer-list, multiple members, ctor/statement/class-level and rule-targeted suppression, out-of-line and template-instantiated constructors, the post-error path, and the no-fprofiles path. --- .../SemaCXX/safety-profile-init-ctor-body.cpp | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-init-ctor-body.cpp diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp new file mode 100644 index 0000000000000..c2b760fa92436 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -0,0 +1,152 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 -Wno-uninitialized %s +// The ERROR run adds a leading unrelated error so every later function is +// analyzed through the post-error path; the same constructor-body diagnostics +// must still fire there. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -Wno-uninitialized -DLEADING_ERROR %s + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +#ifdef LEADING_ERROR +int leading_unrelated_error = undeclared_identifier; +// expected-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} +#endif + +namespace std { enum class byte : unsigned char {}; } + +// A [[uninit]] member that is never read needs no assignment: the constructor +// is not required to initialize it (paper §5.1/§5.3). +struct NeverReadEmpty { + int m [[uninit]]; + NeverReadEmpty() {} +}; + +struct NeverReadActive { + int m [[uninit]]; + int other = 0; + NeverReadActive() { other = 1; } +}; + +struct ReadAfterAssign { + int m [[uninit]]; + ReadAfterAssign() { m = 1; int y = m; (void)y; } +}; + +struct ReadBeforeAssign { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + ReadBeforeAssign() { int y = m; (void)y; m = 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct SelfReadOnRHS { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + SelfReadOnRHS() { m = m + 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct CompoundAssignReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + CompoundAssignReads() { m += 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct OneBranchThenRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + OneBranchThenRead(bool b) { + if (b) + m = 1; + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +struct BothBranchesThenRead { + int m [[uninit]]; + BothBranchesThenRead(bool b) { + if (b) + m = 1; + else + m = 2; + int y = m; + (void)y; + } +}; + +struct LoopBodyThenRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LoopBodyThenRead(int n) { + for (int i = 0; i < n; ++i) + m = i; + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +// std::byte may be read while uninitialized (paper §4.5). +struct ByteExempt { + std::byte b [[uninit]]; + ByteExempt() { std::byte c = b; (void)c; } +}; + +// A member initialized in the (written) member-initializer list is assigned at +// body entry, so a later read is fine and no marker/list-init contradiction is +// introduced. +struct MarkerWithListInit { + int m [[uninit]]; + MarkerWithListInit() : m(0) { int y = m; (void)y; } +}; + +struct MultipleMembers { + int a [[uninit]]; + int b [[uninit]]; // expected-note {{member 'b' declared here}} + int c [[uninit]]; + MultipleMembers() { + a = 1; + int x = a; (void)x; + int y = b; (void)y; // expected-error {{member 'b' is read before initialization under profile 'std::init'}} + c = 2; + int z = c; (void)z; + } +}; + +struct ExplicitThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + ExplicitThis() { int y = this->m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct OutOfLine { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + OutOfLine(); +}; +OutOfLine::OutOfLine() { int y = m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + +template +struct Tmpl { + T m [[uninit]]; // expected-note {{member 'm' declared here}} + Tmpl() { T y = m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; +template struct Tmpl; // expected-note {{in instantiation of member function 'Tmpl::Tmpl' requested here}} + +struct SuppressedCtor { + int m [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] SuppressedCtor() { int y = m; (void)y; } +}; + +struct SuppressedByRule { + int m [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] SuppressedByRule() { int y = m; (void)y; } +}; + +struct SuppressedStmt { + int m [[uninit]]; + SuppressedStmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { int y = m; (void)y; } + } +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedClass { + int m [[uninit]]; + SuppressedClass() { int y = m; (void)y; } +}; From 68ebd6c39369b69d7f3a239a0322d45df5c33026 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 18:10:27 -0400 Subject: [PATCH 128/289] Skip delegating constructors in the std::init ctor-body read check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A delegating constructor's target initializes the members before the delegating body runs (paper §5.1), so analyzing that body falsely flagged a read; skip them, matching how ctor_uninit_member (R5) does. --- clang/docs/ProfilesFramework.rst | 3 +++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index c1a4f4776e2a8..426f2bb1a3b2c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -755,6 +755,9 @@ at the first such read, with a ``note_init_uninit_member_here`` note. Details: - There is **no** constructor-exit requirement: a ``[[uninit]]`` member that is simply never read is left as-is (paper §5.1/§5.3), exactly as R5 structurally excuses a marked member -- the two checks are complementary. +- A *delegating* constructor is skipped: its target initializes the members + before the delegating body runs, so trusting the target (paper §5.1) avoids a + false positive, matching how R5 skips delegating constructors. - Out of scope here (deferred, conservative omissions, not extensions): class-type and array members, ``construct_at`` flow, and double-init/destroy detection. Taking the address of a member or binding a reference to it is R7 diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index b1fda461c2bb9..bd05d47dd1020 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1735,6 +1735,13 @@ static const FieldDecl *getCurrentObjectMember(const Expr *E) { // check (R5) excuses a marked member. static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, AnalysisDeclContext &AC) { + // A delegating constructor leaves member initialization to its target (paper + // §5.1 trusts the constructor that runs first), so by the time the delegating + // body runs the members are already initialized; analyzing its body would + // falsely flag a read. This mirrors how ctor_uninit_member (R5) skips them. + if (Ctor->isDelegatingConstructor()) + return; + CFG *cfg = AC.getCFG(); if (!cfg) return; From a3d85ce6747313617251e7342f846659eaa1831b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 18:10:37 -0400 Subject: [PATCH 129/289] Test delegating and other-object cases for std::init ctor-body reads Locks in that a delegating constructor's body is not analyzed and that a read of another object's member is not treated as a read of the current object. --- .../SemaCXX/safety-profile-init-ctor-body.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index c2b760fa92436..2885fc15d31ed 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -94,6 +94,21 @@ struct MarkerWithListInit { MarkerWithListInit() : m(0) { int y = m; (void)y; } }; +// A delegating constructor's target initializes the members before the body +// runs (paper §5.1), so its body is not analyzed. +struct Delegating { + int m [[uninit]]; + Delegating() : Delegating(0) { int y = m; (void)y; } + Delegating(int v) : m(v) {} +}; + +// A read of another object's member is not a read of the current object. +struct ReadsOtherObject { + int m [[uninit]]; + ReadsOtherObject() { m = 0; } + ReadsOtherObject(const ReadsOtherObject &o) { m = o.m; } +}; + struct MultipleMembers { int a [[uninit]]; int b [[uninit]]; // expected-note {{member 'b' declared here}} From 57d8e8c474715c95f7db48ea58aa3fe1101f1c2e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:04:03 -0400 Subject: [PATCH 130/289] Diagnose pointer assignment through indirection under std::init ref_to_uninit --- clang/lib/Sema/SemaExpr.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 97714ead6d281..b8c2f5622677a 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -15494,20 +15494,21 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, DiagnoseSelfMove(LHS.get(), RHS.get(), OpLoc); // std::init / ref_to_uninit (paper §5): assigning a pointer must respect - // the [[ref_to_uninit]] marking of the assigned-to pointer named on the - // LHS. References cannot be reseated, so only pointer assignment applies; - // the check is limited to an LHS that directly names a pointer entity (so - // its marker can be read locally). + // the [[ref_to_uninit]] marking of the assigned-to pointer. References + // cannot be reseated, so only pointer assignment applies. The marker is + // read when the LHS directly names a pointer entity; any other lvalue + // (e.g. *pp, arr[i]) cannot carry a local marker, so it is the default + // unmarked pointer (paper §4.3) and must not be bound to uninitialized + // memory. if (getLangOpts().Profiles && LHS.get()->getType()->isPointerType()) { const Expr *L = LHS.get()->IgnoreParenImpCasts(); - const ValueDecl *VD = nullptr; + bool TargetIsRefToUninit = false; if (const auto *DRE = dyn_cast(L)) - VD = DRE->getDecl(); + TargetIsRefToUninit = DRE->getDecl()->hasAttr(); else if (const auto *ME = dyn_cast(L)) - VD = ME->getMemberDecl(); - if (VD) - checkRefToUninitInit(OpLoc, VD->hasAttr(), - /*IsReference=*/false, RHS.get()); + TargetIsRefToUninit = ME->getMemberDecl()->hasAttr(); + checkRefToUninitInit(OpLoc, TargetIsRefToUninit, + /*IsReference=*/false, RHS.get()); } // Avoid copying a block to the heap if the block is assigned to a local From 279c62cd192522760a1f276b2f4d5900b21e276a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:04:20 -0400 Subject: [PATCH 131/289] Test std::init ref_to_uninit for pointer assignment through indirection --- .../safety-profile-init-ref-to-uninit.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index d89f5ac4f7090..37494bd5fc940 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -117,6 +117,25 @@ void test_assignment() { (void)p; (void)q; } +// Assignment through indirection: the assigned-to pointer is reached by +// dereference or subscript, so it cannot carry a local [[ref_to_uninit]] +// marker. It is the default unmarked pointer and must not be bound to +// uninitialized memory, exactly as a directly-named pointer would be. +void test_assignment_indirect() { + int *p = nullptr; + int **pp = &p; + *pp = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + *pp = &g_init; // OK + (*pp) = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + *pp = new int; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + *pp = new int(0); // OK + + int *arr[3] = {}; + arr[0] = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + arr[1] = &g_init; // OK + (void)pp; (void)arr; +} + void test_suppress() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "ref_to_uninit")]] int *s = &g_uninit; // OK: suppressed From 183cba3e245c167ee2985b50e05e76fffd7a4dd9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:15:05 -0400 Subject: [PATCH 132/289] Diagnose defaulted call arguments under std::init ref_to_uninit --- clang/lib/Sema/SemaExpr.cpp | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index b8c2f5622677a..0e8894214db7e 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6281,6 +6281,17 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, return true; Arg = ArgExpr.getAs(); + + // The recognizers don't see through the CXXDefaultArgExpr wrapper, so + // check the underlying default-argument expression. + if (getLangOpts().Profiles) { + QualType PT = Param->getType(); + if (PT->isPointerType() || PT->isReferenceType()) + checkRefToUninitInit(Arg->getExprLoc(), + Param->hasAttr(), + PT->isReferenceType(), + cast(Arg)->getExpr()); + } } // Check for array bounds violations for each argument to the call. This From 7e43b50b863461ab6c56c2242816f56829bc1ee1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:18:58 -0400 Subject: [PATCH 133/289] Test std::init ref_to_uninit for defaulted call arguments --- .../safety-profile-init-ref-to-uninit.cpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 37494bd5fc940..2fafc9a17a78b 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -179,6 +179,35 @@ void test_call_arguments() { [[profiles::suppress(std::init)]] { take_ptr(&g_uninit); } // OK: suppressed } +// A defaulted pointer or reference argument is checked against the parameter's +// [[ref_to_uninit]] marking at the call site, like an explicit argument. The +// declarations themselves stay clean; the diagnostic fires at the call. +void def_uninit_ptr(int *p [[ref_to_uninit]] = &g_uninit); +void def_uninit_ptr_bad(int *p [[ref_to_uninit]] = &g_init); +void def_ptr(int *p = &g_init); +void def_ptr_bad(int *p = &g_uninit); +void def_uninit_ref(int &r [[ref_to_uninit]] = g_uninit); +void def_uninit_ref_bad(int &r [[ref_to_uninit]] = g_init); +void def_ref(int &r = g_init); +void def_ref_bad(int &r = g_uninit); + +void test_default_arguments() { + def_uninit_ptr(); // OK + def_uninit_ptr_bad(); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + def_ptr(); // OK + def_ptr_bad(); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + def_uninit_ref(); // OK + def_uninit_ref_bad(); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + def_ref(); // OK + def_ref_bad(); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // An explicit argument overrides the default and is checked on its own merits. + def_ptr_bad(&g_init); // OK + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { def_ptr_bad(); } // OK: suppressed +} + struct Inner { int m; }; // Member access through a [[ref_to_uninit]] pointer denotes uninitialized From 3244fc237ac043ee903378906962a91fb0776827 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:19:28 -0400 Subject: [PATCH 134/289] Document defaulted-argument ref_to_uninit coverage in the profiles framework docs --- clang/docs/ProfilesFramework.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 426f2bb1a3b2c..c5f64a5c8971b 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -945,7 +945,8 @@ not used, and keep the pointer and reference recognizers symmetric. - Check sites: variable initialization (``Sema::CheckCompleteVariableDeclaration``), default member initializers (``Sema::ActOnFinishCXXInClassMemberInitializer``), pointer assignment - (``Sema::CreateBuiltinBinOp``), call arguments + (``Sema::CreateBuiltinBinOp``), call arguments -- including arguments + supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``), and return statements (``Sema::BuildReturnStmt``). A dependent target type defers to instantiation. From 45ede22a887858e1413174abc1a897c6bb8f8f6a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:43:40 -0400 Subject: [PATCH 135/289] Recognize (*this).m member access in the std::init ctor-body check Fixes a false read-before-init when a [[uninit]] member is assigned via (*this).m. --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index bd05d47dd1020..90f3be3762f00 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1706,12 +1706,24 @@ constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { /*ExemptStdByte=*/true}, }; -// If E names a non-static data member of the current object (an implicit or -// explicit `this->m`), return that field; otherwise null. Access through any -// other object (e.g. `other.m`) is not the current object's member. +// True if E denotes the current object: `this` (the implicit/explicit pointer +// of an arrow access) or `*this` (the object lvalue of a dot access). +static bool isCurrentObjectBase(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (isa(E)) + return true; + const auto *UO = dyn_cast(E); + return UO && UO->getOpcode() == UO_Deref && + isa(UO->getSubExpr()->IgnoreParenImpCasts()); +} + +// If E names a non-static data member of the current object (`this->m`, the +// implicit `m`, or the equivalent `(*this).m`), return that field; otherwise +// null. Access through any other object (e.g. `other.m`) is not the current +// object's member. static const FieldDecl *getCurrentObjectMember(const Expr *E) { const auto *ME = dyn_cast(E->IgnoreParenImpCasts()); - if (!ME || !isa(ME->getBase()->IgnoreParenImpCasts())) + if (!ME || !isCurrentObjectBase(ME->getBase())) return nullptr; return dyn_cast(ME->getMemberDecl()); } From 0bc971db98bfecf634bf800d5ac95db596a7a5cf Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:43:52 -0400 Subject: [PATCH 136/289] Test (*this).m member access in the std::init ctor-body check --- .../test/SemaCXX/safety-profile-init-ctor-body.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index 2885fc15d31ed..4dbe961b6a2b4 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -127,6 +127,19 @@ struct ExplicitThis { ExplicitThis() { int y = this->m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} }; +// Writing through `(*this).m` is an initialization, so a later read of m is +// fine -- the same as `this->m` (the reported false positive). +struct DerefThisWriteThenRead { + int m [[uninit]]; + DerefThisWriteThenRead() { (*this).m = 1; int y = m; (void)y; } +}; + +// A real read through `(*this).m` before assignment is still diagnosed. +struct DerefThisReadBeforeInit { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + DerefThisReadBeforeInit() { int y = (*this).m; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + struct OutOfLine { int m [[uninit]]; // expected-note {{member 'm' declared here}} OutOfLine(); From 9e06f4df351e3f4dd0ffa157837930224813eb75 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:44:40 -0400 Subject: [PATCH 137/289] Document (*this).m recognition in the std::init ctor-body check --- clang/docs/ProfilesFramework.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index c5f64a5c8971b..b85c606dd433c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -752,6 +752,10 @@ at the first such read, with a ``note_init_uninit_member_here`` note. Details: members; a member given a value by the *written* member-initializer list is assigned at body entry (no false positive and no spurious "marker + list-init" contradiction). +- A member access on the current object is recognized whether spelled + ``this->m``, implicitly as ``m``, or as the equivalent ``(*this).m``; an + access through any other object (``other.m``) is not the current object's + member. - There is **no** constructor-exit requirement: a ``[[uninit]]`` member that is simply never read is left as-is (paper §5.1/§5.3), exactly as R5 structurally excuses a marked member -- the two checks are complementary. From dedcaad5b601c1af4de84fd7b1c0bd1bbd143318 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 19:59:13 -0400 Subject: [PATCH 138/289] Defer std::init ref_to_uninit binding checks in dependent contexts The call-arg/default-arg/assignment/return sites passed no Decl, firing on the template pattern (double diagnostics, discarded-branch false positives). Guard checkRefToUninitInit on CurContext dependence so the check runs once, at instantiation. --- clang/lib/Sema/SemaDecl.cpp | 8 +++ .../safety-profile-init-ref-to-uninit.cpp | 53 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 17ccac937fca9..4520fd1aacf4b 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15091,6 +15091,14 @@ void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, // not a source the user wrote, so it must not drive this rule. if (!Src || isa(Src->IgnoreParens())) return; + // The call-argument, default-argument, assignment, and return sites pass no + // Decl, so the D->isTemplated() deferral in shouldEmitProfileViolation can't + // fire. Defer on a template pattern here instead: the binding is re-checked + // on the instantiated expression, so firing on the pattern double-diagnoses + // and wrongly fires in discarded if-constexpr branches / never-instantiated + // templates. The variable and data-member sites pass D and are unaffected. + if (!D && CurContext && CurContext->isDependentContext()) + return; static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "ref_to_uninit"; if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 2fafc9a17a78b..75a04ab7aea3b 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -364,3 +364,56 @@ void template_new_bad() { (void)p; } template void template_new_bad(); // expected-note {{in instantiation of function template specialization 'template_new_bad' requested here}} + +// The call-argument, pointer-assignment, and return sites pass no Decl, so +// (unlike the variable-init site, template_nondependent_bad above) their +// deferral cannot come from D->isTemplated(). They must still fire exactly +// once, at instantiation -- not twice, and not on the pattern. +template +void template_call_arg_unmarked() { + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_call_arg_unmarked(); // expected-note {{in instantiation of function template specialization 'template_call_arg_unmarked' requested here}} + +template +void template_call_arg_marked() { + take_uninit_ptr(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +template void template_call_arg_marked(); // expected-note {{in instantiation of function template specialization 'template_call_arg_marked' requested here}} + +template +void template_assignment_bad() { + int *p = nullptr; + p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_assignment_bad(); // expected-note {{in instantiation of function template specialization 'template_assignment_bad' requested here}} + +template +int *template_return_bad() { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template int *template_return_bad(); // expected-note {{in instantiation of function template specialization 'template_return_bad' requested here}} + +// A never-instantiated template stays silent: the deferred checks never run. +template +void template_never_instantiated() { + take_ptr(&g_uninit); + int *p = nullptr; + p = &g_uninit; + (void)p; +} + +// A violation in a discarded if-constexpr branch is never instantiated, so it +// is not diagnosed -- even though the dependent condition keeps the branch live +// on the pattern (where the check now defers). +template +void template_discarded_branch() { + if constexpr (sizeof(T) > 1000) { + take_ptr(&g_uninit); + int *p = nullptr; + p = &g_uninit; + (void)p; + } +} +template void template_discarded_branch(); From 1643520d8cbadd950d3212b56f32865e0e846a31 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 20:09:54 -0400 Subject: [PATCH 139/289] Document dependent-context deferral of the ref_to_uninit binding checks Clarify in ProfilesFramework.rst and the shouldEmitProfileViolation comment that the call-arg/assignment/return checks defer on a template pattern. The [[uninit]] markers and the reinterpret_cast check stay parse-time-only (not re-run at instantiation), so their template false positives are a separate gap. --- clang/docs/ProfilesFramework.rst | 21 +++++++++++++++++++-- clang/lib/Sema/Sema.cpp | 12 +++++++++--- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index b85c606dd433c..3d3415b22102c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -174,6 +174,17 @@ or template instantiation; selected specializations replay suppressed diagnostics when used. Unevaluated and discarded-statement contexts are skipped. The profile name is passed as ``%0``. +``checkProfileViolation`` fires at parse time. For a *non-dependent* +expression inside a template, the check therefore runs on the template +*pattern*, and -- because for some node kinds (such as casts) a non-dependent +``Build*`` result is reused unchanged at instantiation -- it is not re-run on +the specialization. As a result it can fire for a never-instantiated template +or in an ``if constexpr`` branch discarded only at instantiation. +``test::type_cast`` accepts this (it is a test-only profile). A profile whose +``Build*`` routine *is* re-run at instantiation can instead defer on a template +pattern with a ``CurContext->isDependentContext()`` guard, as the ``std::init`` +ref_to_uninit binding checks do (see ``checkRefToUninitInit``). + Suppression for parse-time check sites is consulted via the ``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` RAII guards (see :ref:`profiles-internals` below). Profile implementers do @@ -952,8 +963,14 @@ not used, and keep the pointer and reference recognizers symmetric. (``Sema::CreateBuiltinBinOp``), call arguments -- including arguments supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``), and return statements - (``Sema::BuildReturnStmt``). A dependent target type defers to - instantiation. + (``Sema::BuildReturnStmt``). Every site defers on a template pattern and + fires once, at instantiation. The variable and data-member sites pass the + instantiated ``Decl`` (deferred by the ``D->isTemplated()`` check in + ``shouldEmitProfileViolation``); the Decl-less call-argument, assignment, and + return sites -- whose ``Build*`` routine is re-run at instantiation -- instead + defer via a ``CurContext->isDependentContext()`` guard in + ``checkRefToUninitInit``, so they neither double-fire nor fire in a discarded + ``if constexpr`` branch or a never-instantiated template. R8. ``pointer_marker`` -- attribute handler ........................................... diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index bd0f58e80af4c..1fcf0f040745b 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3179,9 +3179,15 @@ bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, // only on its instantiation -- where D is the instantiated, non-templated // declaration -- not on the template pattern. Checking the pattern too would // diagnose never-instantiated templates and double-fire (once when the - // pattern is parsed and again at each instantiation). Decl-less check sites - // (the [[uninit]] / [[ref_to_uninit]] markers and casts) pass D == nullptr; - // they run once at parse time and are unaffected. + // pattern is parsed and again at each instantiation). + // + // Decl-less expression check sites whose Build* routine is re-run at + // instantiation (the ref_to_uninit binding checks: call argument, pointer + // assignment, return) instead defer in a dependent context from their own + // wrapper, checkRefToUninitInit, since no Decl is available here. The + // [[uninit]] marker handlers and the reinterpret_cast check also pass + // D == nullptr but are not re-checked at instantiation, so they keep running + // once at parse time (their template false positives are a separate gap). if (D && D->isTemplated()) return false; if (isUnevaluatedContext()) From e25cc9b0913da38affb6717a9ea111c113fc76d5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 20:43:36 -0400 Subject: [PATCH 140/289] Extract the std::init [[uninit]] marker check into a shared helper --- clang/include/clang/Sema/Sema.h | 7 +++++++ clang/lib/Sema/SemaDecl.cpp | 27 +++++++++++++++++++++++++++ clang/lib/Sema/SemaDeclAttr.cpp | 33 +++++---------------------------- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 3192c5fffc02d..2902e077bc741 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1146,6 +1146,13 @@ class Sema final : public SemaBase { bool IsReference, const Expr *Src, const Decl *D = nullptr); + /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose + /// [[uninit]] placed on a pointer, a union variable, or a union member. + /// \p D must already carry the UninitAttr (the marker location is taken from + /// it). Decl-aware via shouldEmitProfileViolation, so it defers on a + /// templated pattern and is re-checked on the instantiated entity. + void diagnoseInitUninitMarkerPlacement(const Decl *D); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 4520fd1aacf4b..8c8c91a6575a5 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14976,6 +14976,33 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, Diag(Loc, diag::err_init_uninit_with_initializer) << Profile << Name; } +void Sema::diagnoseInitUninitMarkerPlacement(const Decl *D) { + const auto *UA = D->getAttr(); + if (!UA) + return; + SourceLocation Loc = UA->getLocation(); + + // std::init / union_marker (paper §5.6): the marker is banned on a union + // object or a union member, because delayed initialization by assigning a + // member would be an erroneous assignment when compiled without the profile. + // std::init / pointer_marker (paper §4.1): "a reference cannot be + // uninitialized. The initialization profile requires the same for pointers." + // A pointer must instead be initialized (e.g. to nullptr). Both are profile + // policy (not a meaningless subject), so they are gated on enforcement; the + // marker is left in place so uninit_decl / ctor_uninit_member treat the + // entity as acknowledged rather than re-diagnosing it. + bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); + bool UnionMember = + isa(D) && cast(D)->getParent()->isUnion(); + if ((UnionVar || UnionMember) && + shouldEmitProfileViolation("std::init", "union_marker", Loc)) + Diag(Loc, diag::err_init_union_marker) + << "std::init" << (UnionMember ? 1 : 0); + else if (cast(D)->getType()->isPointerType() && + shouldEmitProfileViolation("std::init", "pointer_marker", Loc)) + Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; +} + // std::init / ref_to_uninit (paper §5). Two mutually-recursive local // recognizers over the syntactic form of a source expression -- no flow // analysis and no type-system tracking. Uninitialized storage is only ever diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index e3262d447c75e..de0893949d622 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6964,35 +6964,12 @@ static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { return; } - // std::init / union_marker (paper §6.5): the initialization profile bans the - // marker on a union object or a union member, because delayed initialization - // by assigning a member would be an erroneous assignment when compiled - // without the profile. Unlike the cases above this is profile policy rather - // than a meaningless subject, so it is gated on enforcement (and a union may - // still legitimately carry the marker when the profile is not enforced). - bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); - bool UnionMember = - isa(D) && cast(D)->getParent()->isUnion(); - if ((UnionVar || UnionMember) && - S.shouldEmitProfileViolation("std::init", "union_marker", AL.getLoc())) - // Diagnose the banned placement but retain the marker (fall through to - // addAttr) so uninit_decl / ctor_uninit_member treat the entity as - // acknowledged and do not re-diagnose it with a second, contradictory - // error. - S.Diag(AL.getLoc(), diag::err_init_union_marker) - << "std::init" << (UnionMember ? 1 : 0); - else if (cast(D)->getType()->isPointerType() && - S.shouldEmitProfileViolation("std::init", "pointer_marker", - AL.getLoc())) - // std::init / pointer_marker (paper §4.1): "a reference cannot be - // uninitialized. The initialization profile requires the same for - // pointers." A pointer must be initialized (e.g. to nullptr), so the marker - // is rejected -- but, like the union case, retained to avoid a redundant - // uninit_decl. Gated on enforcement: a pointer may carry the marker without - // the profile. - S.Diag(AL.getLoc(), diag::err_init_uninit_pointer_marker) << "std::init"; - D->addAttr(::new (S.Context) UninitAttr(S.Context, AL)); + + // std::init / union_marker + pointer_marker (paper §4.1, §5.6). Shared with + // the template-instantiation re-check sites (VisitFieldDecl / VisitVarDecl), + // since this handler only runs on the pattern. + S.diagnoseInitUninitMarkerPlacement(D); } static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { From 4e051b18b2c4b6fbd7e225800b7c08168fd08112 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 20:49:10 -0400 Subject: [PATCH 141/289] Re-check std::init pointer_marker / union_marker at template instantiation The marker handler ran only on the dependent pattern, so a template member or local that substituted to a pointer or union escaped diagnosis. --- clang/lib/Sema/Sema.cpp | 7 ++-- clang/lib/Sema/SemaDecl.cpp | 10 ++++-- .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 12 +++++++ .../test/SemaCXX/safety-profile-init-decl.cpp | 10 ++++++ .../safety-profile-init-field-marker.cpp | 32 ++++++++++++++++++- .../SemaCXX/safety-profile-init-union.cpp | 17 ++++++++++ 6 files changed, 82 insertions(+), 6 deletions(-) diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 1fcf0f040745b..67039edb55bb8 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3185,9 +3185,10 @@ bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, // instantiation (the ref_to_uninit binding checks: call argument, pointer // assignment, return) instead defer in a dependent context from their own // wrapper, checkRefToUninitInit, since no Decl is available here. The - // [[uninit]] marker handlers and the reinterpret_cast check also pass - // D == nullptr but are not re-checked at instantiation, so they keep running - // once at parse time (their template false positives are a separate gap). + // [[uninit]] marker checks pass D (via diagnoseInitUninitMarkerPlacement) and + // are re-run on the instantiated field / variable, so they defer here too. + // The reinterpret_cast check still passes D == nullptr and is not re-checked + // at instantiation, so it keeps running once at parse time (a separate gap). if (D && D->isTemplated()) return false; if (isUnevaluatedContext()) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 8c8c91a6575a5..23cea695d0c1d 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14991,15 +14991,21 @@ void Sema::diagnoseInitUninitMarkerPlacement(const Decl *D) { // policy (not a meaningless subject), so they are gated on enforcement; the // marker is left in place so uninit_decl / ctor_uninit_member treat the // entity as acknowledged rather than re-diagnosing it. + // + // Passing \p D makes shouldEmitProfileViolation defer on a templated pattern + // (paper / P3589R2: a rule fires on the instantiation, not the template), + // so the parse-time handler skips template members and the rule is re-run on + // the instantiated entity (VisitFieldDecl / VisitVarDecl), once the + // substituted type is known to be a pointer or union. bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); bool UnionMember = isa(D) && cast(D)->getParent()->isUnion(); if ((UnionVar || UnionMember) && - shouldEmitProfileViolation("std::init", "union_marker", Loc)) + shouldEmitProfileViolation("std::init", "union_marker", Loc, D)) Diag(Loc, diag::err_init_union_marker) << "std::init" << (UnionMember ? 1 : 0); else if (cast(D)->getType()->isPointerType() && - shouldEmitProfileViolation("std::init", "pointer_marker", Loc)) + shouldEmitProfileViolation("std::init", "pointer_marker", Loc, D)) Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; } diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index 2c5b96c24e1e8..7c09d1b3e5279 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -1833,6 +1833,12 @@ Decl *TemplateDeclInstantiator::VisitVarDecl(VarDecl *D, if (SemaRef.getLangOpts().OpenACC) SemaRef.OpenACC().ActOnVariableDeclarator(Var); + // std::init / pointer_marker + union_marker: like the field case, re-check + // the instantiated variable now that its type is known (the parse-time + // handler deferred on the template pattern). + if (!Var->isInvalidDecl()) + SemaRef.diagnoseInitUninitMarkerPlacement(Var); + return Var; } @@ -1917,6 +1923,12 @@ Decl *TemplateDeclInstantiator::VisitFieldDecl(FieldDecl *D) { Field->setAccess(D->getAccess()); Owner->addDecl(Field); + // std::init / pointer_marker + union_marker: the parse-time handler deferred + // on the (dependent) template member; re-check now that the substituted type + // is known. + if (!Field->isInvalidDecl()) + SemaRef.diagnoseInitUninitMarkerPlacement(Field); + return Field; } diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index 8decee1d1e67f..f91dad93f8275 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -136,3 +136,13 @@ void template_uninit_never_instantiated() { int x; (void)x; } + +// A dependent local that substitutes to a pointer is deferred on the pattern +// and fires pointer_marker at instantiation, not on the template. +template +void template_ptr_marker() { + T x [[uninit]]; // #template-ptr-marker + (void)x; +} +template void template_ptr_marker(); // expected-note {{in instantiation of function template specialization 'template_ptr_marker' requested here}} +// expected-error@#template-ptr-marker {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 98159f0043b56..06ed44c6f6bed 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -38,9 +38,39 @@ struct MultipleFields { template struct DependentField { + T m [[uninit]]; // #dependent-field-member +}; +template struct DependentField; // OK: a non-pointer, non-union member + +// The marker is deferred on the dependent pattern and re-checked once the +// substituted type is known, so a pointer member fires pointer_marker at +// instantiation (paper section 4.1). +template struct DependentField; // expected-note {{in instantiation of template class 'DependentField' requested here}} +// expected-error@#dependent-field-member {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// An uninstantiated pattern is not yet a phase-7 entity, so nothing fires. +template +struct DependentFieldNeverInstantiated { T m [[uninit]]; }; -template struct DependentField; + +// Suppression on the dependent member carries through instantiation. +template +struct DependentFieldSuppressed { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] T m [[uninit]]; +}; +template struct DependentFieldSuppressed; + +// A member of a union template fires union_marker at instantiation regardless +// of the substituted type (paper section 5.6). +template +union DependentUnion { + T m [[uninit]]; // #dependent-union-member + int tag; +}; +int dependent_union_size = sizeof(DependentUnion); // expected-note {{in instantiation of template class 'DependentUnion' requested here}} +// expected-error@#dependent-union-member {{'[[uninit]]' cannot be applied to a union member under profile 'std::init'}} // expected-error@+2 {{'uninit' attribute only applies to variables and non-static data members}} // no-profiles-error@+1 {{'uninit' attribute only applies to variables and non-static data members}} diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index cfaf9038d9acd..da4753c8b3936 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -60,3 +60,20 @@ struct HasUnionMember { HasUnionMember() : z(0) {} // expected-error {{constructor does not initialize member 'u' under profile 'std::init'}} HasUnionMember(int) : u{1}, z(0) {} }; + +// A dependent local that substitutes to a union type is deferred on the pattern +// and fires union_marker at instantiation, not on the template. +template +void template_union_marker() { + T x [[uninit]]; // #template-union-marker + (void)x; +} +template void template_union_marker(); // expected-note {{in instantiation of function template specialization 'template_union_marker' requested here}} +// expected-error@#template-union-marker {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +// An uninstantiated pattern never reaches phase 7, so the marker is silent. +template +void template_union_never_instantiated() { + T x [[uninit]]; + (void)x; +} From 82e659569abc00759effbb324b580a49c5c1103e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 20:49:59 -0400 Subject: [PATCH 142/289] Document the instantiation re-check of the std::init marker rules --- clang/docs/ProfilesFramework.rst | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 3d3415b22102c..0ed08fa530969 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -908,11 +908,15 @@ assignment when compiled without the profile. [[profiles::suppress(std::init)]] U e [[uninit]]; // OK - Diagnostic: ``err_init_union_marker``. -- Check site: the ``Uninit`` handler in - ``clang/lib/Sema/SemaDeclAttr.cpp``. Unlike the reference / parameter / - structured-binding rejections, which are unconditional, this is gated on - enforcement -- a union may legitimately carry the marker without the - profile. +- Check site: the shared helper ``Sema::diagnoseInitUninitMarkerPlacement``, + called from the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp`` and + re-run on the instantiated entity from ``VisitFieldDecl`` / ``VisitVarDecl`` + in ``clang/lib/Sema/SemaTemplateInstantiateDecl.cpp``. Unlike the reference / + parameter / structured-binding rejections, which are unconditional, this is + gated on enforcement -- a union may legitimately carry the marker without the + profile. Being Decl-aware it defers on a templated pattern and fires once on + the instantiation (a dependent member or local that substitutes to a union), + consistent with the other ``std::init`` rules. - The banned marker is retained on the declaration after it is diagnosed, so the ``uninit_decl`` / ``ctor_uninit_member`` rules treat the entity as acknowledged and do not emit a second, contradictory diagnostic. @@ -993,11 +997,12 @@ A pointer must instead be initialized (e.g. to ``nullptr``). [[profiles::suppress(std::init, rule: "pointer_marker")]] int *x [[uninit]]; // OK - Diagnostic: ``err_init_uninit_pointer_marker``. -- Check site: the ``Uninit`` handler in - ``clang/lib/Sema/SemaDeclAttr.cpp``, alongside ``union_marker``. Like that - rule it is gated on enforcement -- a pointer may legitimately carry the marker - without the profile -- and the marker is retained after the diagnostic so - ``uninit_decl`` does not also fire. +- Check site: ``Sema::diagnoseInitUninitMarkerPlacement``, alongside + ``union_marker`` (see R6 for the shared parse-time handler and the + re-check on the instantiated field / variable). Like that rule it is gated on + enforcement -- a pointer may legitimately carry the marker without the profile + -- and the marker is retained after the diagnostic so ``uninit_decl`` does not + also fire. - A pointer parameter is rejected earlier and unconditionally (the marker is meaningless on a parameter); a pointer-to-member is not a pointer type and is out of scope. From 67c53fcb0c82fe0c6c2f40bcb220b1212cf503e4 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 21:00:09 -0400 Subject: [PATCH 143/289] Test a non-dependent pointer marker deferred to template instantiation --- .../test/SemaCXX/safety-profile-init-field-marker.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 06ed44c6f6bed..9c2ccd18aff09 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -48,6 +48,16 @@ template struct DependentField; // OK: a non-pointer, non-union member template struct DependentField; // expected-note {{in instantiation of template class 'DependentField' requested here}} // expected-error@#dependent-field-member {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +// The deferral is keyed on the member being templated, not on its type being +// dependent: a literally non-dependent pointer member inside a template still +// defers on the pattern and fires once, at instantiation. +template +struct NonDependentPtrField { + int *m [[uninit]]; // #nondependent-ptr-field +}; +template struct NonDependentPtrField; // expected-note {{in instantiation of template class 'NonDependentPtrField' requested here}} +// expected-error@#nondependent-ptr-field {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + // An uninstantiated pattern is not yet a phase-7 entity, so nothing fires. template struct DependentFieldNeverInstantiated { From 6fb5e2b2c98c8e080c6373e3c1976e47db2ba7d4 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 21:40:50 -0400 Subject: [PATCH 144/289] Align the profiles framework docs with the implementation --- clang/docs/ProfilesFramework.rst | 149 ++++++++----------------------- 1 file changed, 38 insertions(+), 111 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 0ed08fa530969..95d2499c0e0e8 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -466,6 +466,12 @@ interface unit of ``M``. ``[[profiles::require(...)]]`` on an import-declaration validates that the imported module's ``EnforcedProfileDesignators`` contains a matching designator. +``[[profiles::enforce(...)]]`` on a *non-interface* module-declaration (a +``module M;`` implementation unit, or a ``module M:P;`` partition +implementation unit) is accepted but recorded only translation-unit-locally; +it is **not** added to ``Module::EnforcedProfileDesignators`` and so is not +visible to an importer's ``[[profiles::require]]``. + A module partition implementation unit ``module M:P;`` is also a module implementation unit of ``M``, so the primary interface's enforcements apply to it as well. However, it does **not** implicitly import the primary interface, @@ -478,7 +484,10 @@ force-loaded and its absence is never diagnosed. When it is not available the partition implementation unit is simply not subject to the inherited profile -- a missed diagnostic, never a change to the meaning of a well-formed program. For guaranteed enforcement, **repeat** ``[[profiles::enforce(...)]]`` in the -partition implementation unit rather than relying on inheritance. +partition implementation unit rather than relying on inheritance. (Best-effort +inheritance is silent when the interface BMI is absent; if the BMI *is* resident +and enforces a profile whose designator conflicts with a locally repeated +``enforce`` of the same name, that mismatch is still diagnosed.) Importing a module that enforces a profile does **not** enforce that profile in the importing translation unit. Enforcement is always explicit and local. @@ -527,9 +536,11 @@ The tree ships five built-in profiles, all gated on ``-fprofiles``. The four - ``test::ctor_final`` (test-only) -- pattern-4 example riding the constructor-finalization dispatch. - ``std::init`` (initial slice of the proposed initialization profile from - Stroustrup's draft, on top of P3589R2 and P3402R3). It uses all four - patterns: the CFG dispatch (with ``test::uninit_read``), the - constructor-finalization dispatch, and several parse-time check sites. + Bjarne Stroustrup's "An initialization profile", P4222R1.1, on top of + P3589R2 and P3402R3). It uses all four patterns: the CFG dispatch (with + ``test::uninit_read``), the constructor-finalization dispatch, and several + parse-time check sites. Paper section references (``§``) for ``std::init`` + in this document are to P4222R1.1. By convention: @@ -555,7 +566,9 @@ a rule can be checked from a single Sema entry point. - **Diagnostic**: ``err_profile_type_cast_reinterpret`` ("'reinterpret_cast' is unsafe under profile '%0'"). - **Check site**: ``Sema::BuildCXXNamedCast`` in ``clang/lib/Sema/SemaCast.cpp``, - inside the ``reinterpret_cast`` arm. + inside the ``reinterpret_cast`` arm. Only the ``reinterpret_cast<>`` keyword + form is checked; a C-style or functional cast with reinterpret semantics goes + through a different path and is not diagnosed. The entire profile implementation is the single call: @@ -640,8 +653,8 @@ The ``std::init`` Profile (initial slice) ----------------------------------------- A slice of the proposed initialization profile. It does not yet implement -classes that expose uninitialized memory to users (paper §6.2) or random-access -initialization of uninitialized arrays (paper §6.4). A read of a scalar +classes that expose uninitialized memory to users (paper §5.3) or random-access +initialization of uninitialized arrays (paper §5.5). A read of a scalar ``[[uninit]]`` data member before it is assigned in a constructor body *is* diagnosed by R1 via a CFG-based definite-assignment pass over the body (paper §7.1 "initialized ... before use"). Class-type and array members (which need @@ -690,7 +703,7 @@ regardless of ``-fprofiles``; its profile rules carry weight only when default-initialization is a no-op is *not* such an initializer, so the marker is accepted there (the object is genuinely left uninitialized). - Is banned on a pointer by ``pointer_marker`` (a pointer must be - initialized, paper §4.1), and on a union object or member by + initialized, paper §4.3), and on a union object or member by ``union_marker`` (see R6 / R8 below for usage examples). Both are gated on enforcement, and the marker is retained after the diagnostic so ``uninit_decl`` does not re-diagnose the entity. @@ -734,7 +747,7 @@ the table-order priority makes ``test::uninit_read`` fire first. Use ``[[profiles::suppress(test::uninit_read)]]`` to demote it at a use site and surface the ``std::init`` diagnostic. -A read of an uninitialized ``std::byte`` is not diagnosed (paper §4 exempts +A read of an uninitialized ``std::byte`` is not diagnosed (paper §4.5 exempts ``std::byte``). The exemption is per-entry via ``CFGUninitProfileEntry`` so it applies to ``std::init`` but not the generic ``test::uninit_read`` profile. @@ -785,7 +798,7 @@ R2. ``uninit_decl`` -- pattern 1 An automatic-storage variable definition whose default-initialization leaves it (or a scalar subobject) indeterminate must either carry ``[[uninit]]`` or be initialized. This covers a scalar / pointer / -enum with no initializer, and -- per paper §6 ("classes without +enum with no initializer, and -- per paper §5.4 ("classes without constructors") -- an aggregate or trivially-default-constructible class type whose default-initialization leaves a scalar subobject indeterminate (e.g. ``struct S { int x; }; S s;``). A class type with a user-provided default @@ -802,7 +815,7 @@ variable is instead rejected by ``static_marker`` (R9)). - The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=true``, which recurses through bases and members, trusts user-provided default constructors, and skips data members marked - ``[[uninit]]`` (acknowledged uninitialized, paper §6.2). So a type + ``[[uninit]]`` (acknowledged uninitialized, paper §5.3). So a type whose only indeterminate scalars are all marked is trusted (e.g. ``struct A { int x [[uninit]]; }; A a;`` is accepted), while a mixed type still fires for its unmarked scalars. @@ -849,12 +862,12 @@ R5. ``ctor_uninit_member`` -- pattern 4 A user-provided constructor must initialize every non-static data member via its member-initializer list or an NSDMI, unless the member is marked -``[[uninit]]`` (paper §6.1). A plain assignment in the constructor +``[[uninit]]`` (paper §5.1). A plain assignment in the constructor body does not count. A member whose own default-initialization leaves an *unacknowledged* scalar subobject indeterminate (a nested aggregate) is flagged as well; a member whose type's indeterminate scalars are all themselves marked ``[[uninit]]`` is trusted (the same -``HonorUninitMarkers`` walk as R2, paper §6.2). A direct non-virtual +``HonorUninitMarkers`` walk as R2, paper §5.3). A direct non-virtual base-class subobject left indeterminate is flagged the same way: the guarantee is over the *complete object* (paper §5.1, §7.1), and -- unlike a member -- a base cannot carry an ``[[uninit]]`` marker, so it must always be @@ -872,7 +885,7 @@ a base with a user-provided default constructor is trusted. anonymous-aggregate members and unnamed bit-fields are skipped (named bit-fields are checked like any other member). - A union's own constructor is exempt from this rule -- its members are - mutually exclusive, so a constructor initializes at most one (paper §6.5; + mutually exclusive, so a constructor initializes at most one (paper §5.6; see R6). A union *data member* of a non-union class is still checked, and must be initialized via the member-initializer list. - Known gaps: *virtual* base-class subobjects are not checked. A virtual @@ -888,7 +901,7 @@ R6. ``union_marker`` -- attribute handler ......................................... ``[[uninit]]`` on a union object or a union member is banned (paper -§6.5): delayed initialization by assigning a member would be an erroneous +§5.6): delayed initialization by assigning a member would be an erroneous assignment when compiled without the profile. .. code-block:: c++ @@ -921,7 +934,7 @@ assignment when compiled without the profile. the ``uninit_decl`` / ``ctor_uninit_member`` rules treat the entity as acknowledged and do not emit a second, contradictory diagnostic. -An *unmarked* union left uninitialized is itself the error (paper §6.5): +An *unmarked* union left uninitialized is itself the error (paper §5.6): ``Sema::defaultInitLeavesScalarIndeterminate`` reports a union as indeterminate unless it has no members, a user-provided default constructor, or a default member initializer. A uninitialized union variable is therefore diagnosed by @@ -932,7 +945,7 @@ R7. ``ref_to_uninit`` -- pattern 1 .................................. A pointer or reference must be bound consistently with its -``[[ref_to_uninit]]`` marking (paper §5): a marked pointer/reference may only +``[[ref_to_uninit]]`` marking (paper §4.3): a marked pointer/reference may only refer to uninitialized memory, and an unmarked one may only refer to initialized memory. "Refers to uninitialized memory" is recognised purely locally from the source expression's syntactic form (no flow analysis): the @@ -975,11 +988,18 @@ not used, and keep the pointer and reference recognizers symmetric. defer via a ``CurContext->isDependentContext()`` guard in ``checkRefToUninitInit``, so they neither double-fire nor fire in a discarded ``if constexpr`` branch or a never-instantiated template. +- Known gaps: the recognizer has no ``InitListExpr`` arm, so a *braced* source + (``= {p}``, ``f({p})``, ``return {p}``) is treated as initialized -- a missed + diagnostic for an unmarked target, and a *false positive* for a + ``[[ref_to_uninit]]`` target. Variadic (``...``) call arguments and calls + with no ``FunctionDecl`` (e.g. through a function pointer) are not checked, + and only plain ``=`` pointer assignment is covered (compound assignment + rebinds nothing and is skipped). R8. ``pointer_marker`` -- attribute handler ........................................... -``[[uninit]]`` on a pointer is banned (paper §4.1): "a reference cannot +``[[uninit]]`` on a pointer is banned (paper §4.3): "a reference cannot be uninitialized. The initialization profile requires the same for pointers." A pointer must instead be initialized (e.g. to ``nullptr``). @@ -1059,96 +1079,3 @@ Every rule is suppressible per-site with ``[[profiles::suppress(std::init, rule: "rule_name")]]`` (rule-targeted). The token-based-dominion limitation noted earlier applies: a suppress attribute on a ``VarDecl`` covers only that declaration's tokens. - - -In-Tree Tests -============= - -These tests collectively exercise the framework and the built-in -profiles. When changing the framework, run them all with -``check-clang-sema``, ``check-clang-parser``, and ``check-clang-pch``. - -- ``clang/test/Parser/cxx-profiles-framework.cpp`` -- attribute parser: - valid ``enforce``/``suppress``/``require`` forms, the ``[[using profiles: - ...]]`` syntax, profile-name and profile-argument grammar, and the - parse-error / missing-argument-clause paths. -- ``clang/test/SemaCXX/safety-profile-framework.cpp`` -- attribute placement - and basic semantic checks (``enforce`` only on empty-declarations at TU - scope, ``require`` only on imports, ``suppress`` on declarations and - statements, ``justification:`` must be a string literal, etc.). -- ``clang/test/SemaCXX/safety-profile-framework-modules.cppm`` -- module - integration: ``enforce`` on a module-declaration is exported via the BMI, - ``require`` validates against an imported module's exported set, GMF-only - ``enforce`` does not leak through the BMI, interface-to-implementation - propagation, partition interfaces, and the without-``-fprofiles`` ignored - paths. -- ``clang/test/SemaCXX/safety-profile-type-cast.cpp`` -- the - ``test::type_cast`` profile: enforcement, suppression on every supported - declaration and statement form, template instantiation, SFINAE - exclusion, lambdas (including generic lambdas with suppression carried - through instantiation), and out-of-line members of suppressed classes - and namespaces. -- ``clang/test/SemaCXX/safety-profile-uninit-read.cpp`` -- the - ``test::uninit_read`` profile. Cases are gated on ``-DCASE=N`` so the - analysis-based-warnings early-exit-on-first-error does not hide later - cases; case 0 is the no-violation baseline used by both the - ``-fprofiles`` and the without-``-fprofiles`` runs. -- ``clang/test/SemaCXX/safety-profile-class-final.cpp`` -- the - ``test::class_final`` profile: end-to-end exercise of the - class-finalization dispatch (pattern 3) including basic firing, class - template instantiation, lambda skipping, suppression on the class and on - enclosing lexical parents, SFINAE exclusion, and the - without-``-fprofiles`` ignored path. -- ``clang/test/SemaCXX/safety-profile-init-read.cpp`` -- the ``std::init`` - profile's ``uninit_read`` rule. Same ``-DCASE=N`` style as the - ``test::uninit_read`` test; CASE=4 additionally enforces - ``test::uninit_read`` to exercise the table-order priority. -- ``clang/test/SemaCXX/safety-profile-ctor-final.cpp`` -- the - ``test::ctor_final`` profile: end-to-end exercise of the - constructor-finalization dispatch (pattern 4) including written / - no-list / out-of-line / instantiated constructors, the delegating and - defaulted skips, suppression, and the without-``-fprofiles`` path. -- ``clang/test/SemaCXX/safety-profile-init-decl.cpp`` -- the ``std::init`` - profile's ``uninit_decl`` rule for scalars / pointers / enums: require an - initializer or ``[[uninit]]``; statics / thread-locals are - excluded; class types with a user-provided default constructor are - trusted. -- ``clang/test/SemaCXX/safety-profile-init-aggregate.cpp`` -- the - ``uninit_decl`` rule for aggregates / trivially-default-constructible - class types whose default-init leaves a scalar subobject indeterminate - (paper §6); braced and value initialization are accepted. -- ``clang/test/SemaCXX/safety-profile-init-static.cpp`` -- the ``std::init`` - profile's ``static_runtime_init`` rule: non-local vars need a - constant initializer; locals / static-locals / thread-locals are - excluded; ``constinit`` failures still produce the existing hard error - regardless of ``-fprofiles``. Also the ``static_marker`` rule: a - ``[[uninit]]`` marker on a zero-initialized static or thread-local - variable (including ``std::byte``) is rejected, while the with-initializer - case stays ``uninit_with_initializer``, plus suppression and template - instantiation. -- ``clang/test/SemaCXX/safety-profile-init-with-initializer.cpp`` -- the - ``std::init`` profile's ``uninit_with_initializer`` rule: every - combination of ``[[uninit]]`` placement (prefix / postfix) with - every initializer form (``= e``, ``{}``, ``(e)``), plus the - synthesized-initializer and RecoveryExpr cases. -- ``clang/test/SemaCXX/safety-profile-init-field-marker.cpp`` -- placement - of ``[[uninit]]`` on data members, the marker / NSDMI - contradiction, and rejection on references, parameters, and structured - bindings. -- ``clang/test/SemaCXX/safety-profile-init-ctor.cpp`` -- the ``std::init`` - profile's ``ctor_uninit_member`` rule: member-initializer-list / NSDMI / - marker coverage, the nested-aggregate and body-assignment cases, - out-of-line and instantiated constructors, and suppression. -- ``clang/test/SemaCXX/safety-profile-init-union.cpp`` -- the ``std::init`` - profile's ``union_marker`` rule banning the marker on a union object or - union member. -- ``clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp`` -- placement - of ``[[ref_to_uninit]]`` and rejection on subjects that are not pointers, - references, or pointer/reference-returning functions. -- ``clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp`` -- the - ``std::init`` profile's ``ref_to_uninit`` rule across every check site: - variable and data-member initialization, assignment, call arguments - (including the paper's ``uninitialized_fill`` example), and return - statements, plus suppression and template instantiation. -- ``clang/test/PCH/cxx-profiles-enforce.cpp`` -- ``[[profiles::enforce]]`` - state survives PCH serialization round-trip. From 77aed2c9d15abb5b36f1694aae8eb96766d71f41 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 22:59:25 -0400 Subject: [PATCH 145/289] Add a read-only mode to the ref_to_uninit recognizer --- clang/lib/Sema/SemaDecl.cpp | 67 ++++++++++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 13 deletions(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 23cea695d0c1d..699a2935abf13 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15014,22 +15014,45 @@ void Sema::diagnoseInitUninitMarkerPlacement(const Decl *D) { // analysis and no type-system tracking. Uninitialized storage is only ever // introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker; // anything unrecognized is treated as initialized (the trust model). -static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E); +// +// \p ForRead selects read-through mode (Sema::checkRefToUninitRead): a +// directly named [[uninit]] object is then not treated as uninitialized, +// because a read of such a named object is the flow-based uninit_read pass's +// responsibility; only indirection through a [[ref_to_uninit]] +// pointer/reference still counts. +static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + bool ForRead = false); // \p E is a pointer prvalue. True if it points to uninitialized storage. -static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E) { +static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, + bool ForRead = false) { if (!E) return false; E = E->IgnoreParenImpCasts(); + // Pass-through forms are transparent to their operand: a single-element + // braced initializer { e } binds from e (modeling + // MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); empty {} and + // multi-element lists are not pointer sources and fall through. A conditional + // is uninit if either arm is, so a value that may be uninit forces a marked + // target; a comma yields its right operand. + if (const auto *ILE = dyn_cast(E)) + return ILE->getNumInits() == 1 && + pointerRefersToUninitStorage(Ctx, ILE->getInit(0), ForRead); + if (const auto *CO = dyn_cast(E)) + return pointerRefersToUninitStorage(Ctx, CO->getTrueExpr(), ForRead) || + pointerRefersToUninitStorage(Ctx, CO->getFalseExpr(), ForRead); + if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) + return pointerRefersToUninitStorage(Ctx, BO->getRHS(), ForRead); + // Array-to-pointer decay has been stripped above, leaving the array glvalue. if (E->getType()->isArrayType()) - return glvalueDenotesUninitStorage(Ctx, E); + return glvalueDenotesUninitStorage(Ctx, E, ForRead); // &G, where G denotes uninitialized storage. if (const auto *UO = dyn_cast(E)) if (UO->getOpcode() == UO_AddrOf) - return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr()); + return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), ForRead); // A value of a [[ref_to_uninit]] pointer, or a call to a // [[ref_to_uninit]]-returning function. @@ -15063,23 +15086,40 @@ static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E) { // manufactured from an integer (operand not a pointer) is not propagated. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->getType()->isPointerType()) - return pointerRefersToUninitStorage(Ctx, CE->getSubExpr()); + return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), ForRead); return false; } // \p E is a glvalue. True if it denotes uninitialized storage. -static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E) { +static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + bool ForRead) { if (!E) return false; E = E->IgnoreParenImpCasts(); + // Pass-through forms are transparent to their operand, routed through the + // glvalue recognizer (symmetric to the pointer side): a single-element braced + // initializer binds from its element, a conditional is uninit if either arm + // is, and a comma yields its right operand. + if (const auto *ILE = dyn_cast(E)) + return ILE->getNumInits() == 1 && + glvalueDenotesUninitStorage(Ctx, ILE->getInit(0), ForRead); + if (const auto *CO = dyn_cast(E)) + return glvalueDenotesUninitStorage(Ctx, CO->getTrueExpr(), ForRead) || + glvalueDenotesUninitStorage(Ctx, CO->getFalseExpr(), ForRead); + if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) + return glvalueDenotesUninitStorage(Ctx, BO->getRHS(), ForRead); + // A named entity denotes uninitialized storage if it is [[uninit]], or // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes // the pointer object itself -- which is initialized -- so it does not count. - auto DeclDenotesUninit = [](const ValueDecl *VD) { - return VD->hasAttr() || + // In read mode the [[uninit]] arm is dropped: a direct read of a named + // [[uninit]] object is left to the flow-based uninit_read pass, so only a + // [[ref_to_uninit]] reference (or indirection, handled below) still counts. + auto DeclDenotesUninit = [&](const ValueDecl *VD) { + return (!ForRead && VD->hasAttr()) || (VD->getType()->isReferenceType() && VD->hasAttr()); }; if (const auto *DRE = dyn_cast(E)) @@ -15088,26 +15128,27 @@ static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E) { // a->m reaches m through the pointer a (object *a); a.m through the // glvalue a. return DeclDenotesUninit(ME->getMemberDecl()) || - (ME->isArrow() ? pointerRefersToUninitStorage(Ctx, ME->getBase()) - : glvalueDenotesUninitStorage(Ctx, ME->getBase())); + (ME->isArrow() + ? pointerRefersToUninitStorage(Ctx, ME->getBase(), ForRead) + : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead)); // A call to a [[ref_to_uninit]]-returning reference function: the referent // it returns is uninitialized. Mirrors the pointer recognizer's call arm. if (const auto *CE = dyn_cast(E)) if (const FunctionDecl *FD = CE->getDirectCallee()) return FD->hasAttr(); if (const auto *ASE = dyn_cast(E)) - return pointerRefersToUninitStorage(Ctx, ASE->getBase()); + return pointerRefersToUninitStorage(Ctx, ASE->getBase(), ForRead); // *p, where p points to uninitialized storage. if (const auto *UO = dyn_cast(E)) if (UO->getOpcode() == UO_Deref) - return pointerRefersToUninitStorage(Ctx, UO->getSubExpr()); + return pointerRefersToUninitStorage(Ctx, UO->getSubExpr(), ForRead); // A reference cast (an explicit cast yielding a glvalue) denotes the same // storage as its operand; propagate. Symmetric to the pointer-cast arm. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->isGLValue()) - return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr()); + return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), ForRead); return false; } From 1e76874fe228487f11a18806a0288413f70d0b38 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 22:59:38 -0400 Subject: [PATCH 146/289] Diagnose reads through [[ref_to_uninit]] under std::init --- .../clang/Basic/DiagnosticSemaKinds.td | 3 +++ clang/include/clang/Sema/Sema.h | 10 ++++++++ clang/lib/Sema/SemaDecl.cpp | 23 +++++++++++++++++++ clang/lib/Sema/SemaExpr.cpp | 7 ++++++ 4 files changed, 43 insertions(+) diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 1b3d9cd6e94bf..6e273f7de2b1d 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14187,4 +14187,7 @@ def err_init_ref_to_uninit_requires_uninit : ProfileRuleError< def err_init_uninit_requires_ref_to_uninit : ProfileRuleError< "%select{pointer|reference}1 to uninitialized memory must be marked " "'[[ref_to_uninit]]' under profile '%0'">; +def err_init_uninit_read_through : ProfileRuleError< + "read through a '[[ref_to_uninit]]' pointer or reference accesses " + "uninitialized memory under profile '%0'">; } // end of sema component. diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 2902e077bc741..9d8245b8f6671 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1146,6 +1146,16 @@ class Sema final : public SemaBase { bool IsReference, const Expr *Src, const Decl *D = nullptr); + /// std::init / uninit_read (paper §4.5): diagnose a read *through* a + /// [[ref_to_uninit]] pointer or reference, whose result is itself + /// uninitialized. Called from Sema::DefaultLvalueConversion at the single + /// lvalue-to-rvalue chokepoint, with \p Glvalue the operand being loaded and + /// \p ValueType its value type. Reuses the ref_to_uninit recognizer in + /// read-only mode, so a direct read of a named [[uninit]] object is left to + /// the flow-based uninit_read pass. A std::byte read is exempt (paper §4.5). + void checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, + QualType ValueType); + /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose /// [[uninit]] placed on a pointer, a union variable, or a union member. /// \p D must already carry the UninitAttr (the marker location is taken from diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 699a2935abf13..d5e6d2a6a128d 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15185,6 +15185,29 @@ void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; } +void Sema::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, + QualType ValueType) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // a read the user wrote, so it must not drive this rule. + if (!Glvalue || isa(Glvalue->IgnoreParens())) + return; + // This read site passes no Decl, so the D->isTemplated() deferral in + // shouldEmitProfileViolation can't fire. Defer on a template pattern here: + // the read is re-checked on the instantiated expression, so firing on the + // pattern double-diagnoses and wrongly fires in discarded if-constexpr + // branches / never-instantiated templates (mirrors checkRefToUninitInit). + if (CurContext && CurContext->isDependentContext()) + return; + // Paper §4.5: reading an uninitialized std::byte is permitted. + if (Context.getBaseElementType(ValueType)->isStdByteType()) + return; + if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) + return; + if (!glvalueDenotesUninitStorage(Context, Glvalue, /*ForRead=*/true)) + return; + Diag(Loc, diag::err_init_uninit_read_through) << "std::init"; +} + void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 0e8894214db7e..b56685f849318 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -737,6 +737,13 @@ ExprResult Sema::DefaultLvalueConversion(Expr *E) { if (!BoundsSafetyCheckUseOfCountAttrPtr(Res.get())) return ExprError(); + // std::init / uninit_read (paper §4.5): a read through a [[ref_to_uninit]] + // pointer or reference accesses uninitialized memory. This is the single + // lvalue-to-rvalue chokepoint that by-value reads (copy-init, by-value + // arguments, returns, operator operands) all funnel through. + if (getLangOpts().Profiles) + checkRefToUninitRead(E->getExprLoc(), E, T); + // C++ [conv.lval]p3: // If T is cv std::nullptr_t, the result is a null pointer constant. CastKind CK = T->isNullPtrType() ? CK_NullToPointer : CK_LValueToRValue; From f59f5913477eb253c0b0863ef2e56435e1c4e90e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 22:59:55 -0400 Subject: [PATCH 147/289] Test std::init read-through of [[ref_to_uninit]] --- .../safety-profile-init-ref-to-uninit.cpp | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 75a04ab7aea3b..5f1ef922c60fd 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -9,6 +9,7 @@ [[profiles::enforce(std::init)]]; int g_init = 0; +int g_init2 = 0; // Static fixtures supplying uninitialized memory for the pointer/reference // tests below. A static [[uninit]] is rejected by static_marker (paper section // 4.2), so suppress that rule here: the test deliberately creates uninitialized @@ -18,9 +19,12 @@ int g_init = 0; [[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit; // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit_arr[3]; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit2; [[ref_to_uninit]] int *allocate(int n); [[ref_to_uninit]] void *alloc_void(); [[ref_to_uninit]] int &get_uninit_ref(); +void h(); void test_pointer_target() { int *p1 [[ref_to_uninit]] = &g_uninit; // OK @@ -104,6 +108,61 @@ void test_reference_casts() { (void)cr1; (void)cr2; (void)cr3; (void)gr1; (void)gr2; (void)p; } +// Pass-through sources are transparent to their operand: a single-element +// braced initializer is looked through to its element, a conditional is +// uninitialized if either arm is, and a comma yields its right operand. +void test_braced_pointer() { + int *b1 [[ref_to_uninit]] = {&g_uninit}; // OK + int *b2 = {&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *b3 [[ref_to_uninit]] = {&g_init}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *b4 = {&g_init}; // OK + // Empty {} value-initializes to nullptr, like = nullptr: not uninitialized. + int *b5 [[ref_to_uninit]] = {}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *b6 = {}; // OK + (void)b1; (void)b2; (void)b3; (void)b4; (void)b5; (void)b6; +} + +void test_braced_reference() { + int &r1 [[ref_to_uninit]] = {g_uninit}; // OK + int &r2 = {g_uninit}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r3 [[ref_to_uninit]] = {g_init}; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r4 = {g_init}; // OK + (void)r1; (void)r2; (void)r3; (void)r4; +} + +void test_conditional_pointer(bool c) { + int *p1 = c ? &g_uninit : &g_init; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p2 [[ref_to_uninit]] = c ? &g_uninit : &g_uninit2; // OK: both arms uninitialized + int *p3 [[ref_to_uninit]] = c ? &g_uninit : &g_init; // OK: either arm may be uninitialized + int *p4 [[ref_to_uninit]] = c ? &g_init : &g_init2; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p5 = c ? &g_init : &g_init2; // OK + (void)p1; (void)p2; (void)p3; (void)p4; (void)p5; +} + +void test_conditional_reference(bool c) { + int &r1 [[ref_to_uninit]] = c ? g_uninit : g_uninit2; // OK + int &r2 = c ? g_uninit : g_init; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r3 [[ref_to_uninit]] = c ? g_init : g_init2; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r4 = c ? g_init : g_init2; // OK + (void)r1; (void)r2; (void)r3; (void)r4; +} + +void test_comma_pointer() { + int *p1 = (h(), &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *p2 [[ref_to_uninit]] = (h(), &g_uninit); // OK + int *p3 [[ref_to_uninit]] = (h(), &g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *p4 = (h(), &g_init); // OK + (void)p1; (void)p2; (void)p3; (void)p4; +} + +void test_comma_reference() { + int &r1 [[ref_to_uninit]] = (h(), g_uninit); // OK + int &r2 = (h(), g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int &r3 [[ref_to_uninit]] = (h(), g_init); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int &r4 = (h(), g_init); // OK + (void)r1; (void)r2; (void)r3; (void)r4; +} + void test_assignment() { int *p [[ref_to_uninit]] = &g_uninit; p = &g_uninit; // OK @@ -208,6 +267,27 @@ void test_default_arguments() { [[profiles::suppress(std::init)]] { def_ptr_bad(); } // OK: suppressed } +// Pass-through sources reach the recognizer at the call-argument site too. A +// braced scalar pointer argument additionally warns (braces around scalar +// initializer), so the braced cases here use references; the pointer recognizer +// is exercised at this site by the conditional and comma forms. +void test_passthrough_call_arguments(bool c) { + take_uninit_ref({g_uninit}); // OK + take_uninit_ref({g_init}); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_ref({g_uninit}); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ref({g_init}); // OK + + take_uninit_ptr(c ? &g_uninit : &g_init); // OK: either arm may be uninitialized + take_ptr(c ? &g_uninit : &g_init); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_uninit_ref(c ? g_uninit : g_uninit2);// OK + take_ref(c ? g_uninit : g_init); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + take_uninit_ptr((h(), &g_uninit)); // OK + take_ptr((h(), &g_uninit)); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_uninit_ref((h(), g_init)); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_ref((h(), g_uninit)); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + struct Inner { int m; }; // Member access through a [[ref_to_uninit]] pointer denotes uninitialized @@ -261,6 +341,28 @@ int &ret_ref_bad() { return g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } +// Pass-through sources at the return site mirror the variable-init behavior. A +// braced scalar pointer return warns (braces around scalar initializer), so the +// braced returns use references; the pointer recognizer is reached here by the +// conditional and comma forms. +[[ref_to_uninit]] int &ret_braced_ref_ok() { return {g_uninit}; } // OK +int &ret_braced_ref_bad() { + return {g_uninit}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +[[ref_to_uninit]] int &ret_braced_ref_bad2() { + return {g_init}; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +[[ref_to_uninit]] int *ret_cond_ptr_ok(bool c) { return c ? &g_uninit : &g_init; } // OK +int *ret_cond_ptr_bad(bool c) { + return c ? &g_uninit : &g_init; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +[[ref_to_uninit]] int *ret_comma_ptr_ok() { return (h(), &g_uninit); } // OK +int *ret_comma_ptr_bad() { + return (h(), &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + int *ret_suppressed() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] return &g_uninit; // OK: suppressed @@ -417,3 +519,88 @@ void template_discarded_branch() { } } template void template_discarded_branch(); + +// std::init / uninit_read (paper §4.5): a read *through* a [[ref_to_uninit]] +// pointer or reference yields an uninitialized value, diagnosed at the +// lvalue-to-rvalue conversion (Sema::DefaultLvalueConversion). These reads fire +// only under -fprofiles; the no-profiles run stays clean. A direct read of a +// named [[uninit]] object is left to the flow-based uninit_read pass and is not +// retested here. +void take_value(int v); + +int test_read_through_pointer(int *p [[ref_to_uninit]], int i) { + int y1 = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y2 = p[i]; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y3 = *p + 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + take_value(*p); // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + if (*p) // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + h(); + (void)y1; (void)y2; (void)y3; + return *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} + +void test_read_through_reference(int &r [[ref_to_uninit]], Inner *ptr [[ref_to_uninit]], + void *vp [[ref_to_uninit]]) { + int y1 = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y2 = ptr->m; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y3 = (*ptr).m; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y4 = *(int *)vp; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y5 = get_uninit_ref(); // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y1; (void)y2; (void)y3; (void)y4; (void)y5; +} + +// Paper §4.5: reading an uninitialized std::byte is permitted, so a read +// through a [[ref_to_uninit]] std::byte pointer/reference is not diagnosed. +void test_read_byte_exempt(std::byte *bp [[ref_to_uninit]], std::byte &br [[ref_to_uninit]]) { + std::byte b1 = *bp; // OK + std::byte b2 = br; // OK + (void)b1; (void)b2; +} + +void test_read_suppress(int *p [[ref_to_uninit]]) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { take_value(*p); } // OK: whole-profile suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(*p); } // OK: rule-targeted suppress +} + +// None of these is a read through the marker: a discarded-value expression and +// an address-of apply no lvalue-to-rvalue conversion, a write targets the +// glvalue without loading it, a reference binding is not a load, and copying +// the pointer value reads the (initialized) pointer object rather than through +// it. +void test_read_negatives(int *p [[ref_to_uninit]], int &r [[ref_to_uninit]], + Inner *ptr [[ref_to_uninit]], int *base [[ref_to_uninit]]) { + (void)r; // OK: discarded value + (void)*p; // OK: discarded value + int *ap [[ref_to_uninit]] = &*p; // OK: address-of is not a read + *p = 5; // OK: write, not a read + ptr->m = 5; // OK: write, not a read + int &r2 [[ref_to_uninit]] = *p; // OK: reference binding, not a read + int *q [[ref_to_uninit]] = base; // OK: reads the pointer value, not through it + (void)ap; (void)q; (void)r2; +} + +// A read through a [[ref_to_uninit]] parameter inside a template body defers on +// the pattern (a dependent context) and fires once, at instantiation -- whether +// the read's operand is non-dependent (template_read_nondependent_bad) or +// dependent (template_read_dependent_bad). A never-instantiated template stays +// silent. Mirrors the binding template_* cases above. +template +void template_read_nondependent_bad(int *p [[ref_to_uninit]]) { + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_read_nondependent_bad(int *); // expected-note {{in instantiation of function template specialization 'template_read_nondependent_bad' requested here}} + +template +T template_read_dependent_bad(T *p [[ref_to_uninit]]) { + return *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} +template int template_read_dependent_bad(int *); // expected-note {{in instantiation of function template specialization 'template_read_dependent_bad' requested here}} + +template +void template_read_never_instantiated(int *p [[ref_to_uninit]]) { + int y = *p; + (void)y; +} From 8a38c998e2253b0f9bd9053fd467c187a6e09333 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 22:59:55 -0400 Subject: [PATCH 148/289] Document std::init ref_to_uninit read-through enforcement --- clang/docs/ProfilesFramework.rst | 56 +++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 95d2499c0e0e8..8d0c9269ff9fa 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -663,9 +663,10 @@ double-initialization / double-destruction detection remain deferred. The constructor is *not* required to initialize a ``[[uninit]]`` member (paper §5.1 excepts members with an uninitialized indicator, and §5.3 leaves such a member for the user): the obligation is keyed on a read before assignment, not on the -constructor's end. Writes *through* a ``[[ref_to_uninit]]`` pointer/reference -are not yet verified (the paper relegates them to ``construct_at`` or -suppression). +constructor's end. A scalar *read through* a ``[[ref_to_uninit]]`` +pointer/reference is diagnosed at the lvalue-to-rvalue conversion (R7, +``uninit_read``); *writes* through such a pointer/reference are not yet verified +(the paper relegates them to ``construct_at`` or suppression). Dynamically-created objects are covered when bound: a ``new`` expression that default-initializes its allocated object and leaves a scalar subobject @@ -955,11 +956,16 @@ pointer; a cast of such a pointer to another pointer type (paper §4.3), or of such a glvalue to another reference; a call to a ``[[ref_to_uninit]]``-returning function; or a ``new`` expression that default-initializes its allocated object and leaves a scalar subobject -indeterminate (e.g. ``new int``, ``new int[n]``, paper §1.2 / §4.3). Anything -else is treated as initialized (the trust model). The reference cast and the -``[[ref_to_uninit]]``-returning reference call are not spelled out by the -paper but follow from the profile's guarantee that uninitialized objects are -not used, and keep the pointer and reference recognizers symmetric. +indeterminate (e.g. ``new int``, ``new int[n]``, paper §1.2 / §4.3). +Pass-through forms are transparent to the operand they forward: a single-element +braced initializer (``{e}``) is looked through to its element, a conditional +(``c ? a : b``) is uninitialized if either arm is, and a comma (``(a, b)``) +takes its right operand -- so each is handled like the direct binding it +forwards. Anything else is treated as initialized (the trust model). The +reference cast and the ``[[ref_to_uninit]]``-returning reference call are not +spelled out by the paper but follow from the profile's guarantee that +uninitialized objects are not used, and keep the pointer and reference +recognizers symmetric. - Diagnostics: ``err_init_ref_to_uninit_requires_uninit`` (marked target, initialized source) and ``err_init_uninit_requires_ref_to_uninit`` (unmarked @@ -988,13 +994,33 @@ not used, and keep the pointer and reference recognizers symmetric. defer via a ``CurContext->isDependentContext()`` guard in ``checkRefToUninitInit``, so they neither double-fire nor fire in a discarded ``if constexpr`` branch or a never-instantiated template. -- Known gaps: the recognizer has no ``InitListExpr`` arm, so a *braced* source - (``= {p}``, ``f({p})``, ``return {p}``) is treated as initialized -- a missed - diagnostic for an unmarked target, and a *false positive* for a - ``[[ref_to_uninit]]`` target. Variadic (``...``) call arguments and calls - with no ``FunctionDecl`` (e.g. through a function pointer) are not checked, - and only plain ``=`` pointer assignment is covered (compound assignment - rebinds nothing and is skipped). +- Read-through enforcement (paper §4.5): a scalar *read* through a + ``[[ref_to_uninit]]`` pointer or reference loads an uninitialized value and is + diagnosed at the single lvalue-to-rvalue chokepoint + (``Sema::DefaultLvalueConversion`` calling ``Sema::checkRefToUninitRead``), + which by-value reads -- copy-initialization, by-value arguments, returns, and + operator/condition operands -- all funnel through. It reuses the recognizer + in a read-only mode (a ``ForRead`` flag that drops the directly-named + ``[[uninit]]`` arm, leaving a direct read of such a named object to the + flow-based ``uninit_read`` pass while still recognizing indirection through a + ``[[ref_to_uninit]]`` pointer/reference), reports the shared rule + ``uninit_read`` via ``err_init_uninit_read_through``, and exempts ``std::byte`` + (paper §4.5). Being Decl-less, it defers on a dependent context and fires + once, at instantiation. An address-of (``&*p``), a reference binding, a + discarded-value expression (``(void)*p``), and a write (``*p = 5``) apply no + lvalue-to-rvalue conversion and so are not reads. Out of scope, as remaining + limitations: class-type read-through (copy construction from ``*p``) and + compound-assignment reads (``*p += 1``, which build no lvalue-to-rvalue node), + consistent with the scalar slice and the deferred-writes stance. +- Known gaps: recognition is purely of the source's syntactic form, so a + binding whose underlying operand is unrecognized -- pointer arithmetic, an + integer-to-pointer cast, a call through a function pointer (no + ``FunctionDecl``), or a variadic (``...``) argument -- is treated as + initialized: a missed diagnostic for an unmarked target, and a *false + positive* for a ``[[ref_to_uninit]]`` target. The pass-through forms above + forward to such an operand without laundering it, so they inherit this gap + rather than introducing one. Only plain ``=`` pointer assignment is covered; + compound assignment is not a binding and is skipped. R8. ``pointer_marker`` -- attribute handler ........................................... From e640d9a20756ce2bafa358b1ca759087d584177e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 23:41:15 -0400 Subject: [PATCH 149/289] Enforce [[ref_to_uninit]] on constructor member-initializers --- clang/lib/Sema/SemaDeclCXX.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 2de8ba7c13c35..11db1a01b4baa 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4684,6 +4684,20 @@ Sema::BuildMemberInitializer(ValueDecl *Member, Expr *Init, } else { Init = MemberInit.get(); } + + // std::init / ref_to_uninit (paper §5): a pointer/reference member given a + // written member-initializer must be bound consistently with its marking. + // Pass the enclosing constructor as the Decl so a class-template pattern + // defers (via D->isTemplated()) and fires once at instantiation, where + // BuildMemberInitializer re-runs with the instantiated constructor as + // CurContext, matching ctor_uninit_member. + if (getLangOpts().Profiles) + if (QualType MT = Member->getType(); + !MT->isDependentType() && + (MT->isPointerType() || MT->isReferenceType())) + if (auto *Ctor = dyn_cast(CurContext)) + checkRefToUninitInit(IdLoc, Member->hasAttr(), + MT->isReferenceType(), Init, Ctor); } if (DirectMember) { From 12005a62f707cb5d557c7f3096b595346844291e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 23:41:29 -0400 Subject: [PATCH 150/289] Enforce [[ref_to_uninit]] on aggregate field initialization --- clang/lib/Sema/SemaInit.cpp | 39 +++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index ede2b9beef49b..9c06331d95e78 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -1592,6 +1592,24 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, if (Result.isInvalid()) hadError = true; + // std::init / ref_to_uninit (paper §5): in C++ a pointer field + // initialized by an enclosing aggregate's init list is copy- + // initialized here (a scalar member never reaches CheckScalarType). + // Scoped to an aggregate subobject (EK_Member) so the enclosing + // variable/argument/return is left to its own site, which already + // handles a braced source via the recognizer. No Decl is passed: the + // init list can appear in a template independently of whether the + // aggregate is one, so deferral comes from the dependent-context + // guard in checkRefToUninitInit and suppression from the parse-time + // stack. (A reference field is routed to CheckReferenceType above.) + if (SemaRef.getLangOpts().Profiles && !Result.isInvalid() && + Entity.getKind() == InitializedEntity::EK_Member && + ElemType->isPointerType()) + SemaRef.checkRefToUninitInit( + expr->getExprLoc(), + Entity.getDecl()->hasAttr(), + /*IsReference=*/false, expr, /*D=*/nullptr); + UpdateStructuredListElement(StructuredList, StructuredIndex, Result.getAs()); } else if (!Seq) { @@ -1864,6 +1882,11 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, return; } + // Capture the source element before 'expr' is overwritten by the + // PerformCopyInitialization result below; the ref_to_uninit recognizer needs + // the written source, not the bound reference. + const Expr *Src = expr; + ExprResult Result; if (VerifyOnly) { if (SemaRef.CanPerformCopyInitialization(Entity,expr)) @@ -1884,6 +1907,22 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, if (!VerifyOnly && expr) IList->setInit(Index, expr); + // std::init / ref_to_uninit (paper §5): a reference field bound by an + // enclosing aggregate's init list must be bound consistently with its + // marking. getParent() restricts this to a genuine aggregate subobject: a + // top-level reference member braced-init (a constructor member-initializer or + // an NSDMI) has a null parent and is checked at its own Decl-aware site, so + // the parent guard prevents a double diagnostic there. No Decl is passed: the + // init list can appear in a template independently of whether the aggregate + // is one, so deferral comes from the dependent-context guard in + // checkRefToUninitInit and suppression from the parse-time stack. + if (SemaRef.getLangOpts().Profiles && !VerifyOnly && !Result.isInvalid() && + Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent() && + DeclType->isReferenceType()) + SemaRef.checkRefToUninitInit(Src->getExprLoc(), + Entity.getDecl()->hasAttr(), + /*IsReference=*/true, Src, /*D=*/nullptr); + UpdateStructuredListElement(StructuredList, StructuredIndex, expr); ++Index; if (AggrDeductionCandidateParamTypes) From 85e731cff27530660968232986aefce78e2ea656 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 23:42:01 -0400 Subject: [PATCH 151/289] Test std::init ref_to_uninit on member and aggregate init --- .../safety-profile-init-ref-to-uninit.cpp | 191 ++++++++++++++++++ 1 file changed, 191 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 5f1ef922c60fd..2bc6b087be8f1 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -604,3 +604,194 @@ void template_read_never_instantiated(int *p [[ref_to_uninit]]) { int y = *p; (void)y; } + +// std::init / ref_to_uninit (paper §5): a pointer/reference member given a +// *written* constructor member-initializer is checked with the enclosing +// constructor as the Decl, so a class-template pattern defers and fires once at +// instantiation, mirroring ctor_uninit_member. Both the parenthesized and the +// braced member-initializer forms reach the same recognizer. +struct CtorMemberPtrBad { + int *p1; + int *p2 [[ref_to_uninit]]; + CtorMemberPtrBad() : p1(&g_uninit), p2(&g_init) {} + // expected-error@-1 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // expected-error@-2 {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +}; + +struct CtorMemberPtrOK { + int *p; + int *q [[ref_to_uninit]]; + CtorMemberPtrOK() : p(&g_init), q(&g_uninit) {} // OK +}; + +struct CtorMemberRefBad { + int &r; + int &s [[ref_to_uninit]]; + CtorMemberRefBad() : r(g_uninit), s(g_init) {} + // expected-error@-1 {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // expected-error@-2 {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +}; + +struct CtorMemberRefOK { + int &r; + int &s [[ref_to_uninit]]; + CtorMemberRefOK() : r(g_init), s(g_uninit) {} // OK +}; + +// A braced member-initializer is looked through to its single element, exactly +// like the variable-init site. +struct CtorBracedPtr { + int *p; + CtorBracedPtr() : p{&g_uninit} {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +struct CtorBracedRef { + int &r; + CtorBracedRef() : r{g_uninit} {} // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +// An out-of-line constructor definition is checked where it is defined; the +// enclosing constructor is still the CurContext there. +struct CtorOutOfLine { + int *p; + CtorOutOfLine(); +}; +CtorOutOfLine::CtorOutOfLine() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + +// Cast and call sources reach the recognizer at the member-init site too: a +// cast of a [[ref_to_uninit]]-returning call propagates the marking. +struct CtorCastSource { + int *p; + CtorCastSource() : p((int *)alloc_void()) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +struct CtorSuppressWhole { + int *p; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] CtorSuppressWhole() : p(&g_uninit) {} // OK: suppressed +}; + +struct CtorSuppressRule { + int *p; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] CtorSuppressRule() : p(&g_uninit) {} // OK: suppressed +}; + +template +struct CtorTmpl { + int *p; + CtorTmpl() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; +template struct CtorTmpl; // expected-note {{in instantiation of member function 'CtorTmpl::CtorTmpl' requested here}} + +// A never-instantiated class template stays silent: the deferred check never +// runs on the pattern. +template +struct CtorTmplNever { + int *p; + CtorTmplNever() : p(&g_uninit) {} +}; + +// A written pointer member-initializer that binds to uninitialized memory +// yields exactly one ref_to_uninit error; the member is written, so +// ctor_uninit_member does not also fire. +struct NoDoubleFire { + int *p; + NoDoubleFire() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +// A pointer member left uninitialized is not a ref_to_uninit binding (there is +// no written initializer) and yields only the ctor_uninit_member error. +struct UninitMemberOnly { + int *p; // expected-note {{member 'p' declared here}} + UninitMemberOnly() {} // expected-error {{constructor does not initialize member 'p' under profile 'std::init'}} +}; + +// std::init / ref_to_uninit (paper §5): a pointer/reference field initialized +// by an enclosing aggregate's init list is checked Decl-less, scoped to the +// field subobject, so the enclosing variable/argument/return is left to its own +// site (the variable site is not a pointer/reference here) and there is no +// double diagnostic. +struct AggPtr { int *p; }; +struct AggPtrMarked { int *p [[ref_to_uninit]]; }; +struct AggRef { int &r; }; +struct AggRefMarked { int &r [[ref_to_uninit]]; }; + +void test_aggregate_pointer() { + AggPtr a1{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr a2{&g_init}; // OK + AggPtr a3 = {&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtrMarked m1{&g_init}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggPtrMarked m2{&g_uninit}; // OK + (void)a1; (void)a2; (void)a3; (void)m1; (void)m2; +} + +void test_aggregate_reference() { + AggRef a1{g_uninit}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggRef a2{g_init}; // OK + AggRefMarked m1{g_init}; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggRefMarked m2{g_uninit}; // OK + (void)a1; (void)a2; (void)m1; (void)m2; +} + +void test_aggregate_designated() { + AggPtr a{.p = &g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr b{.p = &g_init}; // OK + (void)a; (void)b; +} + +struct AggNested { AggPtr inner; }; + +void test_aggregate_nested() { + AggNested a{{&g_uninit}}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggNested b{{&g_init}}; // OK + (void)a; (void)b; +} + +// An aggregate temporary built from an init list is checked the same way +// wherever it appears -- as a call argument, a return value, or a new-expression +// initializer. The enclosing pointer (the parameter, the return type, the +// new-expression result) is not a pointer/reference to the field's storage, so +// only the field binding is diagnosed. +void take_agg_ptr(AggPtr a); +AggPtr make_agg_bad() { return {&g_uninit}; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +AggPtr make_agg_ok() { return {&g_init}; } // OK + +void test_aggregate_temporary() { + take_agg_ptr(AggPtr{&g_uninit}); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_agg_ptr(AggPtr{&g_init}); // OK + AggPtr *h = new AggPtr{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + delete h; +} + +void test_aggregate_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] AggPtr a{&g_uninit}; // OK: whole-profile suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] AggPtr b{&g_uninit}; // OK: rule-targeted suppress + (void)a; (void)b; +} + +// Aggregate field init inside a template body is non-dependent here, so it +// defers on the pattern (a dependent context) and fires once at instantiation; +// a never-instantiated template stays silent. +template +void template_aggregate_bad() { + AggPtr a{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} +template void template_aggregate_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_bad' requested here}} + +template +void template_aggregate_never() { + AggPtr a{&g_uninit}; + (void)a; +} + +// A plain pointer *variable* with a braced initializer is checked once at its +// own variable site (EK_Variable); the aggregate field hooks are scoped to a +// member subobject, so this fires exactly once with no new duplicate. +void test_variable_braced_once() { + int *p{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} From 873d1df97b4aaa1bdc32c4d3ef997e7c7f7b0fa2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 29 Jun 2026 23:42:38 -0400 Subject: [PATCH 152/289] Document ref_to_uninit member/aggregate init check sites --- clang/docs/ProfilesFramework.rst | 35 ++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 8d0c9269ff9fa..687ca82b978e3 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -982,18 +982,29 @@ recognizers symmetric. ``new`` (``new int[n]``) is handled uniformly. - Check sites: variable initialization (``Sema::CheckCompleteVariableDeclaration``), default member initializers - (``Sema::ActOnFinishCXXInClassMemberInitializer``), pointer assignment + (``Sema::ActOnFinishCXXInClassMemberInitializer``), constructor + member-initializers (``Sema::BuildMemberInitializer``), aggregate/list field + initialization (``InitListChecker``), pointer assignment (``Sema::CreateBuiltinBinOp``), call arguments -- including arguments supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``), and return statements (``Sema::BuildReturnStmt``). Every site defers on a template pattern and - fires once, at instantiation. The variable and data-member sites pass the - instantiated ``Decl`` (deferred by the ``D->isTemplated()`` check in - ``shouldEmitProfileViolation``); the Decl-less call-argument, assignment, and - return sites -- whose ``Build*`` routine is re-run at instantiation -- instead - defer via a ``CurContext->isDependentContext()`` guard in - ``checkRefToUninitInit``, so they neither double-fire nor fire in a discarded - ``if constexpr`` branch or a never-instantiated template. + fires once, at instantiation. The variable, data-member, and constructor + member-initializer sites pass the instantiated ``Decl`` (deferred by the + ``D->isTemplated()`` check in ``shouldEmitProfileViolation``); the + constructor site passes the enclosing constructor and is re-run by + ``BuildMemberInitializer`` at instantiation, exactly like + ``ctor_uninit_member``. The Decl-less call-argument, assignment, return, and + aggregate-field sites -- whose ``Build*`` / ``InitListChecker`` routine is + re-run at instantiation -- instead defer via a + ``CurContext->isDependentContext()`` guard in ``checkRefToUninitInit``, so + they neither double-fire nor fire in a discarded ``if constexpr`` branch or a + never-instantiated template. The aggregate-field hooks + (``CheckSubElementType`` for a pointer field, ``CheckReferenceType`` for a + reference field) are scoped to a member subobject (``EK_Member`` with a + non-null parent), so the enclosing variable/argument/return is left to its own + site, and a top-level member braced-initializer -- which the constructor or + NSDMI site already checks -- is not diagnosed twice. - Read-through enforcement (paper §4.5): a scalar *read* through a ``[[ref_to_uninit]]`` pointer or reference loads an uninitialized value and is diagnosed at the single lvalue-to-rvalue chokepoint @@ -1020,7 +1031,13 @@ recognizers symmetric. positive* for a ``[[ref_to_uninit]]`` target. The pass-through forms above forward to such an operand without laundering it, so they inherit this gap rather than introducing one. Only plain ``=`` pointer assignment is covered; - compound assignment is not a binding and is skipped. + compound assignment is not a binding and is skipped. Aggregate field + initialization is checked per scalar field, so an array-of-pointer (or + array-of-reference) member is out of scope -- its elements are + ``EK_ArrayElement`` and the ``[[ref_to_uninit]]`` marking lives on the field, + not the element -- as is a pointer/reference member reached through an + ``IndirectFieldDecl`` (a member of an anonymous struct/union), consistent with + the scalar slice. R8. ``pointer_marker`` -- attribute handler ........................................... From fb7a4dbab5e5f81eeb790b027368e2dd52f52e91 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 30 Jun 2026 00:02:47 -0400 Subject: [PATCH 153/289] Diagnose ++/-- reads of [[uninit]] members in ctor bodies --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 90f3be3762f00..4e7d8defc5570 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1853,6 +1853,21 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, BlockEvents.push_back( {BO->isCompoundAssignmentOp() ? ReadWrite : Write, It->second, BO}); Gen[B->getBlockID()].set(It->second); + } else if (const auto *UO = dyn_cast(St)) { + // A built-in ++m / m++ / --m / m-- reads the old value and then writes, + // but unlike -m / !m it carries no lvalue-to-rvalue cast, so the Read + // arm above never sees it. Model it like the compound-assignment case: + // a ReadWrite that also marks the member assigned. + if (!UO->isIncrementDecrementOp()) + continue; + const FieldDecl *F = getCurrentObjectMember(UO->getSubExpr()); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({ReadWrite, It->second, UO}); + Gen[B->getBlockID()].set(It->second); } } } From 1df624fa6bf33ba6b1b0e80c7b7ab9c45e6a587e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 30 Jun 2026 00:02:56 -0400 Subject: [PATCH 154/289] Test std::init ctor-body increment/decrement reads --- .../SemaCXX/safety-profile-init-ctor-body.cpp | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index 4dbe961b6a2b4..536af59bab6e5 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -48,6 +48,80 @@ struct CompoundAssignReads { CompoundAssignReads() { m += 1; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} }; +// A built-in increment or decrement reads the member's old (uninitialized) +// value before writing, exactly like a compound assignment. +struct PreIncReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PreIncReads() { ++m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PostIncReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PostIncReads() { m++; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PreDecReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PreDecReads() { --m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PostDecReads { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PostDecReads() { m--; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct IncDecAfterAssign { + int m [[uninit]]; + IncDecAfterAssign() { m = 0; ++m; m--; } +}; + +struct PostIncInInitBeforeAssign { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + PostIncInInitBeforeAssign() { int y = m++; (void)y; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct PostIncInInitAfterAssign { + int m [[uninit]]; + PostIncInInitAfterAssign() { m = 0; int y = m++; (void)y; } +}; + +struct IncExplicitThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + IncExplicitThis() { ++this->m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct IncDerefThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + IncDerefThis() { ++(*this).m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +// Incrementing another object's member is not a read of the current object. +struct IncOtherObject { + int m [[uninit]]; + IncOtherObject(IncOtherObject &o) { m = 0; ++o.m; } +}; + +template +struct IncTmpl { + T m [[uninit]]; // expected-note {{member 'm' declared here}} + IncTmpl() { ++m; } // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; +template struct IncTmpl; // expected-note {{in instantiation of member function 'IncTmpl::IncTmpl' requested here}} + +struct IncSuppressedByRule { + int m [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] IncSuppressedByRule() { ++m; } +}; + +struct IncSuppressedStmt { + int m [[uninit]]; + IncSuppressedStmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { ++m; } + } +}; + struct OneBranchThenRead { int m [[uninit]]; // expected-note {{member 'm' declared here}} OneBranchThenRead(bool b) { From 53d20f8d47b613fd93a88cd2b8d2b8d88080aea9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 30 Jun 2026 00:03:02 -0400 Subject: [PATCH 155/289] Document increment/decrement in the ctor-body read check --- clang/docs/ProfilesFramework.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 687ca82b978e3..bf4a1116b36ea 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -760,9 +760,12 @@ member is assigned by a plain ``m = e`` (for a built-in type a write is its initialization, paper §4.5) and is *definitely assigned* at a point only if assigned on every path reaching it (the meet is intersection, paper §1.3 "consider all branches ... executed"). A value read (an lvalue-to-rvalue load, -including the RHS of an assignment or a compound assignment) of a member that is -not yet definitely assigned is reported via ``err_init_member_read_before_init`` -at the first such read, with a ``note_init_uninit_member_here`` note. Details: +including the RHS of an assignment) of a member that is not yet definitely +assigned is reported via ``err_init_member_read_before_init`` at the first such +read, with a ``note_init_uninit_member_here`` note. A compound assignment +``m op= e`` and a built-in increment or decrement (``++m``, ``m++``, ``--m``, +``m--``) read the member's old value before writing it, so each is treated as a +read-then-write of that member. Details: - It runs from ``IssueWarnings`` for an enforced ``std::init`` constructor, reusing the CFG built for the uninitialized-variables analysis, and also from From aa70ab3ef3dd579090f2ba4cab571c58290031ce Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 08:50:36 -0400 Subject: [PATCH 156/289] Reject [[ref_to_uninit]] on function pointers/references A function can never denote uninitialized memory, so the marker is unsatisfiable. --- clang/docs/ProfilesFramework.rst | 6 +++-- clang/include/clang/Basic/AttrDocs.td | 4 +++- clang/lib/Sema/SemaDeclAttr.cpp | 12 ++++++---- .../safety-profile-ref-to-uninit-marker.cpp | 24 +++++++++++++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index bf4a1116b36ea..5e03175a7ea5e 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -721,8 +721,10 @@ profile rule carries weight only when ``std::init`` is enforced. custom handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. - Subjects: ``Var``, ``Field``, and ``Function``. The handler rejects any subject whose type (or, for a function, return type) is not a pointer or - reference, via ``err_ref_to_uninit_attr_invalid_type`` -- regardless of - ``-fprofiles``. + reference to an object, via ``err_ref_to_uninit_attr_invalid_type`` -- + regardless of ``-fprofiles``. A function pointer or reference (and a + pointer-to-member) denotes a function or member, never uninitialized memory, + so it is rejected too. - Behaviour: drives the ``ref_to_uninit`` rule (below); has no other effect. Rules diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index e88d952c4a1a2..f4d82f818dc9c 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -938,7 +938,9 @@ only be bound to initialized memory. The attribute applies to a variable, non-static data member, function parameter, or function (for its return value) whose type is a pointer or -reference; applying it elsewhere is diagnosed regardless of ``-fprofiles``. +reference to an object; applying it elsewhere is diagnosed regardless of +``-fprofiles``. A function pointer or reference (or a pointer-to-member) +denotes a function or member, never uninitialized memory, so it is rejected. Outside the initialization profile the attribute is otherwise silently accepted and has no effect, so code that uses it remains valid when compiled diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index de0893949d622..376e09c2419ee 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6975,16 +6975,20 @@ static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // The SubjectList restricts D to a variable, non-static data member, or // function. "Refers to uninitialized memory" is only meaningful for a - // pointer or reference (for a function, its return value), so reject any - // other type. Like the [[uninit]] subject checks, this is not - // profile policy and so fires regardless of -fprofiles. + // pointer or reference to an object (for a function, its return value), so + // reject any other type. A function pointer or reference denotes a function, + // never uninitialized storage, so the marker could never be satisfied; reject + // it too (like a pointer-to-member, which is not a pointer type here). Like + // the [[uninit]] subject checks, this is not profile policy and so fires + // regardless of -fprofiles. QualType T; if (const auto *FD = dyn_cast(D)) T = FD->getReturnType(); else T = cast(D)->getType(); - if (!T->isPointerType() && !T->isReferenceType()) { + if ((!T->isPointerType() && !T->isReferenceType()) || + T->isFunctionPointerType() || T->isFunctionReferenceType()) { S.Diag(AL.getLoc(), diag::err_ref_to_uninit_attr_invalid_type); AL.setInvalid(); return; diff --git a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp index 7eab184c71183..60e92f3298427 100644 --- a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp @@ -10,6 +10,7 @@ int g; int *gp [[ref_to_uninit]] = &g; int &gr [[ref_to_uninit]] = g; +void *gvp [[ref_to_uninit]] = &g; [[ref_to_uninit]] int *gp_prefix = &g; [[ref_to_uninit]] int *allocate(int n); @@ -34,3 +35,26 @@ struct BadMember { int m [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} }; + +// A function pointer or reference (or a function returning one) denotes a +// function, never uninitialized memory, so the marker can never be satisfied +// and is rejected -- like a pointer-to-member, which is not a pointer type. +void some_fn(); + +void (*bad_fn_ptr [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +void (&bad_fn_ref [[ref_to_uninit]])() = some_fn; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +[[ref_to_uninit]] void (*bad_ret_fn_ptr())(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +struct C { void mf(); }; +void (C::*bad_mem_fn_ptr [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} + +struct BadFnPtrMember { + void (*m [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +}; From 8687b40334707c828f5285e1e7d1ac17e7b726b1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 11:11:52 -0400 Subject: [PATCH 157/289] Classify ref_to_uninit sources as init/uninit/unknown NFC: recognizers return a tri-state; diagnostics are unchanged. --- clang/lib/Sema/SemaDecl.cpp | 156 ++++++++++++++++++++++++------------ 1 file changed, 106 insertions(+), 50 deletions(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index d5e6d2a6a128d..70fe1f16cb9b9 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15012,36 +15012,61 @@ void Sema::diagnoseInitUninitMarkerPlacement(const Decl *D) { // std::init / ref_to_uninit (paper §5). Two mutually-recursive local // recognizers over the syntactic form of a source expression -- no flow // analysis and no type-system tracking. Uninitialized storage is only ever -// introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker; -// anything unrecognized is treated as initialized (the trust model). +// introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker. +// +// The classification is tri-state: a recognized form is Initialized or +// Uninitialized, while an unrecognized one (pointer arithmetic, an +// integer-to-pointer cast, a call through a function pointer) is Unknown rather +// than assumed Initialized. Callers wanting a plain "is it uninitialized?" +// answer (Sema::refersToUninitializedMemory, the read-through check) treat +// Unknown as not uninitialized. // // \p ForRead selects read-through mode (Sema::checkRefToUninitRead): a // directly named [[uninit]] object is then not treated as uninitialized, // because a read of such a named object is the flow-based uninit_read pass's // responsibility; only indirection through a [[ref_to_uninit]] // pointer/reference still counts. -static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, - bool ForRead = false); - -// \p E is a pointer prvalue. True if it points to uninitialized storage. -static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, - bool ForRead = false) { +enum class UninitStorage { Initialized, Uninitialized, Unknown }; + +// Combine the arms of a conditional: Uninitialized dominates (either arm may be +// taken), then Unknown, else Initialized. +static UninitStorage combineArms(UninitStorage A, UninitStorage B) { + if (A == UninitStorage::Uninitialized || B == UninitStorage::Uninitialized) + return UninitStorage::Uninitialized; + if (A == UninitStorage::Unknown || B == UninitStorage::Unknown) + return UninitStorage::Unknown; + return UninitStorage::Initialized; +} + +static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + bool ForRead = false); + +// \p E is a pointer prvalue. Classifies whether it points to uninitialized +// storage. +static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, + const Expr *E, + bool ForRead = false) { if (!E) - return false; + return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); // Pass-through forms are transparent to their operand: a single-element // braced initializer { e } binds from e (modeling - // MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); empty {} and - // multi-element lists are not pointer sources and fall through. A conditional - // is uninit if either arm is, so a value that may be uninit forces a marked - // target; a comma yields its right operand. - if (const auto *ILE = dyn_cast(E)) - return ILE->getNumInits() == 1 && - pointerRefersToUninitStorage(Ctx, ILE->getInit(0), ForRead); + // MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); an empty {} + // value-initializes to a null pointer (Initialized) and a multi-element list + // is not a pointer source (Unknown). A conditional is uninit if either arm + // is, so a value that may be uninit forces a marked target; a comma yields + // its right operand. + if (const auto *ILE = dyn_cast(E)) { + if (ILE->getNumInits() == 1) + return pointerRefersToUninitStorage(Ctx, ILE->getInit(0), ForRead); + return ILE->getNumInits() == 0 ? UninitStorage::Initialized + : UninitStorage::Unknown; + } if (const auto *CO = dyn_cast(E)) - return pointerRefersToUninitStorage(Ctx, CO->getTrueExpr(), ForRead) || - pointerRefersToUninitStorage(Ctx, CO->getFalseExpr(), ForRead); + return combineArms( + pointerRefersToUninitStorage(Ctx, CO->getTrueExpr(), ForRead), + pointerRefersToUninitStorage(Ctx, CO->getFalseExpr(), ForRead)); if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) return pointerRefersToUninitStorage(Ctx, BO->getRHS(), ForRead); @@ -15055,14 +15080,23 @@ static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), ForRead); // A value of a [[ref_to_uninit]] pointer, or a call to a - // [[ref_to_uninit]]-returning function. + // [[ref_to_uninit]]-returning function, is Uninitialized. An unmarked named + // pointer or direct callee is a trusted Initialized pointer (paper §4.3); a + // call with no direct callee (through a function pointer) is Unknown. if (const auto *DRE = dyn_cast(E)) - return DRE->getDecl()->hasAttr(); + return DRE->getDecl()->hasAttr() + ? UninitStorage::Uninitialized + : UninitStorage::Initialized; if (const auto *ME = dyn_cast(E)) - return ME->getMemberDecl()->hasAttr(); - if (const auto *CE = dyn_cast(E)) + return ME->getMemberDecl()->hasAttr() + ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + if (const auto *CE = dyn_cast(E)) { if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->hasAttr(); + return FD->hasAttr() ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + return UninitStorage::Unknown; + } // A default-initialized new-expression (none init style: no initializer // written) whose allocated type's default-initialization leaves a scalar @@ -15074,40 +15108,45 @@ static bool pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, // default constructor is trusted by defaultInitLeavesScalarIndeterminate. if (const auto *NE = dyn_cast(E)) { if (NE->getInitializationStyle() != CXXNewInitializationStyle::None) - return false; + return UninitStorage::Initialized; llvm::SmallPtrSet Visited; return defaultInitLeavesScalarIndeterminateImpl( - Ctx, NE->getAllocatedType(), /*HonorUninitMarkers=*/true, Visited); + Ctx, NE->getAllocatedType(), /*HonorUninitMarkers=*/true, Visited) + ? UninitStorage::Uninitialized + : UninitStorage::Initialized; } // Paper §4.3: a [[ref_to_uninit]] pointer cast to another pointer type is // itself [[ref_to_uninit]]. Implicit casts were already stripped above, so // this only looks through an explicit pointer-to-pointer cast; a pointer - // manufactured from an integer (operand not a pointer) is not propagated. + // manufactured from an integer (operand not a pointer) is Unknown. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->getType()->isPointerType()) return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), ForRead); - return false; + return UninitStorage::Unknown; } -// \p E is a glvalue. True if it denotes uninitialized storage. -static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, - bool ForRead) { +// \p E is a glvalue. Classifies whether it denotes uninitialized storage. +static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, + const Expr *E, bool ForRead) { if (!E) - return false; + return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); // Pass-through forms are transparent to their operand, routed through the // glvalue recognizer (symmetric to the pointer side): a single-element braced // initializer binds from its element, a conditional is uninit if either arm // is, and a comma yields its right operand. - if (const auto *ILE = dyn_cast(E)) - return ILE->getNumInits() == 1 && - glvalueDenotesUninitStorage(Ctx, ILE->getInit(0), ForRead); + if (const auto *ILE = dyn_cast(E)) { + if (ILE->getNumInits() == 1) + return glvalueDenotesUninitStorage(Ctx, ILE->getInit(0), ForRead); + return UninitStorage::Unknown; + } if (const auto *CO = dyn_cast(E)) - return glvalueDenotesUninitStorage(Ctx, CO->getTrueExpr(), ForRead) || - glvalueDenotesUninitStorage(Ctx, CO->getFalseExpr(), ForRead); + return combineArms( + glvalueDenotesUninitStorage(Ctx, CO->getTrueExpr(), ForRead), + glvalueDenotesUninitStorage(Ctx, CO->getFalseExpr(), ForRead)); if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) return glvalueDenotesUninitStorage(Ctx, BO->getRHS(), ForRead); @@ -15123,19 +15162,28 @@ static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, (VD->getType()->isReferenceType() && VD->hasAttr()); }; if (const auto *DRE = dyn_cast(E)) - return DeclDenotesUninit(DRE->getDecl()); - if (const auto *ME = dyn_cast(E)) + return DeclDenotesUninit(DRE->getDecl()) ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + if (const auto *ME = dyn_cast(E)) { // a->m reaches m through the pointer a (object *a); a.m through the - // glvalue a. - return DeclDenotesUninit(ME->getMemberDecl()) || - (ME->isArrow() - ? pointerRefersToUninitStorage(Ctx, ME->getBase(), ForRead) - : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead)); + // glvalue a. When m does not itself denote uninit storage, the subobject is + // uninit exactly when its base is. + if (DeclDenotesUninit(ME->getMemberDecl())) + return UninitStorage::Uninitialized; + return ME->isArrow() + ? pointerRefersToUninitStorage(Ctx, ME->getBase(), ForRead) + : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead); + } // A call to a [[ref_to_uninit]]-returning reference function: the referent - // it returns is uninitialized. Mirrors the pointer recognizer's call arm. - if (const auto *CE = dyn_cast(E)) + // it returns is uninitialized. An unmarked direct callee returns a trusted + // Initialized referent; a call with no direct callee is Unknown. Mirrors the + // pointer recognizer's call arm. + if (const auto *CE = dyn_cast(E)) { if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->hasAttr(); + return FD->hasAttr() ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + return UninitStorage::Unknown; + } if (const auto *ASE = dyn_cast(E)) return pointerRefersToUninitStorage(Ctx, ASE->getBase(), ForRead); @@ -15150,12 +15198,19 @@ static bool glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, if (CE->getSubExpr()->isGLValue()) return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), ForRead); - return false; + return UninitStorage::Unknown; +} + +// Dispatches a binding source to the pointer or glvalue recognizer. +static UninitStorage classifyUninitSource(ASTContext &Ctx, const Expr *E, + bool IsReference) { + return IsReference ? glvalueDenotesUninitStorage(Ctx, E) + : pointerRefersToUninitStorage(Ctx, E); } bool Sema::refersToUninitializedMemory(const Expr *E, bool IsReference) const { - return IsReference ? glvalueDenotesUninitStorage(Context, E) - : pointerRefersToUninitStorage(Context, E); + return classifyUninitSource(Context, E, IsReference) == + UninitStorage::Uninitialized; } void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, @@ -15203,7 +15258,8 @@ void Sema::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, return; if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) return; - if (!glvalueDenotesUninitStorage(Context, Glvalue, /*ForRead=*/true)) + if (glvalueDenotesUninitStorage(Context, Glvalue, /*ForRead=*/true) != + UninitStorage::Uninitialized) return; Diag(Loc, diag::err_init_uninit_read_through) << "std::init"; } From 580289ad130550d318f099e3e005b11dfd2eecc1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 11:20:06 -0400 Subject: [PATCH 158/289] Don't reject [[ref_to_uninit]] bindings from unknown sources An unrecognized source is now unknown, not initialized, fixing a false positive. --- clang/docs/ProfilesFramework.rst | 17 +++-- clang/include/clang/Sema/Sema.h | 22 +++--- clang/lib/Sema/SemaDecl.cpp | 11 ++- .../safety-profile-init-ref-to-uninit.cpp | 73 +++++++++++++++++++ 4 files changed, 103 insertions(+), 20 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 5e03175a7ea5e..1ad2ccd2908f9 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -966,7 +966,10 @@ Pass-through forms are transparent to the operand they forward: a single-element braced initializer (``{e}``) is looked through to its element, a conditional (``c ? a : b``) is uninitialized if either arm is, and a comma (``(a, b)``) takes its right operand -- so each is handled like the direct binding it -forwards. Anything else is treated as initialized (the trust model). The +forwards. A source whose form is recognized as neither uninitialized nor +trusted-initialized is classified as *unknown* rather than assumed initialized, +so it is diagnosed for neither a marked target (avoiding a false rejection) nor +an unmarked one (leaving a possible missed diagnostic). The reference cast and the ``[[ref_to_uninit]]``-returning reference call are not spelled out by the paper but follow from the profile's guarantee that uninitialized objects are not used, and keep the pointer and reference @@ -1031,11 +1034,13 @@ recognizers symmetric. - Known gaps: recognition is purely of the source's syntactic form, so a binding whose underlying operand is unrecognized -- pointer arithmetic, an integer-to-pointer cast, a call through a function pointer (no - ``FunctionDecl``), or a variadic (``...``) argument -- is treated as - initialized: a missed diagnostic for an unmarked target, and a *false - positive* for a ``[[ref_to_uninit]]`` target. The pass-through forms above - forward to such an operand without laundering it, so they inherit this gap - rather than introducing one. Only plain ``=`` pointer assignment is covered; + ``FunctionDecl``), or a variadic (``...``) argument -- is classified as + *unknown* and diagnosed for neither direction. A ``[[ref_to_uninit]]`` target + therefore accepts it (rather than the earlier *false positive*), while an + unmarked target also accepts it (a remaining missed diagnostic). The + pass-through forms above forward to such an operand without laundering it, so + they inherit this gap rather than introducing one. Only plain ``=`` pointer + assignment is covered; compound assignment is not a binding and is skipped. Aggregate field initialization is checked per scalar field, so an array-of-pointer (or array-of-reference) member is out of scope -- its elements are diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 9d8245b8f6671..986a519dc5fed 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1124,17 +1124,17 @@ class Sema final : public SemaBase { bool defaultInitLeavesScalarIndeterminate(QualType T, bool HonorUninitMarkers = false); - /// std::init / ref_to_uninit (paper §5): true if \p E refers to (for a - /// pointer source) or, when \p IsReference, denotes (for a glvalue source) - /// uninitialized storage. Recognized purely locally from the expression's - /// syntactic form -- the address of, or a subobject of, a [[uninit]] - /// entity; a value of a [[ref_to_uninit]] pointer/reference or array; a - /// dereference of such a pointer; a cast of such a pointer to another pointer - /// type, or of such a glvalue to another reference; a call to a - /// [[ref_to_uninit]]-returning function; or a new-expression whose - /// default-initialization leaves the allocated object indeterminate (e.g. - /// new int). Anything else is treated as initialized (the trust model; no - /// flow analysis). + /// std::init / ref_to_uninit (paper §5): true only if \p E is affirmatively + /// recognized as referring to (for a pointer source) or, when \p IsReference, + /// denoting (for a glvalue source) uninitialized storage. Recognized purely + /// locally from the expression's syntactic form -- the address of, or a + /// subobject of, a [[uninit]] entity; a value of a [[ref_to_uninit]] + /// pointer/reference or array; a dereference of such a pointer; a cast of such + /// a pointer to another pointer type, or of such a glvalue to another + /// reference; a call to a [[ref_to_uninit]]-returning function; or a + /// new-expression whose default-initialization leaves the allocated object + /// indeterminate (e.g. new int). A trusted-initialized source and an + /// unrecognized (unknown) source both return false (no flow analysis). bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; /// std::init / ref_to_uninit (paper §5): check that the initialization of a diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 70fe1f16cb9b9..19ea76a5c592b 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15232,11 +15232,16 @@ void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, static constexpr StringRef Rule = "ref_to_uninit"; if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) return; - bool SrcUninit = refersToUninitializedMemory(Src, IsReference); + UninitStorage SrcState = classifyUninitSource(Context, Src, IsReference); unsigned IsRef = IsReference ? 1 : 0; - if (TargetIsRefToUninit && !SrcUninit) + // A marked target is a violation only against an affirmatively Initialized + // source: an Unknown one (pointer arithmetic, an integer-to-pointer cast, a + // call through a function pointer) cannot be proven initialized, so rejecting + // it would be a false positive. An unmarked target is diagnosed only against + // an affirmatively Uninitialized source (Unknown stays a missed diagnostic). + if (TargetIsRefToUninit && SrcState == UninitStorage::Initialized) Diag(Loc, diag::err_init_ref_to_uninit_requires_uninit) << Profile << IsRef; - else if (!TargetIsRefToUninit && SrcUninit) + else if (!TargetIsRefToUninit && SrcState == UninitStorage::Uninitialized) Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 2bc6b087be8f1..3dcf26b0d9d1a 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -795,3 +795,76 @@ void test_variable_braced_once() { int *p{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)p; } + +// std::init / ref_to_uninit (paper §5): a source whose syntactic form the +// recognizer does not model -- pointer arithmetic, an integer-to-pointer cast, +// or a call through a function pointer -- is unknown, not initialized. A marked +// target binds from such a source without error (it cannot be proven +// initialized, so rejecting it would be a false positive); an unmarked target +// also binds without error (a documented missed diagnostic). Neither run +// diagnoses these. +void test_unknown_pointer_arithmetic() { + int *base [[ref_to_uninit]] = &g_uninit; + int *p1 [[ref_to_uninit]] = base + 1; // OK + int *p2 = base + 1; // OK + (void)p1; (void)p2; +} + +void test_unknown_int_to_ptr(long n) { + int *p1 [[ref_to_uninit]] = reinterpret_cast(n); // OK + int *p2 = reinterpret_cast(n); // OK + (void)p1; (void)p2; +} + +void test_unknown_fnptr_call(int *(*fp)()) { + int *p1 [[ref_to_uninit]] = fp(); // OK + int *p2 = fp(); // OK + (void)p1; (void)p2; +} + +// The unknown classification propagates through the pass-through forms. +void test_unknown_passthrough(bool c) { + int *base [[ref_to_uninit]] = &g_uninit; + int *p1 [[ref_to_uninit]] = c ? base + 1 : &g_init; // OK: an unknown arm keeps the whole unknown + int *p2 [[ref_to_uninit]] = (h(), base + 1); // OK + int *p3 [[ref_to_uninit]] = {base + 1}; // OK + (void)p1; (void)p2; (void)p3; +} + +// The unknown classification reaches the assignment, call-argument, and return +// sites unchanged. +void test_unknown_assignment(long n) { + int *base [[ref_to_uninit]] = &g_uninit; + int *p [[ref_to_uninit]] = &g_uninit; + p = base + 1; // OK + p = reinterpret_cast(n); // OK + int *q = &g_init; + q = base + 1; // OK + (void)p; (void)q; +} + +void test_unknown_call_argument(long n) { + int *base [[ref_to_uninit]] = &g_uninit; + take_uninit_ptr(base + 1); // OK + take_uninit_ptr(reinterpret_cast(n)); // OK + take_ptr(base + 1); // OK +} + +[[ref_to_uninit]] int *ret_unknown_marked() { + int *base [[ref_to_uninit]] = &g_uninit; + return base + 1; // OK +} +int *ret_unknown_unmarked() { + int *base [[ref_to_uninit]] = &g_uninit; + return base + 1; // OK +} + +// Regression guard: the fix narrows only the unknown case. A marked target +// bound from an affirmatively initialized source is still rejected, and an +// unmarked target from an affirmatively uninitialized source is still rejected. +void test_unknown_regression_guard() { + int *m1 [[ref_to_uninit]] = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *m2 [[ref_to_uninit]] = new int(5); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *u1 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)m1; (void)m2; (void)u1; +} From 2512e6efe145cb1eebe31e58f76d05f34ad8b7ae Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 11:56:10 -0400 Subject: [PATCH 159/289] Stop parse-time suppress stack leaking into finalization checks --- clang/include/clang/Sema/Sema.h | 8 ++++++++ clang/lib/Sema/Sema.cpp | 9 ++++++++- clang/lib/Sema/SemaDeclCXX.cpp | 5 +++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 986a519dc5fed..b217586f96251 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1051,6 +1051,14 @@ class Sema final : public SemaBase { }; SmallVector ProfileSuppressStack; + /// True while a class/constructor finalization profile callback runs. + /// Finalization can fire as a side effect of instantiating an unrelated + /// entity whose ProfileSuppressScope is still on ProfileSuppressStack, so + /// during finalization that transient stack is ignored and suppression is + /// resolved only from the finalized declaration and its lexical parents + /// (token-based dominion, P3589R2 s2.4p3). + bool InProfileFinalizationCheck = false; + bool isProfileEnforced(StringRef ProfileName) const; /// True if any entry of \p Entries names an enforced profile. \p Entries is diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 67039edb55bb8..dc67b9f173473 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3166,7 +3166,14 @@ bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, // available, from the declaration and its lexical parents. The latter does // not depend on a parse-time scope still being active, so finalization checks // that run after the parse scope is torn down still respect suppression. - if (isProfileSuppressed(ProfileName, RuleName) || + // + // Finalization callbacks skip the parse-time stack: they can fire while an + // unrelated entity's instantiation ProfileSuppressScope is still active, and + // that scope does not lexically enclose the finalized declaration (token- + // based dominion, P3589R2 s2.4p3). Their decl-aware walk already covers a + // suppression on the declaration or a lexical parent. + if ((!InProfileFinalizationCheck && + isProfileSuppressed(ProfileName, RuleName)) || isProfileSuppressed(ProfileName, RuleName, D)) return false; // P3589R2 Section 1.1: "its static semantic effects are as-if applied only diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 11db1a01b4baa..f476acec06ccf 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -7192,6 +7192,11 @@ void dispatchFinalizationProfiles(Sema &S, Node *D, const FinalizationProfile (&Table)[N]) { if (!S.anyProfileEnforced(Table)) return; + // Finalization can run nested in an unrelated instantiation whose + // [[profiles::suppress]] scope is still on the parse-time stack; the callbacks + // must resolve suppression only from D and its lexical parents, not that + // transient stack (P3589R2 s2.4p3). + llvm::SaveAndRestore InFinalization(S.InProfileFinalizationCheck, true); for (const auto &E : Table) if (S.isProfileEnforced(E.Name)) E.Callback(S, D); From 021ca87c1f6cb26f78b4778735b5e4481a2d335a Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 11:56:33 -0400 Subject: [PATCH 160/289] Add regression tests for finalization suppress-leak fix --- .../test/SemaCXX/safety-profile-class-final.cpp | 16 ++++++++++++++++ clang/test/SemaCXX/safety-profile-init-ctor.cpp | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp index 7d1e3898c6bfa..b2018d0b877e3 100644 --- a/clang/test/SemaCXX/safety-profile-class-final.cpp +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -218,6 +218,22 @@ class HasFriendDecl { // expected-error {{test profile fired on completion of cl }; struct FriendedLater { int m; }; // expected-error {{test profile fired on completion of class 'FriendedLater' under profile 'test::class_final'}} +// A suppress active only because an unrelated template instantiation is in +// progress must not leak into a separate class's finalization (P3589R2 s2.4p3, +// token-based dominion). +template +struct LeakSeparateClass { // expected-error {{test profile fired on completion of class 'LeakSeparateClass' under profile 'test::class_final'}} + T m; +}; + +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void class_leak_user() { + LeakSeparateClass v; // expected-note {{in instantiation of template class 'LeakSeparateClass' requested here}} + (void)v; +} +template void class_leak_user(); // expected-note {{in instantiation of function template specialization 'class_leak_user' requested here}} + // Without `-fprofiles`, the enforce attribute is `warn_attribute_ignored` // and the diagnostic never fires. This is exercised by the no-profiles RUN // line, which expects only the two attribute-ignored warnings above. diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 15d4e91087dac..8fd97beccac28 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -103,6 +103,18 @@ struct [[profiles::suppress(std::init)]] SuppressedClass { SuppressedClass() {} }; +// Constructor finalization ignores the parse-time suppress stack (it can fire +// nested in an unrelated instantiation), so a [[profiles::suppress]] on a class +// template must still reach the instantiated constructor through the decl-aware +// walk on the lexical parent -- not the stack pushed while instantiating it. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedTemplate { + T x; + SuppressedTemplate() {} +}; +template struct SuppressedTemplate; + struct Base { int b; }; struct UninitBase : Base { // expected-note {{base class 'Base' declared here}} From 93246e1248cc097a7908333648771c2b69abf11d Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 11:56:39 -0400 Subject: [PATCH 161/289] Document finalization suppression ignores parse-time stack --- clang/docs/ProfilesFramework.rst | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 1ad2ccd2908f9..3855a3f311b2c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -318,7 +318,14 @@ This pattern needs two pieces, both colocated with the dispatcher. overload, which walks the declaration and its lexical parents for a matching ``[[profiles::suppress]]``, so suppression on the class or any enclosing lexical ``Decl`` works without the dispatcher establishing a - suppress scope. + suppress scope. For the duration of the callbacks the dispatcher sets + ``Sema::InProfileFinalizationCheck``, which makes + ``shouldEmitProfileViolation`` ignore the transient parse-time + ``ProfileSuppressStack``: finalization can run as a side effect of an + *unrelated* template instantiation whose ``[[profiles::suppress]]`` scope is + still on that stack, and that scope does not lexically enclose the finalized + class (see :ref:`profiles-token-dominion`). Suppression is therefore + resolved only from the declaration and its lexical parents. 2. **Emit diagnostics from the callback via** ``Sema::shouldEmitProfileViolation``. Each callback decides where on @@ -387,8 +394,16 @@ table ``ConstructorFinalizationProfiles`` of the same passes the ``CXXConstructorDecl`` to the decl-aware ``shouldEmitProfileViolation`` overload; that overload walks the declaration and its lexical parents, so ``[[profiles::suppress]]`` on the constructor, -the class, or an enclosing lexical ``Decl`` works. A callback that should -only apply to user-written constructors checks ``Ctor->isUserProvided()``. +the class, or an enclosing lexical ``Decl`` works. As for pattern 3, the +shared ``dispatchFinalizationProfiles`` dispatcher runs these callbacks under +the ``Sema::InProfileFinalizationCheck`` guard, so they resolve suppression +only from that decl-aware walk and ignore the transient parse-time +``ProfileSuppressStack`` (see :ref:`profiles-token-dominion`). A constructor +body is normally instantiated lazily -- outside any unrelated suppress scope -- +so here the guard is defensive; the scenario it actually prevents arises in +pattern 3, where a class completes synchronously inside an enclosing +instantiation. A callback that should only apply to user-written constructors +checks ``Ctor->isUserProvided()``. .. _profiles-token-dominion: @@ -411,6 +426,18 @@ marker. This applies identically to the parse-time suppression stack and the post-parse Stmt-tree walker described in pattern 2. +Token-based dominion is also why the class- and constructor-finalization +dispatch (patterns 3 and 4) deliberately ignores the parse-time +``ProfileSuppressStack``. A finalization callback can run while an +*unrelated* entity is being instantiated -- for example, completing a class +template used inside a ``[[profiles::suppress(P)]]``-annotated function +template that is itself being instantiated. That instantiation's suppress +scope is on the stack, but its tokens do not enclose the finalized class, so +honoring it would suppress a violation outside its dominion. Finalization +therefore resolves suppression only from the finalized declaration and its +lexical parents (via the ``Sema::InProfileFinalizationCheck`` guard on +``dispatchFinalizationProfiles``). + .. _profiles-internals: From 960fbc36026c7f3e24acd5d83015f2108dace7b9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 1 Jul 2026 13:19:22 -0400 Subject: [PATCH 162/289] Add nested member-class variant to finalization leak test --- .../test/SemaCXX/safety-profile-class-final.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp index b2018d0b877e3..5b1c633fefe8a 100644 --- a/clang/test/SemaCXX/safety-profile-class-final.cpp +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -234,6 +234,22 @@ template } template void class_leak_user(); // expected-note {{in instantiation of function template specialization 'class_leak_user' requested here}} +// The same leak reaches a *member* (nested) class: a member class template is +// completed synchronously when instantiated inside the unrelated suppressed +// function template, so its finalization must not honor that suppress either. +struct LeakNestedEnclosing { // expected-error {{test profile fired on completion of class 'LeakNestedEnclosing' under profile 'test::class_final'}} + template + struct LeakInner { T m; }; // expected-error {{test profile fired on completion of class 'LeakInner' under profile 'test::class_final'}} +}; + +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void nested_leak_user() { + LeakNestedEnclosing::LeakInner v; // expected-note {{in instantiation of template class 'LeakNestedEnclosing::LeakInner' requested here}} + (void)v; +} +template void nested_leak_user(); // expected-note {{in instantiation of function template specialization 'nested_leak_user' requested here}} + // Without `-fprofiles`, the enforce attribute is `warn_attribute_ignored` // and the diagnostic never fires. This is exercised by the no-profiles RUN // line, which expects only the two attribute-ignored warnings above. From 6f6316b12fbb09b4422c101416dac251d9229d90 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:07:40 -0400 Subject: [PATCH 163/289] Remove redundant Sema.h includes from parser files Parser.h already includes clang/Sema/Sema.h. --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 1 - clang/lib/Parse/ParseDecl.cpp | 1 - clang/lib/Parse/ParseStmt.cpp | 1 - 3 files changed, 3 deletions(-) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index c6a028100cd81..2391c569cbdaf 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -17,7 +17,6 @@ #include "clang/Sema/DeclSpec.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" -#include "clang/Sema/Sema.h" #include "llvm/ADT/ScopeExit.h" using namespace clang; diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index 11615c39c2c68..de47763b3efac 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -23,7 +23,6 @@ #include "clang/Parse/Parser.h" #include "clang/Parse/RAIIObjectsForParser.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" -#include "clang/Sema/Sema.h" #include "clang/Sema/Lookup.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/ParsedTemplate.h" diff --git a/clang/lib/Parse/ParseStmt.cpp b/clang/lib/Parse/ParseStmt.cpp index 3c3d3a91ed88a..16780a0a0b35f 100644 --- a/clang/lib/Parse/ParseStmt.cpp +++ b/clang/lib/Parse/ParseStmt.cpp @@ -20,7 +20,6 @@ #include "clang/Parse/Parser.h" #include "clang/Parse/RAIIObjectsForParser.h" #include "clang/Sema/DeclSpec.h" -#include "clang/Sema/Sema.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" #include "clang/Sema/SemaCodeCompletion.h" From acf4408d7d52857cda389537169332a341b5f1d7 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:07:41 -0400 Subject: [PATCH 164/289] Harden the ParsedAttr custom-data accessors and pool allocators Assert the attribute kind in the three profile-argument accessors so a mismatched getProfile*Args() call cannot silently reinterpret memory, and static_assert trivial destructibility in AttributePool::make/allocateArray since pool allocations are never destroyed. --- clang/include/clang/Sema/ParsedAttr.h | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/clang/include/clang/Sema/ParsedAttr.h b/clang/include/clang/Sema/ParsedAttr.h index 9ed3f26c4d774..944035aabe929 100644 --- a/clang/include/clang/Sema/ParsedAttr.h +++ b/clang/include/clang/Sema/ParsedAttr.h @@ -511,14 +511,20 @@ class ParsedAttr final } const detail::ProfileEnforceArgs &getProfileEnforceArgs() const { + assert(getKind() == AT_ProfilesEnforce && + "not a profiles::enforce attribute"); return getCustomData(); } const detail::ProfileSuppressArgs &getProfileSuppressArgs() const { + assert(getKind() == AT_ProfilesSuppress && + "not a profiles::suppress attribute"); return getCustomData(); } const detail::ProfileRequireArgs &getProfileRequireArgs() const { + assert(getKind() == AT_ProfilesRequire && + "not a profiles::require attribute"); return getCustomData(); } @@ -767,6 +773,8 @@ class AttributePool { AttributeFactory &getFactory() const { return Factory; } template T *make(Args &&...args) { + // Pool-allocated objects are never destroyed, only deallocated wholesale. + static_assert(std::is_trivially_destructible_v); void *Mem = Factory.Alloc.Allocate(sizeof(T), alignof(T)); return ::new (Mem) T(std::forward(args)...); } @@ -780,6 +788,8 @@ class AttributePool { } template MutableArrayRef allocateArray(unsigned N) { + // Pool-allocated objects are never destroyed, only deallocated wholesale. + static_assert(std::is_trivially_destructible_v); if (N == 0) return {}; auto *Arr = From d6d2abc5a62561b9da878f22d8b77ccc988b8fd2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:07:41 -0400 Subject: [PATCH 165/289] Document TryParseProfilesAttribute and null-init its CustomData --- clang/include/clang/Parse/Parser.h | 5 +++++ clang/lib/Parse/ParseDeclCXX.cpp | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index bfaa108a72115..4374be6790b3d 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2275,6 +2275,11 @@ class Parser : public CodeCompletionHandler { IdentifierInfo *ScopeName, SourceLocation ScopeLoc, ParsedAttr::Form Form); + /// Handle a `profiles::` scoped attribute (P3589R2), which needs custom + /// argument parsing. Returns false if the attribute is not one of the + /// profile attributes and the caller should parse it normally. Returns true + /// if the attribute was consumed -- either parsed successfully (an attribute + /// was added to \p Attrs) or diagnosed and skipped (no attribute added). bool TryParseProfilesAttribute(IdentifierInfo *AttrName, SourceLocation AttrNameLoc, ParsedAttributes &Attrs, diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index b1f0b64e38f5e..8a05cb1559d91 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5314,7 +5314,7 @@ bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, return true; }; - void *CustomData; + void *CustomData = nullptr; if (AttrName->isStr("enforce")) { SmallVector Parsed; if (ParseProfileDesignatorList(Parsed)) From f911ff03e16e3deb53c6d6d7244a14fa1337812e Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:07:41 -0400 Subject: [PATCH 166/289] Use hasEnforcedCFGUninitProfile for both CFG-uninit profile gates --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 4e7d8defc5570..ac5ec6e126eca 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -3527,7 +3527,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( // disables them for all later functions. Run only that analysis, only for // an enforced profile, and only on a valid decl (so the CFG is buildable). // Other analyses keep the early-out. - if (S.anyProfileEnforced(CFGUninitProfiles) && !D->isInvalidDecl() && + if (hasEnforcedCFGUninitProfile() && !D->isInvalidDecl() && !Diags.hasFatalErrorOccurred()) runUninitProfileAnalysisAfterError(S, D); return; @@ -3651,7 +3651,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( Analyzer.run(AC); } - if (S.anyProfileEnforced(CFGUninitProfiles) || + if (hasEnforcedCFGUninitProfile() || !Diags.isIgnored(diag::warn_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_sometimes_uninit_var, D->getBeginLoc()) || !Diags.isIgnored(diag::warn_maybe_uninit_var, D->getBeginLoc()) || From 0fefcaa504e05ce0c3f1243fa348d5bd219fcfac Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:34:34 -0400 Subject: [PATCH 167/289] Share one EnforcedProfile struct across Sema, Module, and the reader Sema::ProfileEnforcement, Module::EnforcedProfile, and the ASTReader's EnforcedProfileEntry were three copies of the same {name, designator} pair; define it once in Basic/Profiles.h. --- clang/include/clang/Basic/Module.h | 6 ++---- clang/include/clang/Basic/Profiles.h | 10 ++++++++++ clang/include/clang/Sema/Sema.h | 5 ++--- clang/include/clang/Serialization/ASTReader.h | 7 ++----- clang/lib/Sema/Sema.cpp | 4 ++-- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/clang/include/clang/Basic/Module.h b/clang/include/clang/Basic/Module.h index 8829e4147ab25..30aa802e11505 100644 --- a/clang/include/clang/Basic/Module.h +++ b/clang/include/clang/Basic/Module.h @@ -17,6 +17,7 @@ #include "clang/Basic/DirectoryEntry.h" #include "clang/Basic/FileEntry.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/DenseSet.h" @@ -458,10 +459,7 @@ class alignas(8) Module { llvm::SmallSetVector Imports; /// Profile enforcements on this module's declaration (P3589R2). - struct EnforcedProfile { - std::string ProfileName; - std::string Designator; - }; + using EnforcedProfile = profiles::EnforcedProfile; SmallVector EnforcedProfileDesignators; /// The set of top-level modules that affected the compilation of this module, diff --git a/clang/include/clang/Basic/Profiles.h b/clang/include/clang/Basic/Profiles.h index 1d7a317760f1a..0390cbdb08283 100644 --- a/clang/include/clang/Basic/Profiles.h +++ b/clang/include/clang/Basic/Profiles.h @@ -29,6 +29,16 @@ struct ProfileArgument { bool isNamed() const { return Kind == ProfileArgumentKind::Named; } }; +/// A profile enforced by [[profiles::enforce]]: the profile name plus the +/// canonical spelling of the designator that enforced it (P3589R2 [decl.attr +/// .enforce]p3 compares repeated enforcements by their spelling). Shared by +/// Sema's enforcement list, Module's exported enforcement set, and the +/// serialized PCH record. +struct EnforcedProfile { + std::string ProfileName; + std::string Designator; +}; + inline std::string getCanonicalProfileArgumentSpelling( const ProfileArgument &Argument) { if (!Argument.isNamed()) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index b217586f96251..a1429e675c2f3 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -49,6 +49,7 @@ #include "clang/Basic/OperatorKinds.h" #include "clang/Basic/PartialDiagnostic.h" #include "clang/Basic/PragmaKinds.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/Specifiers.h" #include "clang/Basic/StackExhaustionHandler.h" @@ -1038,9 +1039,7 @@ class Sema final : public SemaBase { // C++ Profiles framework (P3589R2) - struct ProfileEnforcement { - std::string ProfileName; - std::string CanonicalDesignator; + struct ProfileEnforcement : profiles::EnforcedProfile { SourceLocation EnforceLoc; }; SmallVector EnforcedProfiles; diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h index b6198262e3df6..9ac06f57640bc 100644 --- a/clang/include/clang/Serialization/ASTReader.h +++ b/clang/include/clang/Serialization/ASTReader.h @@ -18,6 +18,7 @@ #include "clang/Basic/DiagnosticOptions.h" #include "clang/Basic/IdentifierTable.h" #include "clang/Basic/OpenCLOptions.h" +#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/StackExhaustionHandler.h" #include "clang/Basic/Version.h" @@ -1008,11 +1009,7 @@ class ASTReader SmallVector FPPragmaOptions; /// Enforced profile designators from PCH (P3589R2). - struct EnforcedProfileEntry { - std::string ProfileName; - std::string Designator; - }; - SmallVector SerializedEnforcedProfiles; + SmallVector SerializedEnforcedProfiles; /// The pragma clang optimize location (if the pragma state is "off"). SourceLocation OptimizeOffPragmaLocation; diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index dc67b9f173473..7b08ac95a5e70 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3007,14 +3007,14 @@ Sema::getProfileEnforcement(StringRef ProfileName) const { bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, SourceLocation Loc) { if (const auto *Existing = getProfileEnforcement(Name)) { - if (Existing->CanonicalDesignator != Designator) { + if (Existing->Designator != Designator) { Diag(Loc, diag::err_profiles_enforce_mismatch) << Name; Diag(Existing->EnforceLoc, diag::note_previous_attribute); return false; } return true; } - EnforcedProfiles.push_back({Name.str(), Designator.str(), Loc}); + EnforcedProfiles.push_back({{Name.str(), Designator.str()}, Loc}); return true; } From d17bbe3d72b4033db6b3efe2932fa9b4f44aad81 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:34:34 -0400 Subject: [PATCH 168/289] Extract enforced-profile blob encode/decode helpers The reader decoded the name-length + blob layout in two switch cases and the writer built the same abbrev and record in two places; give each side one helper so the PCH and submodule records cannot drift apart. --- clang/lib/Serialization/ASTReader.cpp | 26 +++++++------ clang/lib/Serialization/ASTWriter.cpp | 53 +++++++++++++++------------ 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 6e126f72d241e..d24252cb81503 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -3661,6 +3661,16 @@ ASTReader::ReadControlBlock(ModuleFile &F, } } +/// Decode an enforced-profile blob record (P3589R2), the inverse of the +/// ASTWriter encoding: Record[0] is the profile name length and the blob is +/// the concatenated name + designator. Shared by the ENFORCED_PROFILES (PCH) +/// and SUBMODULE_ENFORCED_PROFILES cases. +static profiles::EnforcedProfile readEnforcedProfile(ArrayRef Record, + StringRef Blob) { + unsigned NameLen = Record[0]; + return {Blob.substr(0, NameLen).str(), Blob.substr(NameLen).str()}; +} + llvm::Error ASTReader::ReadASTBlock(ModuleFile &F, unsigned ClientLoadCapabilities) { BitstreamCursor &Stream = F.Stream; @@ -4363,13 +4373,9 @@ llvm::Error ASTReader::ReadASTBlock(ModuleFile &F, FPPragmaOptions.swap(Record); break; - case ENFORCED_PROFILES: { - unsigned NameLen = Record[0]; - std::string Name = Blob.substr(0, NameLen).str(); - std::string Desig = Blob.substr(NameLen).str(); - SerializedEnforcedProfiles.push_back({std::move(Name), std::move(Desig)}); + case ENFORCED_PROFILES: + SerializedEnforcedProfiles.push_back(readEnforcedProfile(Record, Blob)); break; - } case DECLS_WITH_EFFECTS_TO_VERIFY: for (unsigned I = 0, N = Record.size(); I != N; /*in loop*/) @@ -6577,15 +6583,11 @@ llvm::Error ASTReader::ReadSubmoduleBlock(ModuleFile &F, ModMap.addLinkAsDependency(CurrentModule); break; - case SUBMODULE_ENFORCED_PROFILES: { - unsigned NameLen = Record[0]; - std::string Name = Blob.substr(0, NameLen).str(); - std::string Desig = Blob.substr(NameLen).str(); + case SUBMODULE_ENFORCED_PROFILES: CurrentModule->EnforcedProfileDesignators.push_back( - {std::move(Name), std::move(Desig)}); + readEnforcedProfile(Record, Blob)); break; } - } } } diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 867c92f2f4629..53fd993acbf8c 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -2991,6 +2991,28 @@ static unsigned getNumberOfModules(Module *Mod) { return ChildModules + 1; } +/// Create the abbrev for an enforced-profile blob record (P3589R2): the +/// profile name length as VBR followed by the concatenated name + designator +/// blob. Decoded by readEnforcedProfile in ASTReader.cpp. Shared by the +/// ENFORCED_PROFILES (PCH) and SUBMODULE_ENFORCED_PROFILES records. +static unsigned createEnforcedProfileAbbrev(llvm::BitstreamWriter &Stream, + unsigned RecordCode) { + auto Abbrev = std::make_shared(); + Abbrev->Add(llvm::BitCodeAbbrevOp(RecordCode)); + Abbrev->Add(llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::VBR, + 6)); // Profile name length + Abbrev->Add( + llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::Blob)); // Name + Designator + return Stream.EmitAbbrev(std::move(Abbrev)); +} + +static void emitEnforcedProfile(llvm::BitstreamWriter &Stream, + unsigned AbbrevID, unsigned RecordCode, + const profiles::EnforcedProfile &EP) { + uint64_t Record[] = {RecordCode, EP.ProfileName.size()}; + Stream.EmitRecordWithBlob(AbbrevID, Record, EP.ProfileName + EP.Designator); +} + void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { // Enter the submodule description block. Stream.EnterSubblock(SUBMODULE_BLOCK_ID, /*bits for abbreviations*/5); @@ -3086,11 +3108,8 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Macro name unsigned ExportAsAbbrev = Stream.EmitAbbrev(std::move(Abbrev)); - Abbrev = std::make_shared(); - Abbrev->Add(BitCodeAbbrevOp(SUBMODULE_ENFORCED_PROFILES)); - Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::VBR, 6)); // Profile name length - Abbrev->Add(BitCodeAbbrevOp(BitCodeAbbrevOp::Blob)); // Name + Designator - unsigned EnforcedProfilesAbbrev = Stream.EmitAbbrev(std::move(Abbrev)); + unsigned EnforcedProfilesAbbrev = + createEnforcedProfileAbbrev(Stream, SUBMODULE_ENFORCED_PROFILES); // Write the submodule metadata block. RecordData::value_type Record[] = { @@ -3264,12 +3283,9 @@ void ASTWriter::WriteSubmodules(Module *WritingModule, ASTContext *Context) { } // Emit enforced profile designators (P3589R2). - for (const auto &EP : Mod->EnforcedProfileDesignators) { - RecordData::value_type Record[] = {SUBMODULE_ENFORCED_PROFILES, - EP.ProfileName.size()}; - Stream.EmitRecordWithBlob(EnforcedProfilesAbbrev, Record, - EP.ProfileName + EP.Designator); - } + for (const auto &EP : Mod->EnforcedProfileDesignators) + emitEnforcedProfile(Stream, EnforcedProfilesAbbrev, + SUBMODULE_ENFORCED_PROFILES, EP); // Queue up the submodules of this module. for (auto *M : Mod->submodules()) @@ -5308,18 +5324,9 @@ void ASTWriter::WriteEnforcedProfiles(Sema &SemaRef) { if (SemaRef.EnforcedProfiles.empty()) return; - auto Abbrev = std::make_shared(); - Abbrev->Add(llvm::BitCodeAbbrevOp(ENFORCED_PROFILES)); - Abbrev->Add(llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::VBR, 6)); - Abbrev->Add(llvm::BitCodeAbbrevOp(llvm::BitCodeAbbrevOp::Blob)); - unsigned AbbrevID = Stream.EmitAbbrev(std::move(Abbrev)); - - for (const auto &EP : SemaRef.EnforcedProfiles) { - RecordData::value_type Record[] = {ENFORCED_PROFILES, - EP.ProfileName.size()}; - Stream.EmitRecordWithBlob(AbbrevID, Record, - EP.ProfileName + EP.CanonicalDesignator); - } + unsigned AbbrevID = createEnforcedProfileAbbrev(Stream, ENFORCED_PROFILES); + for (const auto &EP : SemaRef.EnforcedProfiles) + emitEnforcedProfile(Stream, AbbrevID, ENFORCED_PROFILES, EP); } //===----------------------------------------------------------------------===// From 663de1282c6406652d523dc80bc4b93c5796415f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:34:58 -0400 Subject: [PATCH 169/289] Keep a single getCanonicalProfileArgumentSpelling The parser had a template clone of the Profiles.h helper (which itself had no callers); make the Profiles.h version the one template implementation. --- clang/include/clang/Basic/Profiles.h | 14 ++++++++++---- clang/lib/Parse/ParseDeclCXX.cpp | 16 +++++----------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/clang/include/clang/Basic/Profiles.h b/clang/include/clang/Basic/Profiles.h index 0390cbdb08283..34b3e2c1b8d4e 100644 --- a/clang/include/clang/Basic/Profiles.h +++ b/clang/include/clang/Basic/Profiles.h @@ -11,6 +11,7 @@ #include "clang/Basic/SourceLocation.h" #include "llvm/ADT/StringRef.h" +#include "llvm/ADT/Twine.h" #include namespace clang::profiles { @@ -39,11 +40,16 @@ struct EnforcedProfile { std::string Designator; }; -inline std::string getCanonicalProfileArgumentSpelling( - const ProfileArgument &Argument) { +/// The canonical spelling of a profile argument: the value token for a +/// positional argument, "key : value" for a named one. Enforcement identity +/// (P3589R2 [decl.attr.enforce]p3) compares designators by this spelling. +/// A template over the argument representation so it serves both +/// ProfileArgument and the parser's owning-string argument type. +template +std::string getCanonicalProfileArgumentSpelling(const ArgumentT &Argument) { if (!Argument.isNamed()) - return Argument.Value.str(); - return (Argument.Key + " : " + Argument.Value).str(); + return std::string(Argument.Value); + return (llvm::Twine(Argument.Key) + " : " + Argument.Value).str(); } } // namespace clang::profiles diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index 8a05cb1559d91..cabaab6bff368 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5067,14 +5067,6 @@ bool Parser::ParseProfileName(std::string &Name) { return false; } -template -static std::string -getCanonicalProfileArgumentSpelling(const ProfileArgument &Argument) { - if (!Argument.isNamed()) - return Argument.Value; - return Argument.Key + " : " + Argument.Value; -} - template static ArrayRef copyProfileArguments( AttributePool &Pool, const ProfileArguments &Parsed) { @@ -5193,7 +5185,7 @@ bool Parser::ParseProfileDesignator(ParsedProfileDesignator &D) { for (unsigned I = 0; I < D.Arguments.size(); ++I) { if (I > 0) D.Spelling += ", "; - D.Spelling += getCanonicalProfileArgumentSpelling(D.Arguments[I]); + D.Spelling += profiles::getCanonicalProfileArgumentSpelling(D.Arguments[I]); } if (!Tok.is(tok::r_paren)) { @@ -5241,7 +5233,8 @@ bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { ParsedProfileDesignator::Argument Arg; if (ParseNonOperatorNonPunctuatorToken(Arg.Value, &Arg.Range)) return true; - Args.RawArguments.push_back(getCanonicalProfileArgumentSpelling(Arg)); + Args.RawArguments.push_back( + profiles::getCanonicalProfileArgumentSpelling(Arg)); Args.Arguments.push_back(std::move(Arg)); continue; } @@ -5279,7 +5272,8 @@ bool Parser::ParseProfileSuppressBody(ParsedProfileSuppressArgs &Args) { Arg.Value = std::move(Value); Arg.Kind = profiles::ProfileArgumentKind::Named; Arg.Range = SourceRange(KeyLoc, ValueRange.getEnd()); - Args.RawArguments.push_back(getCanonicalProfileArgumentSpelling(Arg)); + Args.RawArguments.push_back( + profiles::getCanonicalProfileArgumentSpelling(Arg)); Args.Arguments.push_back(std::move(Arg)); } From 036c14262052446347b4bc1b0eca46f0af3cf410 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:34:58 -0400 Subject: [PATCH 170/289] Fold the duplicated module-attribute rejection loops into a helper ParseModuleDecl and ParseModuleImport had the same reject-all-but-one-kind loop differing only in the allowed profile attribute and diagnostics. --- clang/include/clang/Parse/Parser.h | 9 ++++++++ clang/lib/Parse/Parser.cpp | 36 +++++++++++++++++------------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h index 4374be6790b3d..6f6d048244d32 100644 --- a/clang/include/clang/Parse/Parser.h +++ b/clang/include/clang/Parse/Parser.h @@ -2322,6 +2322,15 @@ class Parser : public CodeCompletionHandler { bool ParseNonOperatorNonPunctuatorToken(std::string &Spelling, SourceRange *Range = nullptr); + /// Diagnose C++11 attributes on a module- or import-declaration, which + /// accept none except the profile attribute \p AllowedKind (handled in + /// Sema). \p KeywordDiagID / \p AttrDiagID are the declaration's + /// keyword-attribute and standard-attribute diagnostics. + void ProhibitModuleAttributesExcept(const ParsedAttributesView &Attrs, + ParsedAttr::Kind AllowedKind, + unsigned KeywordDiagID, + unsigned AttrDiagID); + void MaybeParseCXX11Attributes(Declarator &D) { if (isAllowedCXX11AttributeSpecifier()) { ParsedAttributes Attrs(AttrFactory); diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp index d24abaade1aa9..3db4729ac8124 100644 --- a/clang/lib/Parse/Parser.cpp +++ b/clang/lib/Parse/Parser.cpp @@ -2307,6 +2307,20 @@ void Parser::ParseMicrosoftIfExistsExternalDeclaration() { Braces.consumeClose(); } +void Parser::ProhibitModuleAttributesExcept(const ParsedAttributesView &Attrs, + ParsedAttr::Kind AllowedKind, + unsigned KeywordDiagID, + unsigned AttrDiagID) { + for (const ParsedAttr &AL : Attrs) { + if (AL.getKind() == AllowedKind) + continue; + if (AL.isRegularKeywordAttribute()) + Diag(AL.getLoc(), KeywordDiagID) << AL; + else if (AL.isStandardAttributeSyntax()) + Diag(AL.getLoc(), AttrDiagID) << AL; + } +} + Parser::DeclGroupPtrTy Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { Token Introducer = Tok; @@ -2383,14 +2397,9 @@ Parser::ParseModuleDecl(Sema::ModuleImportState &ImportState) { // Reject non-profile attributes on the module-declaration. Profile // attributes are handled by ActOnModuleDecl. - for (const ParsedAttr &AL : Attrs) { - if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce) - continue; - if (AL.isRegularKeywordAttribute()) - Diag(AL.getLoc(), diag::err_keyword_not_module_attr) << AL; - else if (AL.isStandardAttributeSyntax()) - Diag(AL.getLoc(), diag::err_attribute_not_module_attr) << AL; - } + ProhibitModuleAttributesExcept(Attrs, ParsedAttr::AT_ProfilesEnforce, + diag::err_keyword_not_module_attr, + diag::err_attribute_not_module_attr); if (ExpectAndConsumeSemi(diag::err_expected_semi_after_module_or_import, tok::getKeywordSpelling(tok::kw_module))) @@ -2448,14 +2457,9 @@ Decl *Parser::ParseModuleImport(SourceLocation AtLoc, // Reject non-profile attributes on the import-declaration. Profile // attributes are handled by ActOnModuleImportAttrs. - for (const ParsedAttr &AL : Attrs) { - if (AL.getKind() == ParsedAttr::AT_ProfilesRequire) - continue; - if (AL.isRegularKeywordAttribute()) - Diag(AL.getLoc(), diag::err_keyword_not_import_attr) << AL; - else if (AL.isStandardAttributeSyntax()) - Diag(AL.getLoc(), diag::err_attribute_not_import_attr) << AL; - } + ProhibitModuleAttributesExcept(Attrs, ParsedAttr::AT_ProfilesRequire, + diag::err_keyword_not_import_attr, + diag::err_attribute_not_import_attr); if (PP.hadModuleLoaderFatalFailure()) { // With a fatal failure in the module loader, we abort parsing. From 02efabf910c9940538a14e272221a738404fa572 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:37:10 -0400 Subject: [PATCH 171/289] Derive uninit_with_initializer inputs from the checked declaration checkInitProfileUninitWithInitializer took six parameters that both call sites spelled out identically from the same declaration; take the ValueDecl and the initializer instead. --- clang/include/clang/Sema/Sema.h | 9 +++------ clang/lib/Sema/SemaDecl.cpp | 21 +++++++++------------ clang/lib/Sema/SemaDeclCXX.cpp | 6 +----- 3 files changed, 13 insertions(+), 23 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index a1429e675c2f3..b9da4e7169d59 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1102,17 +1102,14 @@ class Sema final : public SemaBase { bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); - /// std::init / uninit_with_initializer (R4): diagnose an entity that is both + /// std::init / uninit_with_initializer (R4): diagnose \p D if it is both /// marked [[uninit]] and has an initializer. Shared by the variable /// (\c CheckCompleteVariableDeclaration) and non-static data member /// (\c ActOnFinishCXXInClassMemberInitializer) paths. \p Init is the /// (possibly null) initializer; a RecoveryExpr placeholder for a failed /// initialization does not count as a user-written initializer. - void checkInitProfileUninitWithInitializer(SourceLocation Loc, - DeclarationName Name, - QualType DeclType, - const Expr *Init, bool HasMarker, - const Decl *D); + void checkInitProfileUninitWithInitializer(const ValueDecl *D, + const Expr *Init); /// True if default-initialization of \p T would leave at least one scalar /// subobject with an indeterminate value. Shared by the std::init rules diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 19ea76a5c592b..e193c667d2c52 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14945,19 +14945,17 @@ bool Sema::defaultInitLeavesScalarIndeterminate(QualType T, HonorUninitMarkers, Visited); } -void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, - DeclarationName Name, - QualType DeclType, - const Expr *Init, - bool HasMarker, - const Decl *D) { +void Sema::checkInitProfileUninitWithInitializer(const ValueDecl *D, + const Expr *Init) { // [[uninit]] documents that the entity is intentionally left // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr // is a placeholder for an initialization that already failed (e.g. // default-init of a const scalar), not an initializer the user wrote, so it // must not trigger this rule. - if (!HasMarker || !Init || isa(Init->IgnoreParens())) + if (!D->hasAttr() || !Init || + isa(Init->IgnoreParens())) return; + SourceLocation Loc = D->getLocation(); static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "uninit_with_initializer"; // Gate the (possibly recursive) type walk below on enforcement. @@ -14971,9 +14969,10 @@ void Sema::checkInitProfileUninitWithInitializer(SourceLocation Loc, // -- contradicts the marker. if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) if (CCE->getConstructor()->isDefaultConstructor() && - defaultInitLeavesScalarIndeterminate(DeclType)) + defaultInitLeavesScalarIndeterminate(D->getType())) return; - Diag(Loc, diag::err_init_uninit_with_initializer) << Profile << Name; + Diag(Loc, diag::err_init_uninit_with_initializer) + << Profile << D->getDeclName(); } void Sema::diagnoseInitUninitMarkerPlacement(const Decl *D) { @@ -15272,9 +15271,7 @@ void Sema::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; - checkInitProfileUninitWithInitializer( - var->getLocation(), var->getDeclName(), var->getType(), var->getInit(), - var->hasAttr(), var); + checkInitProfileUninitWithInitializer(var, var->getInit()); // std::init / ref_to_uninit (paper §5): a pointer or reference variable must // be bound consistently with its [[ref_to_uninit]] marking. A dependent type diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index f476acec06ccf..51ae398649ee9 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4231,11 +4231,7 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, // the field and its lexical parents. This does not depend on a parse-time // suppress scope still being active (the late-parsed NSDMI finishes parsing // before this finalization runs). - checkInitProfileUninitWithInitializer(FD->getLocation(), FD->getDeclName(), - FD->getType(), - FD->getInClassInitializer(), - FD->hasAttr(), - FD); + checkInitProfileUninitWithInitializer(FD, FD->getInClassInitializer()); // std::init / ref_to_uninit (paper §5): a pointer or reference data member // with a default member initializer must be bound consistently with its From 6094e01bb40085314d240950b81a540c828e1daa Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:38:07 -0400 Subject: [PATCH 172/289] Name the static_runtime_init check instead of an else-if lambda The rule lived in an immediately-invoked lambda inside an else-if condition with an empty body; a named member makes the control flow and the diagnostic side effect explicit. --- clang/include/clang/Sema/Sema.h | 9 ++++++ clang/lib/Sema/SemaDecl.cpp | 54 +++++++++++++++++---------------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index b9da4e7169d59..ba087f27a1c40 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1102,6 +1102,15 @@ class Sema final : public SemaBase { bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); + /// std::init / static_runtime_init (paper §3): diagnose a non-local static + /// whose initialization needs a runtime constructor. \p CheckConstInit + /// lazily evaluates whether the initializer is constant (trivial + /// default-init counts as constant here). Returns true if the diagnostic + /// was emitted, in which case the caller skips -Wglobal-constructors. + bool + checkInitProfileStaticRuntimeInit(const VarDecl *Var, + llvm::function_ref CheckConstInit); + /// std::init / uninit_with_initializer (R4): diagnose \p D if it is both /// marked [[uninit]] and has an initializer. Shared by the variable /// (\c CheckCompleteVariableDeclaration) and non-static data member diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index e193c667d2c52..7738f1de5e1ed 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14945,6 +14945,31 @@ bool Sema::defaultInitLeavesScalarIndeterminate(QualType T, HonorUninitMarkers, Visited); } +bool Sema::checkInitProfileStaticRuntimeInit( + const VarDecl *Var, llvm::function_ref CheckConstInit) { + // Thread-locals have thread (not static) storage duration; paper §3 scopes + // this rule to non-local *static* objects (uninit_decl likewise excludes + // thread storage). + if (Var->getTLSKind() != VarDecl::TLS_None) + return false; + // std::init / static_runtime_init: paper says non-local statics must be + // initialized at compile or link time. CheckConstInit() permits trivial + // default initialization (not a constant initializer but needs no global + // constructor), so a zero-initialized aggregate such as + // `struct S { int x; }; S g;` is not a violation. Runs before + // -Wglobal-constructors so the profile error (when enforced) takes + // precedence over the standalone warning. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "static_runtime_init"; + if (CheckConstInit()) + return false; + if (!shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var)) + return false; + Diag(Var->getLocation(), diag::err_init_static_runtime_init) + << Profile << Var->getDeclName(); + return true; +} + void Sema::checkInitProfileUninitWithInitializer(const ValueDecl *D, const Expr *Init) { // [[uninit]] documents that the entity is intentionally left @@ -15517,32 +15542,9 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { for (auto &it : Notes) Diag(it.first, it.second); var->setInvalidDecl(); - } else if (IsGlobal && [&] { - // Thread-locals have thread (not static) storage duration; - // paper §3 scopes this rule to non-local *static* objects - // (uninit_decl likewise excludes thread storage). - if (var->getTLSKind() != VarDecl::TLS_None) - return false; - // std::init / static_runtime_init: paper says non-local - // statics must be initialized at compile or link time. - // checkConstInit() permits trivial default initialization - // (not a constant initializer but needs no global - // constructor), so a zero-initialized aggregate such as - // `struct S { int x; }; S g;` is not a violation. Runs before - // -Wglobal-constructors so the profile error (when enforced) - // takes precedence over the standalone warning. - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "static_runtime_init"; - if (checkConstInit()) - return false; - if (!shouldEmitProfileViolation(Profile, Rule, - var->getLocation(), var)) - return false; - Diag(var->getLocation(), diag::err_init_static_runtime_init) - << Profile << var->getDeclName(); - return true; - }()) { - // Diagnostic emitted in the lambda above. + } else if (IsGlobal && + checkInitProfileStaticRuntimeInit(var, checkConstInit)) { + // The profile diagnostic supersedes -Wglobal-constructors below. } else if (IsGlobal && !getDiagnostics().isIgnored(diag::warn_global_constructor, var->getLocation())) { From 8b49eae65bfb46d77613511b7a580ebf56202c44 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:39:20 -0400 Subject: [PATCH 173/289] Extract the uninit_decl and static_marker rules into named members ActOnUninitializedDecl carried two ~40-line inline condition chains; give each rule a named entry point alongside the other std::init checks. --- clang/include/clang/Sema/Sema.h | 12 +++ clang/lib/Sema/SemaDecl.cpp | 134 +++++++++++++++++--------------- 2 files changed, 83 insertions(+), 63 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index ba087f27a1c40..627ce89044182 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1102,6 +1102,18 @@ class Sema final : public SemaBase { bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); + /// std::init / uninit_decl (R2, paper §4.2): diagnose an automatic variable + /// definition that leaves the object (or a scalar subobject) indeterminate + /// without an acknowledging [[uninit]] marker. Called from + /// \c ActOnUninitializedDecl after default-initialization is attempted. + void checkInitProfileUninitDecl(const VarDecl *Var); + + /// std::init / static_marker (paper §3, §4.2): diagnose [[uninit]] on a + /// static or thread-storage variable, which is zero-initialized by language + /// rule and therefore an initialized object. Called from + /// \c ActOnUninitializedDecl. + void checkInitProfileStaticMarker(const VarDecl *Var); + /// std::init / static_runtime_init (paper §3): diagnose a non-local static /// whose initialization needs a runtime constructor. \p CheckConstInit /// lazily evaluates whether the initializer is constant (trivial diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 7738f1de5e1ed..c16b344a87c9c 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14715,69 +14715,8 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { Var->setInit(RecoveryExpr.get()); } - // std::init / uninit_decl: a definition without any initializer (after - // attempted default-initialization) must either carry [[uninit]] or - // be initialized by a language rule. Static / thread storage duration is - // excluded -- those are zero-initialized; runtime-init concerns are R3's. - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "uninit_decl"; - // The enforcement check gates the (possibly recursive) type walk below so - // it runs only under the profile, not on every default-initialized - // variable. - QualType BaseTy = Context.getBaseElementType(Var->getType()); - if (!Var->isInvalidDecl() && - Var->getStorageDuration() == SD_Automatic && - !Var->hasAttr() && - // std::byte may be left uninitialized (paper §4), so it -- and arrays - // of it -- are exempt from this rule. - !BaseTy->isStdByteType() && - shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && - // A definition with no initializer (scalar / pointer / enum, or an - // array of them), or a class/aggregate type -- possibly the element - // type of an array -- whose default-init leaves a scalar subobject - // indeterminate (its synthesized constructor call provides an - // initializer, so the !getInit() test alone misses it). - (!Var->getInit() || - (BaseTy->isRecordType() && - defaultInitLeavesScalarIndeterminate( - Var->getType(), /*HonorUninitMarkers=*/true)))) { - // A union variable cannot carry [[uninit]] (union_marker bans it), - // so it must be initialized; use a message that does not suggest the - // marker as a remedy. - bool IsUnion = BaseTy->isUnionType(); - Diag(Var->getLocation(), - IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) - << Profile << Var->getDeclName(); - } - - // std::init / static_marker: a variable with static or thread storage - // duration is zero-initialized by language rule (paper §3), so it is an - // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an - // initialized object marked [[uninit]] is an error"). The case with a real - // initializer -- explicit, or a constructor that actually runs -- is - // already caught by uninit_with_initializer (R4, in - // CheckCompleteVariableDeclaration below); this covers the zero-initialized, - // no-real-initializer case R4 treats as a consistent no-op. The factual - // (HonorUninitMarkers=false) walk matches R4: a static object whose only - // indeterminate scalars are themselves marked is still zero-initialized. - if (!Var->isInvalidDecl() && - (Var->getStorageDuration() == SD_Static || - Var->getStorageDuration() == SD_Thread) && - Var->hasAttr() && - // A union or pointer object marked [[uninit]] is already rejected by - // union_marker / pointer_marker (regardless of storage duration), and - // they retain the marker; do not pile a second diagnostic on top. - !Var->getType()->isUnionType() && !Var->getType()->isPointerType() && - shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), - Var) && - (!Var->getInit() || - (BaseTy->isRecordType() && - defaultInitLeavesScalarIndeterminate( - Var->getType(), /*HonorUninitMarkers=*/false)))) { - bool IsThread = Var->getStorageDuration() == SD_Thread; - Diag(Var->getLocation(), diag::err_init_uninit_static_marker) - << Profile << Var->getDeclName() << IsThread; - } + checkInitProfileUninitDecl(Var); + checkInitProfileStaticMarker(Var); CheckCompleteVariableDeclaration(Var); } @@ -14945,6 +14884,75 @@ bool Sema::defaultInitLeavesScalarIndeterminate(QualType T, HonorUninitMarkers, Visited); } +void Sema::checkInitProfileUninitDecl(const VarDecl *Var) { + // std::init / uninit_decl: a definition without any initializer (after + // attempted default-initialization) must either carry [[uninit]] or + // be initialized by a language rule. Static / thread storage duration is + // excluded -- those are zero-initialized; runtime-init concerns are R3's. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_decl"; + // The enforcement check gates the (possibly recursive) type walk below so + // it runs only under the profile, not on every default-initialized + // variable. + QualType BaseTy = Context.getBaseElementType(Var->getType()); + if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && + !Var->hasAttr() && + // std::byte may be left uninitialized (paper §4), so it -- and arrays + // of it -- are exempt from this rule. + !BaseTy->isStdByteType() && + shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && + // A definition with no initializer (scalar / pointer / enum, or an + // array of them), or a class/aggregate type -- possibly the element + // type of an array -- whose default-init leaves a scalar subobject + // indeterminate (its synthesized constructor call provides an + // initializer, so the !getInit() test alone misses it). + (!Var->getInit() || + (BaseTy->isRecordType() && + defaultInitLeavesScalarIndeterminate(Var->getType(), + /*HonorUninitMarkers=*/true)))) { + // A union variable cannot carry [[uninit]] (union_marker bans it), + // so it must be initialized; use a message that does not suggest the + // marker as a remedy. + bool IsUnion = BaseTy->isUnionType(); + Diag(Var->getLocation(), + IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) + << Profile << Var->getDeclName(); + } +} + +void Sema::checkInitProfileStaticMarker(const VarDecl *Var) { + // std::init / static_marker: a variable with static or thread storage + // duration is zero-initialized by language rule (paper §3), so it is an + // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an + // initialized object marked [[uninit]] is an error"). The case with a real + // initializer -- explicit, or a constructor that actually runs -- is + // already caught by uninit_with_initializer (R4, in + // CheckCompleteVariableDeclaration); this covers the zero-initialized, + // no-real-initializer case R4 treats as a consistent no-op. The factual + // (HonorUninitMarkers=false) walk matches R4: a static object whose only + // indeterminate scalars are themselves marked is still zero-initialized. + static constexpr StringRef Profile = "std::init"; + QualType BaseTy = Context.getBaseElementType(Var->getType()); + if (!Var->isInvalidDecl() && + (Var->getStorageDuration() == SD_Static || + Var->getStorageDuration() == SD_Thread) && + Var->hasAttr() && + // A union or pointer object marked [[uninit]] is already rejected by + // union_marker / pointer_marker (regardless of storage duration), and + // they retain the marker; do not pile a second diagnostic on top. + !Var->getType()->isUnionType() && !Var->getType()->isPointerType() && + shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), + Var) && + (!Var->getInit() || + (BaseTy->isRecordType() && + defaultInitLeavesScalarIndeterminate(Var->getType(), + /*HonorUninitMarkers=*/false)))) { + bool IsThread = Var->getStorageDuration() == SD_Thread; + Diag(Var->getLocation(), diag::err_init_uninit_static_marker) + << Profile << Var->getDeclName() << IsThread; + } +} + bool Sema::checkInitProfileStaticRuntimeInit( const VarDecl *Var, llvm::function_ref CheckConstInit) { // Thread-locals have thread (not static) storage duration; paper §3 scopes From b064ebc3c182c97e0b6a657452d758a512a2e490 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:41:39 -0400 Subject: [PATCH 174/289] Share the uninit-recognizer pass-through and marker-lookup arms The pointer and glvalue recognizers duplicated the braced-init / conditional / comma pass-through preamble and the direct-callee arm, and the DeclRef/Member marker lookup appeared a third time in the pointer assignment check; factor out classifyUninitPassThrough, classifyRefToUninitCallee, and Sema::getDirectlyNamedDecl. --- clang/include/clang/Sema/Sema.h | 6 ++ clang/lib/Sema/SemaDecl.cpp | 129 +++++++++++++++++--------------- clang/lib/Sema/SemaExpr.cpp | 9 +-- 3 files changed, 76 insertions(+), 68 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 627ce89044182..68b67b0cd39f9 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1149,6 +1149,12 @@ class Sema final : public SemaBase { bool defaultInitLeavesScalarIndeterminate(QualType T, bool HonorUninitMarkers = false); + /// If \p E (stripped of parens and implicit casts) directly names a + /// declaration -- a DeclRefExpr or a MemberExpr -- return that declaration; + /// otherwise null. The std::init checks read [[ref_to_uninit]] / + /// [[uninit]] markers only off a directly named entity. + static const ValueDecl *getDirectlyNamedDecl(const Expr *E); + /// std::init / ref_to_uninit (paper §5): true only if \p E is affirmatively /// recognized as referring to (for a pointer source) or, when \p IsReference, /// denoting (for a glvalue source) uninitialized storage. Recognized purely diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index c16b344a87c9c..9863b922c9a3f 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15073,6 +15073,52 @@ static UninitStorage combineArms(UninitStorage A, UninitStorage B) { static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, bool ForRead = false); +const ValueDecl *Sema::getDirectlyNamedDecl(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (const auto *DRE = dyn_cast(E)) + return DRE->getDecl(); + if (const auto *ME = dyn_cast(E)) + return ME->getMemberDecl(); + return nullptr; +} + +// Pass-through forms shared by the pointer and glvalue recognizers, which are +// transparent to their operand: a single-element braced initializer { e } +// binds from e (modeling +// MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); a conditional +// is uninit if either arm is, so a value that may be uninit forces a marked +// target; a comma yields its right operand. \p EmptyListState classifies an +// empty braced list: {} value-initializes a pointer to null (Initialized), +// while a glvalue has no empty-list form (Unknown); a multi-element list is +// Unknown for both. Returns std::nullopt when E is not a pass-through form. +template +static std::optional +classifyUninitPassThrough(const Expr *E, UninitStorage EmptyListState, + RecurseFn Recurse) { + if (const auto *ILE = dyn_cast(E)) { + if (ILE->getNumInits() == 1) + return Recurse(ILE->getInit(0)); + return ILE->getNumInits() == 0 ? EmptyListState : UninitStorage::Unknown; + } + if (const auto *CO = dyn_cast(E)) + return combineArms(Recurse(CO->getTrueExpr()), + Recurse(CO->getFalseExpr())); + if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) + return Recurse(BO->getRHS()); + return std::nullopt; +} + +// A call to a [[ref_to_uninit]]-returning function yields uninitialized +// storage (the pointed-to memory, or the returned referent). An unmarked +// direct callee is trusted Initialized (paper §4.3); a call with no direct +// callee (through a function pointer) is Unknown. Shared by both recognizers. +static UninitStorage classifyRefToUninitCallee(const CallExpr *CE) { + if (const FunctionDecl *FD = CE->getDirectCallee()) + return FD->hasAttr() ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + return UninitStorage::Unknown; +} + // \p E is a pointer prvalue. Classifies whether it points to uninitialized // storage. static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, @@ -15082,25 +15128,12 @@ static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); - // Pass-through forms are transparent to their operand: a single-element - // braced initializer { e } binds from e (modeling - // MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); an empty {} - // value-initializes to a null pointer (Initialized) and a multi-element list - // is not a pointer source (Unknown). A conditional is uninit if either arm - // is, so a value that may be uninit forces a marked target; a comma yields - // its right operand. - if (const auto *ILE = dyn_cast(E)) { - if (ILE->getNumInits() == 1) - return pointerRefersToUninitStorage(Ctx, ILE->getInit(0), ForRead); - return ILE->getNumInits() == 0 ? UninitStorage::Initialized - : UninitStorage::Unknown; - } - if (const auto *CO = dyn_cast(E)) - return combineArms( - pointerRefersToUninitStorage(Ctx, CO->getTrueExpr(), ForRead), - pointerRefersToUninitStorage(Ctx, CO->getFalseExpr(), ForRead)); - if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) - return pointerRefersToUninitStorage(Ctx, BO->getRHS(), ForRead); + if (auto PassThrough = classifyUninitPassThrough( + E, /*EmptyListState=*/UninitStorage::Initialized, + [&](const Expr *Sub) { + return pointerRefersToUninitStorage(Ctx, Sub, ForRead); + })) + return *PassThrough; // Array-to-pointer decay has been stripped above, leaving the array glvalue. if (E->getType()->isArrayType()) @@ -15111,24 +15144,13 @@ static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, if (UO->getOpcode() == UO_AddrOf) return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), ForRead); - // A value of a [[ref_to_uninit]] pointer, or a call to a - // [[ref_to_uninit]]-returning function, is Uninitialized. An unmarked named - // pointer or direct callee is a trusted Initialized pointer (paper §4.3); a - // call with no direct callee (through a function pointer) is Unknown. - if (const auto *DRE = dyn_cast(E)) - return DRE->getDecl()->hasAttr() - ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - if (const auto *ME = dyn_cast(E)) - return ME->getMemberDecl()->hasAttr() - ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - if (const auto *CE = dyn_cast(E)) { - if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->hasAttr() ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - return UninitStorage::Unknown; - } + // A value of a [[ref_to_uninit]] pointer is Uninitialized; an unmarked + // named pointer is a trusted Initialized pointer (paper §4.3). + if (const ValueDecl *VD = Sema::getDirectlyNamedDecl(E)) + return VD->hasAttr() ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + if (const auto *CE = dyn_cast(E)) + return classifyRefToUninitCallee(CE); // A default-initialized new-expression (none init style: no initializer // written) whose allocated type's default-initialization leaves a scalar @@ -15166,21 +15188,12 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); - // Pass-through forms are transparent to their operand, routed through the - // glvalue recognizer (symmetric to the pointer side): a single-element braced - // initializer binds from its element, a conditional is uninit if either arm - // is, and a comma yields its right operand. - if (const auto *ILE = dyn_cast(E)) { - if (ILE->getNumInits() == 1) - return glvalueDenotesUninitStorage(Ctx, ILE->getInit(0), ForRead); - return UninitStorage::Unknown; - } - if (const auto *CO = dyn_cast(E)) - return combineArms( - glvalueDenotesUninitStorage(Ctx, CO->getTrueExpr(), ForRead), - glvalueDenotesUninitStorage(Ctx, CO->getFalseExpr(), ForRead)); - if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) - return glvalueDenotesUninitStorage(Ctx, BO->getRHS(), ForRead); + if (auto PassThrough = classifyUninitPassThrough( + E, /*EmptyListState=*/UninitStorage::Unknown, + [&](const Expr *Sub) { + return glvalueDenotesUninitStorage(Ctx, Sub, ForRead); + })) + return *PassThrough; // A named entity denotes uninitialized storage if it is [[uninit]], or // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, @@ -15207,15 +15220,9 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead); } // A call to a [[ref_to_uninit]]-returning reference function: the referent - // it returns is uninitialized. An unmarked direct callee returns a trusted - // Initialized referent; a call with no direct callee is Unknown. Mirrors the - // pointer recognizer's call arm. - if (const auto *CE = dyn_cast(E)) { - if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->hasAttr() ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - return UninitStorage::Unknown; - } + // it returns is uninitialized. + if (const auto *CE = dyn_cast(E)) + return classifyRefToUninitCallee(CE); if (const auto *ASE = dyn_cast(E)) return pointerRefersToUninitStorage(Ctx, ASE->getBase(), ForRead); diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index b56685f849318..98a8fafc6491f 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -15519,13 +15519,8 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, // unmarked pointer (paper §4.3) and must not be bound to uninitialized // memory. if (getLangOpts().Profiles && LHS.get()->getType()->isPointerType()) { - const Expr *L = LHS.get()->IgnoreParenImpCasts(); - bool TargetIsRefToUninit = false; - if (const auto *DRE = dyn_cast(L)) - TargetIsRefToUninit = DRE->getDecl()->hasAttr(); - else if (const auto *ME = dyn_cast(L)) - TargetIsRefToUninit = ME->getMemberDecl()->hasAttr(); - checkRefToUninitInit(OpLoc, TargetIsRefToUninit, + const ValueDecl *VD = getDirectlyNamedDecl(LHS.get()); + checkRefToUninitInit(OpLoc, VD && VD->hasAttr(), /*IsReference=*/false, RHS.get()); } From 586515e98712e051dd6174e65eee270cdccec233 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:42:32 -0400 Subject: [PATCH 175/289] Share the template-pattern deferral of the Decl-less std::init checks checkRefToUninitInit and checkRefToUninitRead carried the same guard and rationale; one helper now owns both, and documents why the deferral must not move into shouldEmitProfileViolation (the reinterpret_cast check is not re-run at instantiation, so deferring it would lose the diagnostic). --- clang/lib/Sema/SemaDecl.cpp | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 9863b922c9a3f..03d526e2d28d1 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15252,6 +15252,19 @@ bool Sema::refersToUninitializedMemory(const Expr *E, bool IsReference) const { UninitStorage::Uninitialized; } +// The Decl-less ref_to_uninit check sites (call argument, default argument, +// assignment, return, read-through) can't rely on the D->isTemplated() +// deferral in shouldEmitProfileViolation, but their Build* routines are +// re-run at instantiation, so defer on a template pattern here instead: +// firing on the pattern double-diagnoses and wrongly fires in discarded +// if-constexpr branches / never-instantiated templates. Sites that do pass a +// Decl are unaffected. (This must stay out of shouldEmitProfileViolation: +// its other Decl-less caller, the reinterpret_cast check, is *not* re-run at +// instantiation, so deferring there would lose the diagnostic entirely.) +static bool deferUninitCheckOnTemplatePattern(Sema &S, const Decl *D) { + return !D && S.CurContext && S.CurContext->isDependentContext(); +} + void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, bool IsReference, const Expr *Src, const Decl *D) { @@ -15259,13 +15272,7 @@ void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, // not a source the user wrote, so it must not drive this rule. if (!Src || isa(Src->IgnoreParens())) return; - // The call-argument, default-argument, assignment, and return sites pass no - // Decl, so the D->isTemplated() deferral in shouldEmitProfileViolation can't - // fire. Defer on a template pattern here instead: the binding is re-checked - // on the instantiated expression, so firing on the pattern double-diagnoses - // and wrongly fires in discarded if-constexpr branches / never-instantiated - // templates. The variable and data-member sites pass D and are unaffected. - if (!D && CurContext && CurContext->isDependentContext()) + if (deferUninitCheckOnTemplatePattern(*this, D)) return; static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "ref_to_uninit"; @@ -15290,12 +15297,7 @@ void Sema::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, // a read the user wrote, so it must not drive this rule. if (!Glvalue || isa(Glvalue->IgnoreParens())) return; - // This read site passes no Decl, so the D->isTemplated() deferral in - // shouldEmitProfileViolation can't fire. Defer on a template pattern here: - // the read is re-checked on the instantiated expression, so firing on the - // pattern double-diagnoses and wrongly fires in discarded if-constexpr - // branches / never-instantiated templates (mirrors checkRefToUninitInit). - if (CurContext && CurContext->isDependentContext()) + if (deferUninitCheckOnTemplatePattern(*this, /*D=*/nullptr)) return; // Paper §4.5: reading an uninitialized std::byte is permitted. if (Context.getBaseElementType(ValueType)->isStdByteType()) From 263025d4d64eeeb4d1aacdbbcb83293cadb93837 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:43:05 -0400 Subject: [PATCH 176/289] Merge the duplicated call-argument ref_to_uninit checks The regular- and default-argument branches of GatherArgumentsForCall carried the same check, differing only in unwrapping CXXDefaultArgExpr; hoist one copy below the branch and unwrap there. --- clang/lib/Sema/SemaExpr.cpp | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 98a8fafc6491f..83db6073147d2 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6270,16 +6270,6 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, return true; Arg = ArgE.getAs(); - - // std::init / ref_to_uninit (paper §5): a pointer or reference argument - // must match the [[ref_to_uninit]] marking of its parameter. - if (Param && getLangOpts().Profiles) { - QualType PT = Param->getType(); - if (PT->isPointerType() || PT->isReferenceType()) - checkRefToUninitInit(Arg->getExprLoc(), - Param->hasAttr(), - PT->isReferenceType(), Arg); - } } else { assert(Param && "can't use default arguments without a known callee"); @@ -6288,16 +6278,21 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, return true; Arg = ArgExpr.getAs(); + } - // The recognizers don't see through the CXXDefaultArgExpr wrapper, so - // check the underlying default-argument expression. - if (getLangOpts().Profiles) { - QualType PT = Param->getType(); - if (PT->isPointerType() || PT->isReferenceType()) - checkRefToUninitInit(Arg->getExprLoc(), - Param->hasAttr(), - PT->isReferenceType(), - cast(Arg)->getExpr()); + // std::init / ref_to_uninit (paper §5): a pointer or reference argument + // must match the [[ref_to_uninit]] marking of its parameter. The + // recognizers don't see through the CXXDefaultArgExpr wrapper, so check + // the underlying default-argument expression. + if (Param && getLangOpts().Profiles) { + QualType PT = Param->getType(); + if (PT->isPointerType() || PT->isReferenceType()) { + const Expr *Src = Arg; + if (const auto *DAE = dyn_cast(Src)) + Src = DAE->getExpr(); + checkRefToUninitInit(Arg->getExprLoc(), + Param->hasAttr(), + PT->isReferenceType(), Src); } } From b0be0d52ff565f9be2727c48bc5266dcb9bce6d7 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:45:52 -0400 Subject: [PATCH 177/289] Give the ref_to_uninit binding checks a decl-target wrapper Six call sites repeated the non-dependent pointer-or-reference type test and the marker lookup; checkRefToUninitBinding now owns both, taking the marked declaration and the bound type. --- clang/include/clang/Sema/Sema.h | 12 ++++++++++++ clang/lib/Sema/SemaDecl.cpp | 21 ++++++++++++++------- clang/lib/Sema/SemaDeclCXX.cpp | 16 ++++------------ clang/lib/Sema/SemaExpr.cpp | 13 ++++--------- clang/lib/Sema/SemaInit.cpp | 21 ++++++++------------- clang/lib/Sema/SemaStmt.cpp | 8 +++----- 6 files changed, 45 insertions(+), 46 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 68b67b0cd39f9..be2b9fa6c51b4 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1177,6 +1177,18 @@ class Sema final : public SemaBase { bool IsReference, const Expr *Src, const Decl *D = nullptr); + /// std::init / ref_to_uninit (paper §5): check that binding \p Src to + /// \p Target (a variable, data member, parameter, or function) is + /// consistent with the target's [[ref_to_uninit]] marking. \p T is the + /// bound type -- the target's type, or the return type when \p Target is a + /// function. No-op unless \p T is a non-dependent pointer or reference (a + /// dependent type defers to instantiation, where the check site re-runs + /// with the concrete type). \p D, when available, is the declaration used + /// for suppression lookup and template-pattern deferral. + void checkRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, + QualType T, const Expr *Src, + const Decl *D = nullptr); + /// std::init / uninit_read (paper §4.5): diagnose a read *through* a /// [[ref_to_uninit]] pointer or reference, whose result is itself /// uninitialized. Called from Sema::DefaultLvalueConversion at the single diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 03d526e2d28d1..18a7f7f9cbd32 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -15291,6 +15291,16 @@ void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; } +void Sema::checkRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, + QualType T, const Expr *Src, + const Decl *D) { + if (!getLangOpts().Profiles || T.isNull() || T->isDependentType() || + (!T->isPointerType() && !T->isReferenceType())) + return; + checkRefToUninitInit(Loc, Target->hasAttr(), + T->isReferenceType(), Src, D); +} + void Sema::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, QualType ValueType) { // A RecoveryExpr is a placeholder for an expression that already failed, not @@ -15315,13 +15325,10 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { checkInitProfileUninitWithInitializer(var, var->getInit()); - // std::init / ref_to_uninit (paper §5): a pointer or reference variable must - // be bound consistently with its [[ref_to_uninit]] marking. A dependent type - // is deferred to instantiation, where the rule re-runs on the concrete type. - if (QualType VT = var->getType(); !VT->isDependentType() && - (VT->isPointerType() || VT->isReferenceType())) - checkRefToUninitInit(var->getLocation(), var->hasAttr(), - VT->isReferenceType(), var->getInit(), var); + // std::init / ref_to_uninit (paper §5): a pointer or reference variable + // must be bound consistently with its [[ref_to_uninit]] marking. + checkRefToUninitBinding(var->getLocation(), var, var->getType(), + var->getInit(), var); CUDA().MaybeAddConstantAttr(var); diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 51ae398649ee9..b0bfc0fe6ec59 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4236,11 +4236,8 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, // std::init / ref_to_uninit (paper §5): a pointer or reference data member // with a default member initializer must be bound consistently with its // [[ref_to_uninit]] marking. - if (QualType FT = FD->getType(); !FT->isDependentType() && - (FT->isPointerType() || FT->isReferenceType())) - checkRefToUninitInit(FD->getLocation(), FD->hasAttr(), - FT->isReferenceType(), FD->getInClassInitializer(), - FD); + checkRefToUninitBinding(FD->getLocation(), FD, FD->getType(), + FD->getInClassInitializer(), FD); } /// Find the direct and/or virtual base specifiers that @@ -4687,13 +4684,8 @@ Sema::BuildMemberInitializer(ValueDecl *Member, Expr *Init, // defers (via D->isTemplated()) and fires once at instantiation, where // BuildMemberInitializer re-runs with the instantiated constructor as // CurContext, matching ctor_uninit_member. - if (getLangOpts().Profiles) - if (QualType MT = Member->getType(); - !MT->isDependentType() && - (MT->isPointerType() || MT->isReferenceType())) - if (auto *Ctor = dyn_cast(CurContext)) - checkRefToUninitInit(IdLoc, Member->hasAttr(), - MT->isReferenceType(), Init, Ctor); + if (auto *Ctor = dyn_cast(CurContext)) + checkRefToUninitBinding(IdLoc, Member, Member->getType(), Init, Ctor); } if (DirectMember) { diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 83db6073147d2..fba589e4997ba 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6285,15 +6285,10 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, // recognizers don't see through the CXXDefaultArgExpr wrapper, so check // the underlying default-argument expression. if (Param && getLangOpts().Profiles) { - QualType PT = Param->getType(); - if (PT->isPointerType() || PT->isReferenceType()) { - const Expr *Src = Arg; - if (const auto *DAE = dyn_cast(Src)) - Src = DAE->getExpr(); - checkRefToUninitInit(Arg->getExprLoc(), - Param->hasAttr(), - PT->isReferenceType(), Src); - } + const Expr *Src = Arg; + if (const auto *DAE = dyn_cast(Src)) + Src = DAE->getExpr(); + checkRefToUninitBinding(Arg->getExprLoc(), Param, Param->getType(), Src); } // Check for array bounds violations for each argument to the call. This diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 9c06331d95e78..117ec167eabf9 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -1602,13 +1602,10 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, // aggregate is one, so deferral comes from the dependent-context // guard in checkRefToUninitInit and suppression from the parse-time // stack. (A reference field is routed to CheckReferenceType above.) - if (SemaRef.getLangOpts().Profiles && !Result.isInvalid() && - Entity.getKind() == InitializedEntity::EK_Member && - ElemType->isPointerType()) - SemaRef.checkRefToUninitInit( - expr->getExprLoc(), - Entity.getDecl()->hasAttr(), - /*IsReference=*/false, expr, /*D=*/nullptr); + if (!Result.isInvalid() && + Entity.getKind() == InitializedEntity::EK_Member) + SemaRef.checkRefToUninitBinding(expr->getExprLoc(), + Entity.getDecl(), ElemType, expr); UpdateStructuredListElement(StructuredList, StructuredIndex, Result.getAs()); @@ -1916,12 +1913,10 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, // init list can appear in a template independently of whether the aggregate // is one, so deferral comes from the dependent-context guard in // checkRefToUninitInit and suppression from the parse-time stack. - if (SemaRef.getLangOpts().Profiles && !VerifyOnly && !Result.isInvalid() && - Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent() && - DeclType->isReferenceType()) - SemaRef.checkRefToUninitInit(Src->getExprLoc(), - Entity.getDecl()->hasAttr(), - /*IsReference=*/true, Src, /*D=*/nullptr); + if (!VerifyOnly && !Result.isInvalid() && + Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent()) + SemaRef.checkRefToUninitBinding(Src->getExprLoc(), Entity.getDecl(), + DeclType, Src); UpdateStructuredListElement(StructuredList, StructuredIndex, expr); ++Index; diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp index dd00211f43d38..eaeb6fb0d7645 100644 --- a/clang/lib/Sema/SemaStmt.cpp +++ b/clang/lib/Sema/SemaStmt.cpp @@ -4118,12 +4118,10 @@ StmtResult Sema::BuildReturnStmt(SourceLocation ReturnLoc, Expr *RetValExp, } // std::init / ref_to_uninit (paper §5): the returned pointer or reference // must match the function's [[ref_to_uninit]] return marking. - if (RetValExp && getLangOpts().Profiles && !FnRetType->isDependentType() && - (FnRetType->isPointerType() || FnRetType->isReferenceType())) + if (RetValExp && getLangOpts().Profiles) if (const FunctionDecl *FD = getCurFunctionDecl()) - checkRefToUninitInit(RetValExp->getExprLoc(), - FD->hasAttr(), - FnRetType->isReferenceType(), RetValExp); + checkRefToUninitBinding(RetValExp->getExprLoc(), FD, FnRetType, + RetValExp); const VarDecl *NRVOCandidate = getCopyElisionCandidate(NRInfo, FnRetType); From 670ef80c91f371397be2b84e44b48c8c64331fa1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:46:25 -0400 Subject: [PATCH 178/289] Funnel ProfileSuppressScope pushes through one method All three constructors and addFromDecl inlined the push_back-and-count pattern; a private push() now owns it. --- clang/include/clang/Sema/Sema.h | 1 + clang/lib/Sema/Sema.cpp | 29 +++++++++++++---------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index be2b9fa6c51b4..fb22ed602d35f 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1210,6 +1210,7 @@ class Sema final : public SemaBase { Sema &S; unsigned Count = 0; + void push(StringRef ProfileName, StringRef RuleName); void addFromDecl(const Decl *D); public: diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index 7b08ac95a5e70..faae92c2f7866 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3223,6 +3223,12 @@ bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, return true; } +void Sema::ProfileSuppressScope::push(StringRef ProfileName, + StringRef RuleName) { + S.ProfileSuppressStack.push_back({ProfileName, RuleName}); + ++Count; +} + Sema::ProfileSuppressScope::ProfileSuppressScope( Sema &S, const ParsedAttributesView &Attrs) : S(S) { @@ -3232,19 +3238,14 @@ Sema::ProfileSuppressScope::ProfileSuppressScope( if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) continue; const auto &Args = AL.getProfileSuppressArgs(); - if (!Args.Name.empty()) { - S.ProfileSuppressStack.push_back({Args.Name, Args.Rule}); - ++Count; - } + if (!Args.Name.empty()) + push(Args.Name, Args.Rule); } } void Sema::ProfileSuppressScope::addFromDecl(const Decl *D) { - for (const auto *A : D->specific_attrs()) { - S.ProfileSuppressStack.push_back( - {A->getProfileName(), A->getRule()}); - ++Count; - } + for (const auto *A : D->specific_attrs()) + push(A->getProfileName(), A->getRule()); } Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, @@ -3266,13 +3267,9 @@ Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, : S(S) { if (!S.getLangOpts().Profiles) return; - for (const auto *A : Attrs) { - if (const auto *PSA = dyn_cast(A)) { - S.ProfileSuppressStack.push_back( - {PSA->getProfileName(), PSA->getRule()}); - ++Count; - } - } + for (const auto *A : Attrs) + if (const auto *PSA = dyn_cast(A)) + push(PSA->getProfileName(), PSA->getRule()); } Sema::ProfileSuppressScope::~ProfileSuppressScope() { From fdca516fa1abd9fcfdb88281812d6f31c82c6794 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:47:42 -0400 Subject: [PATCH 179/289] Encapsulate the profile-argument parallel-array plumbing A shared unzip helper now feeds the key/value/kind arrays the semantic attributes store, and makeImplicitProfilesSuppressAttr replaces the 8-nullptr CreateImplicit call in the lambda propagation. Stored data is unchanged. --- clang/include/clang/Sema/Sema.h | 6 ++++++ clang/lib/Sema/Sema.cpp | 38 ++++++++++++++++++++++++--------- clang/lib/Sema/SemaLambda.cpp | 12 +++-------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index fb22ed602d35f..5183192a792d4 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -1086,6 +1086,12 @@ class Sema final : public SemaBase { ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); + /// Create an implicit ProfilesSuppressAttr carrying just a profile and rule + /// name (no justification or arguments), for propagating an active + /// suppression onto a declaration. + ProfilesSuppressAttr *makeImplicitProfilesSuppressAttr(StringRef ProfileName, + StringRef RuleName); + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName = "") const; bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index faae92c2f7866..fdfadda5dea67 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -3018,6 +3018,19 @@ bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, return true; } +// Unzip profile arguments into the parallel key/value/kind arrays that the +// semantic attributes store (Attr.td cannot hold structured arguments). +static void unzipProfileArguments(ArrayRef Arguments, + SmallVectorImpl &Keys, + SmallVectorImpl &Values, + SmallVectorImpl &Kinds) { + for (const auto &Arg : Arguments) { + Keys.push_back(Arg.Key); + Values.push_back(Arg.Value); + Kinds.push_back(static_cast(Arg.Kind)); + } +} + static void appendProfileArgumentData( ArrayRef Arguments, SmallVectorImpl *ArgumentCounts, @@ -3029,11 +3042,8 @@ static void appendProfileArgumentData( assert(ArgumentKeys && ArgumentValues && ArgumentKinds); ArgumentCounts->push_back(Arguments.size()); - for (const auto &Arg : Arguments) { - ArgumentKeys->push_back(Arg.Key); - ArgumentValues->push_back(Arg.Value); - ArgumentKinds->push_back(static_cast(Arg.Kind)); - } + unzipProfileArguments(Arguments, *ArgumentKeys, *ArgumentValues, + *ArgumentKinds); } bool Sema::processProfilesEnforceAttr( @@ -3087,11 +3097,8 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { SmallVector RawArgumentKeys; SmallVector RawArgumentValues; SmallVector RawArgumentKinds; - for (const auto &Arg : Args.Arguments) { - RawArgumentKeys.push_back(Arg.Key); - RawArgumentValues.push_back(Arg.Value); - RawArgumentKinds.push_back(static_cast(Arg.Kind)); - } + unzipProfileArguments(Args.Arguments, RawArgumentKeys, RawArgumentValues, + RawArgumentKinds); return ::new (Context) ProfilesSuppressAttr( Context, AL, Args.Name, Args.Justification, Args.Rule, @@ -3101,6 +3108,17 @@ ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { RawArgumentKinds.size()); } +ProfilesSuppressAttr * +Sema::makeImplicitProfilesSuppressAttr(StringRef ProfileName, + StringRef RuleName) { + return ProfilesSuppressAttr::CreateImplicit( + Context, ProfileName, /*Justification=*/"", RuleName, + /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0, + /*RawArgumentKeys=*/nullptr, /*RawArgumentKeysSize=*/0, + /*RawArgumentValues=*/nullptr, /*RawArgumentValuesSize=*/0, + /*RawArgumentKinds=*/nullptr, /*RawArgumentKindsSize=*/0); +} + static bool profileSuppressMatches(StringRef EntryProfile, StringRef EntryRule, StringRef Profile, StringRef Rule) { return EntryProfile == Profile && diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index 2791dce4273a3..803c14b78dc61 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -16,7 +16,6 @@ #include "clang/AST/ExprCXX.h" #include "clang/AST/MangleNumberingContext.h" #include "clang/Basic/TargetInfo.h" -#include "clang/Sema/Attr.h" #include "clang/Sema/DeclSpec.h" #include "clang/Sema/Initialization.h" #include "clang/Sema/Lookup.h" @@ -1506,15 +1505,10 @@ void Sema::ActOnStartOfLambdaDefinition(LambdaIntroducer &Intro, // P3589R2: Propagate active profile suppressions to the call operator so // that generic lambda instantiation (which walks lexical Decl parents, not // the enclosing stmt tree) can recover them. - if (getLangOpts().Profiles) { + if (getLangOpts().Profiles) for (const auto &E : ProfileSuppressStack) - Method->addAttr(ProfilesSuppressAttr::CreateImplicit( - Context, E.ProfileName, /*Justification=*/"", E.RuleName, - /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0, - /*RawArgumentKeys=*/nullptr, /*RawArgumentKeysSize=*/0, - /*RawArgumentValues=*/nullptr, /*RawArgumentValuesSize=*/0, - /*RawArgumentKinds=*/nullptr, /*RawArgumentKindsSize=*/0)); - } + Method->addAttr( + makeImplicitProfilesSuppressAttr(E.ProfileName, E.RuleName)); if (Context.getTargetInfo().getTriple().isAArch64()) ARM().CheckSMEFunctionDefAttributes(Method); From a383c9c6360ada7c370b87a2b09b4b4093e8697c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 12:50:17 -0400 Subject: [PATCH 180/289] Share the CFG configuration between the main and post-error passes The post-error profile rerun hand-copied the main pass's base CFG build options and non-linearized always-add classes, which would silently diverge if the main pass changed; both now call the same helpers. The duplicated std::init ctor-body dispatch pairing gets the same treatment, and the docs pick up the hasEnforcedCFGUninitProfile spelling. --- clang/docs/ProfilesFramework.rst | 5 +- clang/lib/Sema/AnalysisBasedWarnings.cpp | 73 +++++++++++++----------- 2 files changed, 42 insertions(+), 36 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 3855a3f311b2c..1a9704c187109 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -227,7 +227,8 @@ framework intentionally does not learn the profile name). silenced, OR an ``S.anyProfileEnforced(Table)`` check (the shared ``Sema::anyProfileEnforced`` gate, also used by the finalization dispatch) into the existing pass guard. The in-tree example's pass guard becomes - ``S.anyProfileEnforced(CFGUninitProfiles) || !Diags.isIgnored(...)``. + ``hasEnforcedCFGUninitProfile() || !Diags.isIgnored(...)`` (a small + accessor over ``S.anyProfileEnforced(CFGUninitProfiles)``). 3. **Walk the table in the analysis's diagnostic reporter.** For each use site the analysis would have warned about, iterate the @@ -621,7 +622,7 @@ CFG-based uninitialized-variables analysis. declaration. - **Opt-in table**: ``CFGUninitProfiles`` in ``clang/lib/Sema/AnalysisBasedWarnings.cpp``. The ``IssueWarnings`` pass - guard consults it via ``S.anyProfileEnforced(CFGUninitProfiles)`` so the analysis + guard consults it via ``hasEnforcedCFGUninitProfile()`` so the analysis runs even when ``-Wuninitialized`` is silenced, and ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the default warning path -- when an entry's diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index ac5ec6e126eca..e7c8cab3d9574 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -3118,16 +3118,12 @@ void sema::AnalysisBasedWarnings::issueWarningsForRegisteredVarDecl( S, AC, std::make_pair(SecondRange.begin(), SecondRange.end())); } -// Pattern-2 profiles (the CFGUninitProfiles table) ride the uninitialized- -// variables analysis, which IssueWarnings otherwise skips once the TU has an -// uncompilable error. Re-run just that analysis for a single function so an -// early TU error does not silently disable the profile for every later -// function. Diagnostics are restricted to the profile via the ProfileOnly -// reporter, and the CFG build options mirror the non-linearized configuration -// of the main pass so the analysis sees the same CFG. -static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { - AnalysisDeclContext AC(/*Mgr=*/nullptr, D); - +// Base CFG build options shared by the main per-function analysis pass +// (IssueWarnings) and the post-error profile rerun below, so both see the +// same CFG shape. +static void configureBaseCFGBuildOptions(AnalysisDeclContext &AC) { + // Don't generate EH edges for CallExprs as we'd like to avoid the n^2 + // explosion for destructors that can result and the compile time hit. AC.getCFGBuildOptions().PruneTriviallyFalseEdges = true; AC.getCFGBuildOptions().AddEHEdges = false; AC.getCFGBuildOptions().AddInitializers = true; @@ -3136,6 +3132,11 @@ static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { AC.getCFGBuildOptions().AddTemporaryDtors = true; AC.getCFGBuildOptions().AddCXXNewAllocator = false; AC.getCFGBuildOptions().AddCXXDefaultInitExprInCtors = true; +} + +// The always-add statement classes of the main pass's non-linearized CFG +// configuration; shared with the post-error profile rerun. +static void addNonLinearizedAlwaysAddClasses(AnalysisDeclContext &AC) { AC.getCFGBuildOptions() .setAlwaysAdd(Stmt::BinaryOperatorClass) .setAlwaysAdd(Stmt::CompoundAssignOperatorClass) @@ -3144,6 +3145,30 @@ static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { .setAlwaysAdd(Stmt::DeclRefExprClass) .setAlwaysAdd(Stmt::ImplicitCastExprClass) .setAlwaysAdd(Stmt::UnaryOperatorClass); +} + +// std::init: diagnose a read of a [[uninit]] scalar member before it is +// assigned in the constructor body. Shared by the normal per-function pass +// and the post-error rerun so both paths stay in step. +static void runCtorBodyInitCheckIfEnforced(Sema &S, const Decl *D, + AnalysisDeclContext &AC) { + if (!S.isProfileEnforced("std::init")) + return; + if (const auto *Ctor = dyn_cast(D)) + checkInitProfileCtorBody(S, Ctor, AC); +} + +// Pattern-2 profiles (the CFGUninitProfiles table) ride the uninitialized- +// variables analysis, which IssueWarnings otherwise skips once the TU has an +// uncompilable error. Re-run just that analysis for a single function so an +// early TU error does not silently disable the profile for every later +// function. Diagnostics are restricted to the profile via the ProfileOnly +// reporter. +static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { + AnalysisDeclContext AC(/*Mgr=*/nullptr, D); + + configureBaseCFGBuildOptions(AC); + addNonLinearizedAlwaysAddClasses(AC); if (CFG *cfg = AC.getCFG()) { UninitValsDiagReporter reporter(S, AC, /*ProfileOnly=*/true); @@ -3152,9 +3177,7 @@ static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { stats); // Keep the constructor-body read-before-init check alive after a TU error, // for parity with the normal path. - if (S.isProfileEnforced("std::init")) - if (const auto *Ctor = dyn_cast(D)) - checkInitProfileCtorBody(S, Ctor, AC); + runCtorBodyInitCheckIfEnforced(S, D, AC); } } @@ -3539,16 +3562,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( // Construct the analysis context with the specified CFG build options. AnalysisDeclContext AC(/* AnalysisDeclContextManager */ nullptr, D); - // Don't generate EH edges for CallExprs as we'd like to avoid the n^2 - // explosion for destructors that can result and the compile time hit. - AC.getCFGBuildOptions().PruneTriviallyFalseEdges = true; - AC.getCFGBuildOptions().AddEHEdges = false; - AC.getCFGBuildOptions().AddInitializers = true; - AC.getCFGBuildOptions().AddImplicitDtors = true; - AC.getCFGBuildOptions().AddParameterLifetimes = true; - AC.getCFGBuildOptions().AddTemporaryDtors = true; - AC.getCFGBuildOptions().AddCXXNewAllocator = false; - AC.getCFGBuildOptions().AddCXXDefaultInitExprInCtors = true; + configureBaseCFGBuildOptions(AC); bool IsLifetimeSafetyDiagnosticEnabled = !Diags.isIgnored(diag::warn_lifetime_safety_use_after_scope, @@ -3579,14 +3593,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( // Unreachable code analysis and thread safety require a linearized CFG. AC.getCFGBuildOptions().setAllAlwaysAdd(); } else { - AC.getCFGBuildOptions() - .setAlwaysAdd(Stmt::BinaryOperatorClass) - .setAlwaysAdd(Stmt::CompoundAssignOperatorClass) - .setAlwaysAdd(Stmt::BlockExprClass) - .setAlwaysAdd(Stmt::CStyleCastExprClass) - .setAlwaysAdd(Stmt::DeclRefExprClass) - .setAlwaysAdd(Stmt::ImplicitCastExprClass) - .setAlwaysAdd(Stmt::UnaryOperatorClass); + addNonLinearizedAlwaysAddClasses(AC); } if (EnableLifetimeSafetyAnalysis) AC.getCFGBuildOptions().AddLifetime = true; @@ -3680,9 +3687,7 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( // std::init: diagnose a read of a [[uninit]] scalar member before it is // assigned in the constructor body, reusing the CFG built above. - if (S.isProfileEnforced("std::init")) - if (const auto *Ctor = dyn_cast(D)) - checkInitProfileCtorBody(S, Ctor, AC); + runCtorBodyInitCheckIfEnforced(S, D, AC); // TODO: Enable lifetime safety analysis for other languages once it is // stable. From d063d4c7be6c2ff892551827f574ae0ff5cdc430 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 13:18:13 -0400 Subject: [PATCH 181/289] Move the profiles framework into a SemaProfiles subsystem The framework state and checks were spread across Sema.h, Sema.cpp, SemaDecl.cpp, and SemaDeclCXX.cpp. Collect them in a SemaBase subsystem reached via S.Profiles(), following SemaOpenACC and friends: enforcement and suppression state, the violation gate, the std::init rule checks, the uninit-storage recognizers, and the finalization dispatch tables now live in SemaProfiles.{h,cpp}. Mechanical move; no behavior change. --- clang/docs/ProfilesFramework.rst | 50 +- clang/include/clang/Sema/Sema.h | 217 +--- clang/include/clang/Sema/SemaProfiles.h | 246 ++++ clang/lib/Parse/ParseCXXInlineMethods.cpp | 5 +- clang/lib/Parse/ParseDecl.cpp | 5 +- clang/lib/Parse/ParseDeclCXX.cpp | 9 +- clang/lib/Parse/ParseExprCXX.cpp | 3 +- clang/lib/Parse/ParseStmt.cpp | 3 +- clang/lib/Sema/AnalysisBasedWarnings.cpp | 11 +- clang/lib/Sema/CMakeLists.txt | 1 + clang/lib/Sema/Sema.cpp | 314 +---- clang/lib/Sema/SemaCast.cpp | 4 +- clang/lib/Sema/SemaDecl.cpp | 512 +------- clang/lib/Sema/SemaDeclAttr.cpp | 7 +- clang/lib/Sema/SemaDeclCXX.cpp | 181 +-- clang/lib/Sema/SemaExpr.cpp | 11 +- clang/lib/Sema/SemaInit.cpp | 7 +- clang/lib/Sema/SemaLambda.cpp | 6 +- clang/lib/Sema/SemaModule.cpp | 9 +- clang/lib/Sema/SemaProfiles.cpp | 1027 +++++++++++++++++ clang/lib/Sema/SemaStmt.cpp | 3 +- clang/lib/Sema/SemaStmtAttr.cpp | 3 +- clang/lib/Sema/SemaTemplateInstantiate.cpp | 3 +- .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 9 +- clang/lib/Sema/TreeTransform.h | 6 +- clang/lib/Serialization/ASTReader.cpp | 3 +- clang/lib/Serialization/ASTWriter.cpp | 5 +- 27 files changed, 1397 insertions(+), 1263 deletions(-) create mode 100644 clang/include/clang/Sema/SemaProfiles.h create mode 100644 clang/lib/Sema/SemaProfiles.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 1a9704c187109..471632c3823bb 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -28,8 +28,8 @@ violation. The framework is profile-agnostic. It handles attribute parsing, enforcement tracking, suppression scoping, module integration, and serialization. Individual profiles only need to call a single API at the appropriate semantic -check sites (``Sema::checkProfileViolation`` for parse-time checks, or -``Sema::shouldEmitProfileViolation`` from a per-pass dispatch table for +check sites (``SemaProfiles::checkProfileViolation`` for parse-time checks, or +``SemaProfiles::shouldEmitProfileViolation`` from a per-pass dispatch table for post-parse analyses). Everything else -- suppression, template instantiation, SFINAE exclusion, module propagation, and PCH/BMI serialization -- is handled by the framework automatically. @@ -65,7 +65,7 @@ flag, which sets ``LangOpts.ProfilesTestProfiles``. This flag is ``-cc1``-only (not exposed by the driver) and is intended solely for running the test suite. Under ``-fprofiles`` alone, ``[[profiles::enforce(test::...)]]`` is still recognized (it is not ``warn_attribute_ignored``) and its designator is still -recorded and exported across modules, but ``Sema::isProfileEnforced`` reports +recorded and exported across modules, but ``SemaProfiles::isProfileEnforced`` reports any ``test::``-prefixed profile as not enforced, so no ``test::`` rule ever fires. Real profiles such as ``std::init`` are unaffected by this flag. @@ -160,7 +160,7 @@ Used when the rule can be checked at a single, well-defined parse-time site in Sema (typically inside a ``Sema::Build*`` or ``Sema::Act*`` routine). ``test::type_cast`` is the in-tree example. -At each such site, call ``Sema::checkProfileViolation``: +At each such site, call ``SemaProfiles::checkProfileViolation``: .. code-block:: c++ @@ -225,7 +225,7 @@ framework intentionally does not learn the profile name). normally run only when their corresponding warning flag is enabled. To run the pass for an enforced profile even when the underlying warning is silenced, OR an ``S.anyProfileEnforced(Table)`` check (the shared - ``Sema::anyProfileEnforced`` gate, also used by the finalization dispatch) + ``SemaProfiles::anyProfileEnforced`` gate, also used by the finalization dispatch) into the existing pass guard. The in-tree example's pass guard becomes ``hasEnforcedCFGUninitProfile() || !Diags.isIgnored(...)`` (a small accessor over ``S.anyProfileEnforced(CFGUninitProfiles)``). @@ -233,7 +233,7 @@ framework intentionally does not learn the profile name). 3. **Walk the table in the analysis's diagnostic reporter.** For each use site the analysis would have warned about, iterate the table and call - ``Sema::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``, + ``SemaProfiles::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``, which walks parent statements and lexical declaration contexts to honor ``[[profiles::suppress]]`` on enclosing AST nodes (the post-parse counterpart to ``ProfileSuppressStack``). Emit the entry's diagnostic @@ -276,7 +276,7 @@ the class definition is complete -- for example, "every non-private field of this class must satisfy property X" or "this class's field set must look like Y." ``test::class_final`` is the in-tree example. -The dispatch point is ``Sema::checkProfileViolationsAtClassFinalization``, +The dispatch point is ``SemaProfiles::checkProfileViolationsAtClassFinalization``, called from the end of ``Sema::CheckCompletedCXXClass`` in ``clang/lib/Sema/SemaDeclCXX.cpp``. ``CheckCompletedCXXClass`` is the single function reached from every class-completion path -- the parser @@ -297,7 +297,7 @@ are not meant to see: This pattern needs two pieces, both colocated with the dispatcher. 1. **Add the profile to the class-finalization opt-in table.** - ``ClassFinalizationProfiles`` in ``clang/lib/Sema/SemaDeclCXX.cpp`` is a + ``ClassFinalizationProfiles`` in ``clang/lib/Sema/SemaProfiles.cpp`` is a small per-pass table of profile name plus callback. One row per profile: .. code-block:: c++ @@ -329,7 +329,7 @@ This pattern needs two pieces, both colocated with the dispatcher. resolved only from the declaration and its lexical parents. 2. **Emit diagnostics from the callback via** - ``Sema::shouldEmitProfileViolation``. Each callback decides where on + ``SemaProfiles::shouldEmitProfileViolation``. Each callback decides where on the class to point and which diagnostic to use, possibly with notes: .. code-block:: c++ @@ -366,7 +366,7 @@ constructor's complete member-initializer list -- for example, "every member must be initialized by this constructor." ``test::ctor_final`` is the in-tree example, and the ``std::init`` ``ctor_uninit_member`` rule is the real one. -The dispatch point is ``Sema::checkProfileViolationsAtConstructorFinalization``, +The dispatch point is ``SemaProfiles::checkProfileViolationsAtConstructorFinalization``, called right after ``DiagnoseUninitializedFields`` in ``Sema::ActOnMemInitializers`` and ``Sema::ActOnDefaultCtorInitializers`` in ``clang/lib/Sema/SemaDeclCXX.cpp``. Those two functions are the funnel for @@ -390,7 +390,7 @@ The two pieces mirror pattern 3 and share its machinery: a per-pass opt-in table ``ConstructorFinalizationProfiles`` of the same ``FinalizationProfile`` row (here ``FinalizationProfile``), and a callback that emits via -``Sema::shouldEmitProfileViolation``. The same shared +``SemaProfiles::shouldEmitProfileViolation``. The same shared ``dispatchFinalizationProfiles`` dispatcher invokes each callback, which passes the ``CXXConstructorDecl`` to the decl-aware ``shouldEmitProfileViolation`` overload; that overload walks the declaration @@ -526,7 +526,7 @@ Serialization The framework serializes enforcement state automatically. Profile implementers do not need to add any serialization code. -- **PCH**: ``Sema::EnforcedProfiles`` is written as ``ENFORCED_PROFILES`` +- **PCH**: ``SemaProfiles::EnforcedProfiles`` is written as ``ENFORCED_PROFILES`` records in the AST bitstream and restored when the PCH is loaded. - **Module BMI**: ``Module::EnforcedProfileDesignators`` is written as ``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. @@ -575,7 +575,7 @@ By convention: - Real test profiles live under the ``test::`` namespace. Today there are four: ``test::type_cast``, ``test::uninit_read``, ``test::class_final``, and ``test::ctor_final``. Because the ``test::`` prefix is what - ``Sema::isProfileEnforced`` keys on to gate them behind + ``SemaProfiles::isProfileEnforced`` keys on to gate them behind ``-fprofiles-test-profiles``, any new test-only profile must also live under ``test::``. - The names ``test::other``, ``test::bounds``, ``test::new_profile``, and @@ -626,7 +626,7 @@ CFG-based uninitialized-variables analysis. runs even when ``-Wuninitialized`` is silenced, and ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the default warning path -- when an entry's - ``Sema::shouldEmitProfileViolation`` returns true the entry's diagnostic + ``SemaProfiles::shouldEmitProfileViolation`` returns true the entry's diagnostic fires and the default warning is skipped entirely. The Stmt-tree suppression walker is what makes ``[[profiles::suppress]]`` @@ -647,7 +647,7 @@ class-finalization dispatch in ``Sema::CheckCompletedCXXClass``. - **Diagnostic**: ``err_profile_class_final_test`` ("test profile fired on completion of class %1 under profile '%0'"). - **Opt-in table**: ``ClassFinalizationProfiles`` in - ``clang/lib/Sema/SemaDeclCXX.cpp``. + ``clang/lib/Sema/SemaProfiles.cpp``. Because dependent classes are filtered out by the dispatcher, the diagnostic fires on class template *instantiations* rather than on the @@ -669,7 +669,7 @@ member-initializer list is complete. - **Diagnostic**: ``err_profile_ctor_final_test`` ("test profile fired on finalization of a constructor for class %1 under profile '%0'"). - **Opt-in table**: ``ConstructorFinalizationProfiles`` in - ``clang/lib/Sema/SemaDeclCXX.cpp``. + ``clang/lib/Sema/SemaProfiles.cpp``. The diagnostic fires once per user-defined constructor -- written or implicit member-initializer list, inline or out-of-line -- and on constructor template @@ -846,7 +846,7 @@ variable is instead rejected by ``static_marker`` (R9)). with no initializer (so braced or value initialization such as ``S s = {1};`` and ``S s{};`` is unaffected -- omitted aggregate members are value-initialized). -- The aggregate case uses ``Sema::defaultInitLeavesScalarIndeterminate`` +- The aggregate case uses ``SemaProfiles::defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=true``, which recurses through bases and members, trusts user-provided default constructors, and skips data members marked ``[[uninit]]`` (acknowledged uninitialized, paper §5.3). So a type @@ -875,7 +875,7 @@ R4. ``uninit_with_initializer`` -- pattern 1 contradiction (the marker means "no initialization here"). - Diagnostic: ``err_init_uninit_with_initializer``. -- Check site: ``Sema::checkInitProfileUninitWithInitializer``, shared by +- Check site: ``SemaProfiles::checkInitProfileUninitWithInitializer``, shared by ``Sema::CheckCompleteVariableDeclaration`` (variables) and ``Sema::ActOnFinishCXXInClassMemberInitializer`` (data members with a default member initializer). @@ -955,7 +955,7 @@ assignment when compiled without the profile. [[profiles::suppress(std::init)]] U e [[uninit]]; // OK - Diagnostic: ``err_init_union_marker``. -- Check site: the shared helper ``Sema::diagnoseInitUninitMarkerPlacement``, +- Check site: the shared helper ``SemaProfiles::diagnoseInitUninitMarkerPlacement``, called from the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp`` and re-run on the instantiated entity from ``VisitFieldDecl`` / ``VisitVarDecl`` in ``clang/lib/Sema/SemaTemplateInstantiateDecl.cpp``. Unlike the reference / @@ -969,7 +969,7 @@ assignment when compiled without the profile. acknowledged and do not emit a second, contradictory diagnostic. An *unmarked* union left uninitialized is itself the error (paper §5.6): -``Sema::defaultInitLeavesScalarIndeterminate`` reports a union as indeterminate +``SemaProfiles::defaultInitLeavesScalarIndeterminate`` reports a union as indeterminate unless it has no members, a user-provided default constructor, or a default member initializer. A uninitialized union variable is therefore diagnosed by ``uninit_decl`` (with the union-specific ``err_init_uninit_union``) and an @@ -1006,13 +1006,13 @@ recognizers symmetric. - Diagnostics: ``err_init_ref_to_uninit_requires_uninit`` (marked target, initialized source) and ``err_init_uninit_requires_ref_to_uninit`` (unmarked target, uninitialized source). -- Recognizer + shared check: ``Sema::refersToUninitializedMemory`` and - ``Sema::checkRefToUninitInit`` in ``clang/lib/Sema/SemaDecl.cpp``. +- Recognizer + shared check: ``SemaProfiles::refersToUninitializedMemory`` and + ``SemaProfiles::checkRefToUninitInit`` in ``clang/lib/Sema/SemaProfiles.cpp``. - A ``new`` expression is recognised only when it default-initializes its object (none init style, no written initializer); ``new T(...)`` and ``new T{...}`` are value- or list-initialized and excluded. Whether the allocated type leaves a scalar indeterminate reuses - ``Sema::defaultInitLeavesScalarIndeterminate`` (R2), so a ``T`` with a + ``SemaProfiles::defaultInitLeavesScalarIndeterminate`` (R2), so a ``T`` with a user-provided default constructor is trusted and the ``std::byte`` exemption is inherited. ``getAllocatedType`` yields the element type, so array ``new`` (``new int[n]``) is handled uniformly. @@ -1044,7 +1044,7 @@ recognizers symmetric. - Read-through enforcement (paper §4.5): a scalar *read* through a ``[[ref_to_uninit]]`` pointer or reference loads an uninitialized value and is diagnosed at the single lvalue-to-rvalue chokepoint - (``Sema::DefaultLvalueConversion`` calling ``Sema::checkRefToUninitRead``), + (``Sema::DefaultLvalueConversion`` calling ``SemaProfiles::checkRefToUninitRead``), which by-value reads -- copy-initialization, by-value arguments, returns, and operator/condition operands -- all funnel through. It reuses the recognizer in a read-only mode (a ``ForRead`` flag that drops the directly-named @@ -1098,7 +1098,7 @@ A pointer must instead be initialized (e.g. to ``nullptr``). [[profiles::suppress(std::init, rule: "pointer_marker")]] int *x [[uninit]]; // OK - Diagnostic: ``err_init_uninit_pointer_marker``. -- Check site: ``Sema::diagnoseInitUninitMarkerPlacement``, alongside +- Check site: ``SemaProfiles::diagnoseInitUninitMarkerPlacement``, alongside ``union_marker`` (see R6 for the shared parse-time handler and the re-check on the instantiated field / variable). Like that rule it is gated on enforcement -- a pointer may legitimately carry the marker without the profile diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h index 5183192a792d4..a7ccdb63df678 100644 --- a/clang/include/clang/Sema/Sema.h +++ b/clang/include/clang/Sema/Sema.h @@ -49,7 +49,6 @@ #include "clang/Basic/OperatorKinds.h" #include "clang/Basic/PartialDiagnostic.h" #include "clang/Basic/PragmaKinds.h" -#include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Basic/Specifiers.h" #include "clang/Basic/StackExhaustionHandler.h" @@ -128,7 +127,6 @@ class ASTWriter; class CXXBasePath; class CXXBasePaths; class CXXFieldCollector; -class AnalysisDeclContext; class CodeCompleteConsumer; enum class ComparisonCategoryType : unsigned char; class ConstraintSatisfaction; @@ -159,7 +157,6 @@ enum class OverloadCandidateParamOrder : char; enum OverloadCandidateRewriteKind : unsigned; class OverloadCandidateSet; class Preprocessor; -class ProfilesSuppressAttr; class SemaAMDGPU; class SemaARM; class SemaAVR; @@ -179,6 +176,7 @@ class SemaOpenACC; class SemaOpenCL; class SemaOpenMP; class SemaPPC; +class SemaProfiles; class SemaPseudoObject; class SemaRISCV; class SemaSPIRV; @@ -1037,196 +1035,6 @@ class Sema final : public SemaBase { void ActOnEndOfTranslationUnit(); void ActOnEndOfTranslationUnitFragment(TUFragmentKind Kind); - // C++ Profiles framework (P3589R2) - - struct ProfileEnforcement : profiles::EnforcedProfile { - SourceLocation EnforceLoc; - }; - SmallVector EnforcedProfiles; - - struct ProfileSuppressEntry { - StringRef ProfileName; - StringRef RuleName; - }; - SmallVector ProfileSuppressStack; - - /// True while a class/constructor finalization profile callback runs. - /// Finalization can fire as a side effect of instantiating an unrelated - /// entity whose ProfileSuppressScope is still on ProfileSuppressStack, so - /// during finalization that transient stack is ignored and suppression is - /// resolved only from the finalized declaration and its lexical parents - /// (token-based dominion, P3589R2 s2.4p3). - bool InProfileFinalizationCheck = false; - - bool isProfileEnforced(StringRef ProfileName) const; - - /// True if any entry of \p Entries names an enforced profile. \p Entries is - /// any profile opt-in table whose elements expose a \c Name member; shared by - /// the post-parse dispatch gates (the CFG analysis pass guard and the - /// finalization dispatcher). - template bool anyProfileEnforced(const Table &Entries) const { - return llvm::any_of( - Entries, [&](const auto &E) { return isProfileEnforced(E.Name); }); - } - - const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; - bool addProfileEnforcement(StringRef Name, StringRef Designator, - SourceLocation Loc); - bool processProfilesEnforceAttr(const ParsedAttr &AL, Module *Mod, - SmallVectorImpl *NewNames, - SmallVectorImpl *NewDesignators, - SmallVectorImpl *NewArgumentCounts = - nullptr, - SmallVectorImpl *NewArgumentKeys = - nullptr, - SmallVectorImpl *NewArgumentValues = - nullptr, - SmallVectorImpl *NewArgumentKinds = - nullptr); - - ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); - - /// Create an implicit ProfilesSuppressAttr carrying just a profile and rule - /// name (no justification or arguments), for propagating an active - /// suppression onto a declaration. - ProfilesSuppressAttr *makeImplicitProfilesSuppressAttr(StringRef ProfileName, - StringRef RuleName); - - bool isProfileSuppressed(StringRef ProfileName, - StringRef RuleName = "") const; - bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, - const Decl *D) const; - bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, - const Stmt *S, AnalysisDeclContext &AC) const; - bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc); - bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc, const Decl *D); - bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, - const Stmt *UseStmt, - AnalysisDeclContext &AC) const; - bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc, unsigned DiagID); - - /// std::init / uninit_decl (R2, paper §4.2): diagnose an automatic variable - /// definition that leaves the object (or a scalar subobject) indeterminate - /// without an acknowledging [[uninit]] marker. Called from - /// \c ActOnUninitializedDecl after default-initialization is attempted. - void checkInitProfileUninitDecl(const VarDecl *Var); - - /// std::init / static_marker (paper §3, §4.2): diagnose [[uninit]] on a - /// static or thread-storage variable, which is zero-initialized by language - /// rule and therefore an initialized object. Called from - /// \c ActOnUninitializedDecl. - void checkInitProfileStaticMarker(const VarDecl *Var); - - /// std::init / static_runtime_init (paper §3): diagnose a non-local static - /// whose initialization needs a runtime constructor. \p CheckConstInit - /// lazily evaluates whether the initializer is constant (trivial - /// default-init counts as constant here). Returns true if the diagnostic - /// was emitted, in which case the caller skips -Wglobal-constructors. - bool - checkInitProfileStaticRuntimeInit(const VarDecl *Var, - llvm::function_ref CheckConstInit); - - /// std::init / uninit_with_initializer (R4): diagnose \p D if it is both - /// marked [[uninit]] and has an initializer. Shared by the variable - /// (\c CheckCompleteVariableDeclaration) and non-static data member - /// (\c ActOnFinishCXXInClassMemberInitializer) paths. \p Init is the - /// (possibly null) initializer; a RecoveryExpr placeholder for a failed - /// initialization does not count as a user-written initializer. - void checkInitProfileUninitWithInitializer(const ValueDecl *D, - const Expr *Init); - - /// True if default-initialization of \p T would leave at least one scalar - /// subobject with an indeterminate value. Shared by the std::init rules - /// uninit_decl (at the variable declaration), ctor_uninit_member (for a - /// class-typed member), and uninit_with_initializer. A class with a - /// user-provided default constructor is trusted (that constructor is checked - /// at its own definition). Dependent and incomplete types are treated as - /// determinate. - /// - /// When \p HonorUninitMarkers is true, a data member marked [[uninit]] - /// is treated as acknowledged and skipped, so a type whose only indeterminate - /// scalars are all marked is reported as determinate. uninit_decl and - /// ctor_uninit_member pass true (the marker excuses the member, paper §6.2); - /// uninit_with_initializer passes false because it needs the factual answer - /// (whether the default-initialization is genuinely a no-op). - bool defaultInitLeavesScalarIndeterminate(QualType T, - bool HonorUninitMarkers = false); - - /// If \p E (stripped of parens and implicit casts) directly names a - /// declaration -- a DeclRefExpr or a MemberExpr -- return that declaration; - /// otherwise null. The std::init checks read [[ref_to_uninit]] / - /// [[uninit]] markers only off a directly named entity. - static const ValueDecl *getDirectlyNamedDecl(const Expr *E); - - /// std::init / ref_to_uninit (paper §5): true only if \p E is affirmatively - /// recognized as referring to (for a pointer source) or, when \p IsReference, - /// denoting (for a glvalue source) uninitialized storage. Recognized purely - /// locally from the expression's syntactic form -- the address of, or a - /// subobject of, a [[uninit]] entity; a value of a [[ref_to_uninit]] - /// pointer/reference or array; a dereference of such a pointer; a cast of such - /// a pointer to another pointer type, or of such a glvalue to another - /// reference; a call to a [[ref_to_uninit]]-returning function; or a - /// new-expression whose default-initialization leaves the allocated object - /// indeterminate (e.g. new int). A trusted-initialized source and an - /// unrecognized (unknown) source both return false (no flow analysis). - bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; - - /// std::init / ref_to_uninit (paper §5): check that the initialization of a - /// pointer or reference is consistent with its [[ref_to_uninit]] marking -- - /// a marked target must refer to uninitialized memory, and an unmarked - /// target must not. Shared by the variable, data-member, assignment, - /// argument, and return check sites; gated by shouldEmitProfileViolation. - void checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, - bool IsReference, const Expr *Src, - const Decl *D = nullptr); - - /// std::init / ref_to_uninit (paper §5): check that binding \p Src to - /// \p Target (a variable, data member, parameter, or function) is - /// consistent with the target's [[ref_to_uninit]] marking. \p T is the - /// bound type -- the target's type, or the return type when \p Target is a - /// function. No-op unless \p T is a non-dependent pointer or reference (a - /// dependent type defers to instantiation, where the check site re-runs - /// with the concrete type). \p D, when available, is the declaration used - /// for suppression lookup and template-pattern deferral. - void checkRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, - QualType T, const Expr *Src, - const Decl *D = nullptr); - - /// std::init / uninit_read (paper §4.5): diagnose a read *through* a - /// [[ref_to_uninit]] pointer or reference, whose result is itself - /// uninitialized. Called from Sema::DefaultLvalueConversion at the single - /// lvalue-to-rvalue chokepoint, with \p Glvalue the operand being loaded and - /// \p ValueType its value type. Reuses the ref_to_uninit recognizer in - /// read-only mode, so a direct read of a named [[uninit]] object is left to - /// the flow-based uninit_read pass. A std::byte read is exempt (paper §4.5). - void checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, - QualType ValueType); - - /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose - /// [[uninit]] placed on a pointer, a union variable, or a union member. - /// \p D must already carry the UninitAttr (the marker location is taken from - /// it). Decl-aware via shouldEmitProfileViolation, so it defers on a - /// templated pattern and is re-checked on the instantiated entity. - void diagnoseInitUninitMarkerPlacement(const Decl *D); - - class ProfileSuppressScope { - Sema &S; - unsigned Count = 0; - - void push(StringRef ProfileName, StringRef RuleName); - void addFromDecl(const Decl *D); - - public: - ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); - ProfileSuppressScope(Sema &S, const Decl *D, - bool WalkLexicalParents = false); - ProfileSuppressScope(Sema &S, ArrayRef Attrs); - ~ProfileSuppressScope(); - }; - /// Determines the active Scope associated with the given declaration /// context. /// @@ -1725,6 +1533,12 @@ class Sema final : public SemaBase { return *PPCPtr; } + /// C++ Profiles framework (P3589R2); see SemaProfiles.h. + SemaProfiles &Profiles() const { + assert(ProfilesPtr); + return *ProfilesPtr; + } + SemaPseudoObject &PseudoObject() { assert(PseudoObjectPtr); return *PseudoObjectPtr; @@ -1816,6 +1630,7 @@ class Sema final : public SemaBase { std::unique_ptr OpenCLPtr; std::unique_ptr OpenMPPtr; std::unique_ptr PPCPtr; + std::unique_ptr ProfilesPtr; std::unique_ptr PseudoObjectPtr; std::unique_ptr RISCVPtr; std::unique_ptr SPIRVPtr; @@ -6167,22 +5982,6 @@ class Sema final : public SemaBase { /// \param Record The completed class. void CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record); - /// Dispatch class-finalization profile callbacks for a completed class. - /// Called from \c CheckCompletedCXXClass so parser, template instantiation, - /// and lambda finalization paths all reach the same hook. Dependent, - /// invalid, and lambda classes are filtered out. - void checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD); - - /// Dispatch constructor-finalization profile callbacks once a constructor's - /// member-initializer list is complete. Called from \c ActOnMemInitializers - /// and \c ActOnDefaultCtorInitializers, which also serve template - /// instantiations (via \c InstantiateMemInitializers), so every - /// user-defined constructor is covered at the point its \c inits() is fully - /// populated -- unlike class finalization, which runs before any - /// constructor body is parsed. Dependent, invalid, and delegating - /// constructors are filtered out. - void checkProfileViolationsAtConstructorFinalization(CXXConstructorDecl *Ctor); - /// Check that the C++ class annoated with "trivial_abi" satisfies all the /// conditions that are needed for the attribute to have an effect. void checkIllFormedTrivialABIStruct(CXXRecordDecl &RD); diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h new file mode 100644 index 0000000000000..e64998c3d9e46 --- /dev/null +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -0,0 +1,246 @@ +//===----- SemaProfiles.h --- C++ profiles framework ----------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// \file +/// This file declares semantic analysis for the C++ profiles framework +/// (P3589R2) and the built-in std::init initialization profile (P4222R1.1). +/// See clang/docs/ProfilesFramework.rst for the design. +/// +//===----------------------------------------------------------------------===// + +#ifndef LLVM_CLANG_SEMA_SEMAPROFILES_H +#define LLVM_CLANG_SEMA_SEMAPROFILES_H + +#include "clang/AST/ASTFwd.h" +#include "clang/Basic/Profiles.h" +#include "clang/Basic/SourceLocation.h" +#include "clang/Sema/SemaBase.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallVector.h" +#include "llvm/ADT/StringRef.h" + +namespace clang { + +class AnalysisDeclContext; +class Module; +class ParsedAttr; +class ParsedAttributesView; +class ProfilesSuppressAttr; + +class SemaProfiles : public SemaBase { +public: + SemaProfiles(Sema &S); + + struct ProfileEnforcement : profiles::EnforcedProfile { + SourceLocation EnforceLoc; + }; + SmallVector EnforcedProfiles; + + struct ProfileSuppressEntry { + StringRef ProfileName; + StringRef RuleName; + }; + SmallVector ProfileSuppressStack; + + /// True while a class/constructor finalization profile callback runs. + /// Finalization can fire as a side effect of instantiating an unrelated + /// entity whose ProfileSuppressScope is still on ProfileSuppressStack, so + /// during finalization that transient stack is ignored and suppression is + /// resolved only from the finalized declaration and its lexical parents + /// (token-based dominion, P3589R2 s2.4p3). + bool InProfileFinalizationCheck = false; + + bool isProfileEnforced(StringRef ProfileName) const; + + /// True if any entry of \p Entries names an enforced profile. \p Entries is + /// any profile opt-in table whose elements expose a \c Name member; shared + /// by the post-parse dispatch gates (the CFG analysis pass guard and the + /// finalization dispatcher). + template + bool anyProfileEnforced(const Table &Entries) const { + return llvm::any_of( + Entries, [&](const auto &E) { return isProfileEnforced(E.Name); }); + } + + const ProfileEnforcement *getProfileEnforcement(StringRef ProfileName) const; + bool addProfileEnforcement(StringRef Name, StringRef Designator, + SourceLocation Loc); + bool processProfilesEnforceAttr( + const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators, + SmallVectorImpl *NewArgumentCounts = nullptr, + SmallVectorImpl *NewArgumentKeys = nullptr, + SmallVectorImpl *NewArgumentValues = nullptr, + SmallVectorImpl *NewArgumentKinds = nullptr); + + ProfilesSuppressAttr *makeProfilesSuppressAttr(const ParsedAttr &AL); + + /// Create an implicit ProfilesSuppressAttr carrying just a profile and rule + /// name (no justification or arguments), for propagating an active + /// suppression onto a declaration. + ProfilesSuppressAttr *makeImplicitProfilesSuppressAttr(StringRef ProfileName, + StringRef RuleName); + + bool isProfileSuppressed(StringRef ProfileName, + StringRef RuleName = "") const; + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Decl *D) const; + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + const Stmt *S, AnalysisDeclContext &AC) const; + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc); + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, const Decl *D); + bool shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, + const Stmt *UseStmt, + AnalysisDeclContext &AC) const; + bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc, unsigned DiagID); + + /// Dispatch class-finalization profile callbacks for a completed class. + /// Called from \c Sema::CheckCompletedCXXClass so parser, template + /// instantiation, and lambda finalization paths all reach the same hook. + /// Dependent, invalid, and lambda classes are filtered out. + void checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD); + + /// Dispatch constructor-finalization profile callbacks once a constructor's + /// member-initializer list is complete. Called from \c ActOnMemInitializers + /// and \c ActOnDefaultCtorInitializers, which also serve template + /// instantiations (via \c InstantiateMemInitializers), so every + /// user-defined constructor is covered at the point its \c inits() is fully + /// populated -- unlike class finalization, which runs before any + /// constructor body is parsed. Dependent, invalid, and delegating + /// constructors are filtered out. + void + checkProfileViolationsAtConstructorFinalization(CXXConstructorDecl *Ctor); + + /// std::init / uninit_decl (R2, paper §4.2): diagnose an automatic variable + /// definition that leaves the object (or a scalar subobject) indeterminate + /// without an acknowledging [[uninit]] marker. Called from + /// \c ActOnUninitializedDecl after default-initialization is attempted. + void checkInitProfileUninitDecl(const VarDecl *Var); + + /// std::init / static_marker (paper §3, §4.2): diagnose [[uninit]] on a + /// static or thread-storage variable, which is zero-initialized by language + /// rule and therefore an initialized object. Called from + /// \c ActOnUninitializedDecl. + void checkInitProfileStaticMarker(const VarDecl *Var); + + /// std::init / static_runtime_init (paper §3): diagnose a non-local static + /// whose initialization needs a runtime constructor. \p CheckConstInit + /// lazily evaluates whether the initializer is constant (trivial + /// default-init counts as constant here). Returns true if the diagnostic + /// was emitted, in which case the caller skips -Wglobal-constructors. + bool + checkInitProfileStaticRuntimeInit(const VarDecl *Var, + llvm::function_ref CheckConstInit); + + /// std::init / uninit_with_initializer (R4): diagnose \p D if it is both + /// marked [[uninit]] and has an initializer. Shared by the variable + /// (\c CheckCompleteVariableDeclaration) and non-static data member + /// (\c ActOnFinishCXXInClassMemberInitializer) paths. \p Init is the + /// (possibly null) initializer; a RecoveryExpr placeholder for a failed + /// initialization does not count as a user-written initializer. + void checkInitProfileUninitWithInitializer(const ValueDecl *D, + const Expr *Init); + + /// True if default-initialization of \p T would leave at least one scalar + /// subobject with an indeterminate value. Shared by the std::init rules + /// uninit_decl (at the variable declaration), ctor_uninit_member (for a + /// class-typed member), and uninit_with_initializer. A class with a + /// user-provided default constructor is trusted (that constructor is + /// checked at its own definition). Dependent and incomplete types are + /// treated as determinate. + /// + /// When \p HonorUninitMarkers is true, a data member marked [[uninit]] + /// is treated as acknowledged and skipped, so a type whose only + /// indeterminate scalars are all marked is reported as determinate. + /// uninit_decl and ctor_uninit_member pass true (the marker excuses the + /// member, paper §6.2); uninit_with_initializer passes false because it + /// needs the factual answer (whether the default-initialization is + /// genuinely a no-op). + bool defaultInitLeavesScalarIndeterminate(QualType T, + bool HonorUninitMarkers = false); + + /// If \p E (stripped of parens and implicit casts) directly names a + /// declaration -- a DeclRefExpr or a MemberExpr -- return that declaration; + /// otherwise null. The std::init checks read [[ref_to_uninit]] / + /// [[uninit]] markers only off a directly named entity. + static const ValueDecl *getDirectlyNamedDecl(const Expr *E); + + /// std::init / ref_to_uninit (paper §5): true only if \p E is affirmatively + /// recognized as referring to (for a pointer source) or, when + /// \p IsReference, denoting (for a glvalue source) uninitialized storage. + /// Recognized purely locally from the expression's syntactic form -- the + /// address of, or a subobject of, a [[uninit]] entity; a value of a + /// [[ref_to_uninit]] pointer/reference or array; a dereference of such a + /// pointer; a cast of such a pointer to another pointer type, or of such a + /// glvalue to another reference; a call to a [[ref_to_uninit]]-returning + /// function; or a new-expression whose default-initialization leaves the + /// allocated object indeterminate (e.g. new int). A trusted-initialized + /// source and an unrecognized (unknown) source both return false (no flow + /// analysis). + bool refersToUninitializedMemory(const Expr *E, bool IsReference) const; + + /// std::init / ref_to_uninit (paper §5): check that the initialization of a + /// pointer or reference is consistent with its [[ref_to_uninit]] marking -- + /// a marked target must refer to uninitialized memory, and an unmarked + /// target must not. Shared by the variable, data-member, assignment, + /// argument, and return check sites; gated by shouldEmitProfileViolation. + void checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, + bool IsReference, const Expr *Src, + const Decl *D = nullptr); + + /// std::init / ref_to_uninit (paper §5): check that binding \p Src to + /// \p Target (a variable, data member, parameter, or function) is + /// consistent with the target's [[ref_to_uninit]] marking. \p T is the + /// bound type -- the target's type, or the return type when \p Target is a + /// function. No-op unless \p T is a non-dependent pointer or reference (a + /// dependent type defers to instantiation, where the check site re-runs + /// with the concrete type). \p D, when available, is the declaration used + /// for suppression lookup and template-pattern deferral. + void checkRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, + QualType T, const Expr *Src, + const Decl *D = nullptr); + + /// std::init / uninit_read (paper §4.5): diagnose a read *through* a + /// [[ref_to_uninit]] pointer or reference, whose result is itself + /// uninitialized. Called from Sema::DefaultLvalueConversion at the single + /// lvalue-to-rvalue chokepoint, with \p Glvalue the operand being loaded + /// and \p ValueType its value type. Reuses the ref_to_uninit recognizer in + /// read-only mode, so a direct read of a named [[uninit]] object is left to + /// the flow-based uninit_read pass. A std::byte read is exempt (paper + /// §4.5). + void checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, + QualType ValueType); + + /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose + /// [[uninit]] placed on a pointer, a union variable, or a union member. + /// \p D must already carry the UninitAttr (the marker location is taken + /// from it). Decl-aware via shouldEmitProfileViolation, so it defers on a + /// templated pattern and is re-checked on the instantiated entity. + void diagnoseInitUninitMarkerPlacement(const Decl *D); + + class ProfileSuppressScope { + Sema &S; + unsigned Count = 0; + + void push(StringRef ProfileName, StringRef RuleName); + void addFromDecl(const Decl *D); + + public: + ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); + ProfileSuppressScope(Sema &S, const Decl *D, + bool WalkLexicalParents = false); + ProfileSuppressScope(Sema &S, ArrayRef Attrs); + ~ProfileSuppressScope(); + }; +}; + +} // namespace clang + +#endif // LLVM_CLANG_SEMA_SEMAPROFILES_H diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index 2391c569cbdaf..b741dfc9381ee 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -17,6 +17,7 @@ #include "clang/Sema/DeclSpec.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/ScopeExit.h" using namespace clang; @@ -375,7 +376,7 @@ void Parser::ParseLexedMethodDeclaration(LateParsedMethodDeclaration &LM) { // Start the delayed C++ method declaration Actions.ActOnStartDelayedCXXMethodDeclaration(getCurScope(), LM.Method); - Sema::ProfileSuppressScope ProfileSuppressGuard( + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( Actions, LM.Method->getAsFunction(), /*WalkLexicalParents=*/true); // Introduce the parameters into scope and parse their default @@ -606,7 +607,7 @@ void Parser::ParseLexedMethodDef(LexedMethod &LM) { Actions.ActOnStartOfFunctionDef(getCurScope(), LM.D); - Sema::ProfileSuppressScope ProfileSuppressGuard( + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( Actions, LM.D->getAsFunction(), /*WalkLexicalParents=*/true); llvm::scope_exit _([&]() { diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index de47763b3efac..dd46a4de60e53 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -27,6 +27,7 @@ #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaCodeCompletion.h" #include "clang/Sema/SemaObjC.h" @@ -2136,7 +2137,7 @@ Parser::DeclGroupPtrTy Parser::ParseDeclGroup(ParsingDeclSpec &DS, ParsedAttributes LocalAttrs(AttrFactory); LocalAttrs.takeAllPrependingFrom(Attrs); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, LocalAttrs); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, LocalAttrs); ParsingDeclarator D(*this, DS, LocalAttrs, Context); if (TemplateInfo.TemplateParams) @@ -2585,7 +2586,7 @@ Decl *Parser::ParseDeclarationAfterDeclaratorAndAttributes( SemaCUDA::CUDATargetContextRAII X(Actions.CUDA(), SemaCUDA::CTCK_InitGlobalVar, ThisDecl); - Sema::ProfileSuppressScope ProfileSuppressForInit(Actions, ThisDecl); + SemaProfiles::ProfileSuppressScope ProfileSuppressForInit(Actions, ThisDecl); switch (TheInitKind) { // Parse declarator '=' initializer. diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index cabaab6bff368..bf218f5cd37ab 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -28,6 +28,7 @@ #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCodeCompletion.h" #include "clang/Sema/SemaHLSL.h" #include "llvm/Support/TimeProfiler.h" @@ -207,7 +208,7 @@ Parser::DeclGroupPtrTy Parser::ParseNamespace(DeclaratorContext Context, getCurScope(), InlineLoc, NamespaceLoc, IdentLoc, Ident, T.getOpenLocation(), attrs, ImplicitUsingDirectiveDecl, false); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); PrettyDeclStackTraceEntry CrashInfo(Actions.Context, NamespcDecl, NamespaceLoc, "parsing namespace"); @@ -258,7 +259,7 @@ void Parser::ParseInnerNamespace(const InnerNamespaceInfoList &InnerNSs, assert(!ImplicitUsingDirectiveDecl && "nested namespace definition cannot define anonymous namespace"); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, NamespcDecl); ParseInnerNamespace(InnerNSs, ++index, InlineLoc, attrs, Tracker); @@ -3275,7 +3276,7 @@ ExprResult Parser::ParseCXXMemberInitializer(Decl *D, bool IsFunction, : Sema::ExpressionEvaluationContext::PotentiallyEvaluated, D); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, D, + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, D, /*WalkLexicalParents=*/true); // CWG2760 @@ -3665,7 +3666,7 @@ void Parser::ParseCXXMemberSpecification(SourceLocation RecordLoc, IsFinalSpelledSealed, IsAbstract, T.getOpenLocation()); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, TagDecl); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, TagDecl); // C++ 11p3: Members of a class defined with the keyword class are private // by default. Members of a class defined with the keywords struct or union diff --git a/clang/lib/Parse/ParseExprCXX.cpp b/clang/lib/Parse/ParseExprCXX.cpp index b52ca3a18092e..616c73f7afdf2 100644 --- a/clang/lib/Parse/ParseExprCXX.cpp +++ b/clang/lib/Parse/ParseExprCXX.cpp @@ -24,6 +24,7 @@ #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCodeCompletion.h" #include "llvm/Support/Compiler.h" #include "llvm/Support/ErrorHandling.h" @@ -1484,7 +1485,7 @@ ExprResult Parser::ParseLambdaExpressionAfterIntroducer( StmtResult Stmt; { - Sema::ProfileSuppressScope ProfileSuppressGuard( + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( Actions, Actions.getCurLambda()->CallOperator); Stmt = ParseCompoundStatementBody(); } diff --git a/clang/lib/Parse/ParseStmt.cpp b/clang/lib/Parse/ParseStmt.cpp index 16780a0a0b35f..afe82bcfc2a19 100644 --- a/clang/lib/Parse/ParseStmt.cpp +++ b/clang/lib/Parse/ParseStmt.cpp @@ -22,6 +22,7 @@ #include "clang/Sema/DeclSpec.h" #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Scope.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCodeCompletion.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenACC.h" @@ -75,7 +76,7 @@ StmtResult Parser::ParseStatementOrDeclaration(StmtVector &Stmts, if (getLangOpts().HLSL) MaybeParseMicrosoftAttributes(GNUOrMSAttrs); - Sema::ProfileSuppressScope ProfileSuppressGuard(Actions, CXX11Attrs); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(Actions, CXX11Attrs); StmtResult Res = ParseStatementOrDeclarationAfterAttributes( Stmts, StmtCtx, TrailingElseLoc, CXX11Attrs, GNUOrMSAttrs, diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index e7c8cab3d9574..fd219eec568f9 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -48,6 +48,7 @@ #include "clang/Lex/Preprocessor.h" #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/BitVector.h" #include "llvm/ADT/DenseMap.h" @@ -1940,7 +1941,8 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, Bx->getBeginLoc()); }); for (const Expr *R : Offending[I]) { - if (!S.shouldEmitProfileViolation("std::init", "uninit_read", R, AC)) + if (!S.Profiles().shouldEmitProfileViolation("std::init", "uninit_read", + R, AC)) continue; S.Diag(R->getBeginLoc(), diag::err_init_member_read_before_init) << "std::init" << Members[I]->getDeclName(); @@ -2027,7 +2029,8 @@ class UninitValsDiagReporter : public UninitVariablesHandler { if (E.ExemptStdByte && S.Context.getBaseElementType(vd->getType())->isStdByteType()) continue; - if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) + if (!S.Profiles().shouldEmitProfileViolation(E.Name, E.Rule, + U.getUser(), AC)) continue; S.Diag(U.getUser()->getBeginLoc(), E.DiagID) << E.Name << vd->getDeclName(); @@ -3046,7 +3049,7 @@ void sema::AnalysisBasedWarnings::clearOverrides() { } bool sema::AnalysisBasedWarnings::hasEnforcedCFGUninitProfile() const { - return S.anyProfileEnforced(CFGUninitProfiles); + return S.Profiles().anyProfileEnforced(CFGUninitProfiles); } static void flushDiagnostics(Sema &S, const sema::FunctionScopeInfo *fscope) { @@ -3152,7 +3155,7 @@ static void addNonLinearizedAlwaysAddClasses(AnalysisDeclContext &AC) { // and the post-error rerun so both paths stay in step. static void runCtorBodyInitCheckIfEnforced(Sema &S, const Decl *D, AnalysisDeclContext &AC) { - if (!S.isProfileEnforced("std::init")) + if (!S.Profiles().isProfileEnforced("std::init")) return; if (const auto *Ctor = dyn_cast(D)) checkInitProfileCtorBody(S, Ctor, AC); diff --git a/clang/lib/Sema/CMakeLists.txt b/clang/lib/Sema/CMakeLists.txt index 0ebf56ecffe69..ec12d752f902a 100644 --- a/clang/lib/Sema/CMakeLists.txt +++ b/clang/lib/Sema/CMakeLists.txt @@ -80,6 +80,7 @@ add_clang_library(clangSema SemaOpenMP.cpp SemaOverload.cpp SemaPPC.cpp + SemaProfiles.cpp SemaPseudoObject.cpp SemaRISCV.cpp SemaStmt.cpp diff --git a/clang/lib/Sema/Sema.cpp b/clang/lib/Sema/Sema.cpp index fdfadda5dea67..f1c0a47fdd0f3 100644 --- a/clang/lib/Sema/Sema.cpp +++ b/clang/lib/Sema/Sema.cpp @@ -63,6 +63,7 @@ #include "clang/Sema/SemaOpenCL.h" #include "clang/Sema/SemaOpenMP.h" #include "clang/Sema/SemaPPC.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaPseudoObject.h" #include "clang/Sema/SemaRISCV.h" #include "clang/Sema/SemaSPIRV.h" @@ -301,6 +302,7 @@ Sema::Sema(Preprocessor &pp, ASTContext &ctxt, ASTConsumer &consumer, OpenCLPtr(std::make_unique(*this)), OpenMPPtr(std::make_unique(*this)), PPCPtr(std::make_unique(*this)), + ProfilesPtr(std::make_unique(*this)), PseudoObjectPtr(std::make_unique(*this)), RISCVPtr(std::make_unique(*this)), SPIRVPtr(std::make_unique(*this)), @@ -2982,315 +2984,3 @@ Attr *Sema::CreateAnnotationAttr(const ParsedAttr &AL) { return CreateAnnotationAttr(AL, Str, Args); } -//===----------------------------------------------------------------------===// -// C++ Profiles framework (P3589R2) -//===----------------------------------------------------------------------===// - -bool Sema::isProfileEnforced(StringRef ProfileName) const { - if (!getLangOpts().Profiles) - return false; - // The built-in test:: profiles only exercise the framework; keep them inert - // unless the test suite opts in via -fprofiles-test-profiles. - if (!getLangOpts().ProfilesTestProfiles && ProfileName.starts_with("test::")) - return false; - return getProfileEnforcement(ProfileName) != nullptr; -} - -const Sema::ProfileEnforcement * -Sema::getProfileEnforcement(StringRef ProfileName) const { - for (const auto &E : EnforcedProfiles) - if (E.ProfileName == ProfileName) - return &E; - return nullptr; -} - -bool Sema::addProfileEnforcement(StringRef Name, StringRef Designator, - SourceLocation Loc) { - if (const auto *Existing = getProfileEnforcement(Name)) { - if (Existing->Designator != Designator) { - Diag(Loc, diag::err_profiles_enforce_mismatch) << Name; - Diag(Existing->EnforceLoc, diag::note_previous_attribute); - return false; - } - return true; - } - EnforcedProfiles.push_back({{Name.str(), Designator.str()}, Loc}); - return true; -} - -// Unzip profile arguments into the parallel key/value/kind arrays that the -// semantic attributes store (Attr.td cannot hold structured arguments). -static void unzipProfileArguments(ArrayRef Arguments, - SmallVectorImpl &Keys, - SmallVectorImpl &Values, - SmallVectorImpl &Kinds) { - for (const auto &Arg : Arguments) { - Keys.push_back(Arg.Key); - Values.push_back(Arg.Value); - Kinds.push_back(static_cast(Arg.Kind)); - } -} - -static void appendProfileArgumentData( - ArrayRef Arguments, - SmallVectorImpl *ArgumentCounts, - SmallVectorImpl *ArgumentKeys, - SmallVectorImpl *ArgumentValues, - SmallVectorImpl *ArgumentKinds) { - if (!ArgumentCounts) - return; - - assert(ArgumentKeys && ArgumentValues && ArgumentKinds); - ArgumentCounts->push_back(Arguments.size()); - unzipProfileArguments(Arguments, *ArgumentKeys, *ArgumentValues, - *ArgumentKinds); -} - -bool Sema::processProfilesEnforceAttr( - const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, - SmallVectorImpl *NewDesignators, - SmallVectorImpl *NewArgumentCounts, - SmallVectorImpl *NewArgumentKeys, - SmallVectorImpl *NewArgumentValues, - SmallVectorImpl *NewArgumentKinds) { - const auto &Args = AL.getProfileEnforceArgs(); - if (Args.Designators.empty()) { - Diag(AL.getLoc(), diag::err_attribute_too_few_arguments) << AL << 1; - return false; - } - - for (const auto &D : Args.Designators) { - StringRef Name = D.Name; - StringRef Spelling = D.Spelling; - - bool IsNew = !isProfileEnforced(Name); - if (!addProfileEnforcement(Name, Spelling, AL.getLoc())) - continue; - - if (Mod && !llvm::any_of(Mod->EnforcedProfileDesignators, - [&](const Module::EnforcedProfile &EP) { - return EP.ProfileName == Name; - })) - Mod->EnforcedProfileDesignators.push_back({Name.str(), Spelling.str()}); - - if (IsNew) { - if (NewNames) - NewNames->push_back(Name); - if (NewDesignators) - NewDesignators->push_back(Spelling); - appendProfileArgumentData(D.Arguments, NewArgumentCounts, - NewArgumentKeys, NewArgumentValues, - NewArgumentKinds); - } - } - return true; -} - -ProfilesSuppressAttr *Sema::makeProfilesSuppressAttr(const ParsedAttr &AL) { - const auto &Args = AL.getProfileSuppressArgs(); - if (Args.Name.empty()) - return nullptr; - - SmallVector RawArgs; - for (const auto &Arg : Args.RawArguments) - RawArgs.push_back(Arg); - SmallVector RawArgumentKeys; - SmallVector RawArgumentValues; - SmallVector RawArgumentKinds; - unzipProfileArguments(Args.Arguments, RawArgumentKeys, RawArgumentValues, - RawArgumentKinds); - - return ::new (Context) ProfilesSuppressAttr( - Context, AL, Args.Name, Args.Justification, Args.Rule, - RawArgs.data(), RawArgs.size(), RawArgumentKeys.data(), - RawArgumentKeys.size(), RawArgumentValues.data(), - RawArgumentValues.size(), RawArgumentKinds.data(), - RawArgumentKinds.size()); -} - -ProfilesSuppressAttr * -Sema::makeImplicitProfilesSuppressAttr(StringRef ProfileName, - StringRef RuleName) { - return ProfilesSuppressAttr::CreateImplicit( - Context, ProfileName, /*Justification=*/"", RuleName, - /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0, - /*RawArgumentKeys=*/nullptr, /*RawArgumentKeysSize=*/0, - /*RawArgumentValues=*/nullptr, /*RawArgumentValuesSize=*/0, - /*RawArgumentKinds=*/nullptr, /*RawArgumentKindsSize=*/0); -} - -static bool profileSuppressMatches(StringRef EntryProfile, StringRef EntryRule, - StringRef Profile, StringRef Rule) { - return EntryProfile == Profile && - (EntryRule.empty() || EntryRule == Rule); -} - -bool Sema::isProfileSuppressed(StringRef ProfileName, - StringRef RuleName) const { - for (const auto &E : ProfileSuppressStack) - if (profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, - RuleName)) - return true; - return false; -} - -bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName, - const Decl *D) const { - for (; D;) { - for (const auto *PSA : D->specific_attrs()) - if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), - ProfileName, RuleName)) - return true; - const DeclContext *DC = D->getLexicalDeclContext(); - D = DC ? dyn_cast(DC) : nullptr; - } - return false; -} - -bool Sema::isProfileSuppressed(StringRef ProfileName, StringRef RuleName, - const Stmt *S, - AnalysisDeclContext &AC) const { - ParentMap &PM = AC.getParentMap(); - for (const Stmt *Cur = S; Cur; Cur = PM.getParent(Cur)) { - if (const auto *AS = dyn_cast(Cur)) - for (const Attr *A : AS->getAttrs()) - if (const auto *PSA = dyn_cast(A)) - if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), - ProfileName, RuleName)) - return true; - // [[profiles::suppress]] on a local variable attaches to the VarDecl, - // not the enclosing DeclStmt. Walk the declared decls so the post-parse - // walker matches the parse-time ProfileSuppressForInit RAII behavior. - if (const auto *DS = dyn_cast(Cur)) - for (const Decl *D : DS->decls()) - for (const auto *PSA : D->specific_attrs()) - if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), - ProfileName, RuleName)) - return true; - } - return isProfileSuppressed(ProfileName, RuleName, AC.getDecl()); -} - -bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc) { - return shouldEmitProfileViolation(ProfileName, RuleName, Loc, /*D=*/nullptr); -} - -bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc, const Decl *D) { - if (!isProfileEnforced(ProfileName)) - return false; - // Honor [[profiles::suppress]] from the parse-time stack and, when a Decl is - // available, from the declaration and its lexical parents. The latter does - // not depend on a parse-time scope still being active, so finalization checks - // that run after the parse scope is torn down still respect suppression. - // - // Finalization callbacks skip the parse-time stack: they can fire while an - // unrelated entity's instantiation ProfileSuppressScope is still active, and - // that scope does not lexically enclose the finalized declaration (token- - // based dominion, P3589R2 s2.4p3). Their decl-aware walk already covers a - // suppression on the declaration or a lexical parent. - if ((!InProfileFinalizationCheck && - isProfileSuppressed(ProfileName, RuleName)) || - isProfileSuppressed(ProfileName, RuleName, D)) - return false; - // P3589R2 Section 1.1: "its static semantic effects are as-if applied only - // after translation phase 7. It is not possible for a profile to change the - // outcome of overload resolution or template instantiation, nor is it - // possible to 'SFINAE out' failure of a program to satisfy a profile - // requirement." - // - // A templated entity is not yet a phase-7 entity, so a profile rule must fire - // only on its instantiation -- where D is the instantiated, non-templated - // declaration -- not on the template pattern. Checking the pattern too would - // diagnose never-instantiated templates and double-fire (once when the - // pattern is parsed and again at each instantiation). - // - // Decl-less expression check sites whose Build* routine is re-run at - // instantiation (the ref_to_uninit binding checks: call argument, pointer - // assignment, return) instead defer in a dependent context from their own - // wrapper, checkRefToUninitInit, since no Decl is available here. The - // [[uninit]] marker checks pass D (via diagnoseInitUninitMarkerPlacement) and - // are re-run on the instantiated field / variable, so they defer here too. - // The reinterpret_cast check still passes D == nullptr and is not re-checked - // at instantiation, so it keeps running once at parse time (a separate gap). - if (D && D->isTemplated()) - return false; - if (isUnevaluatedContext()) - return false; - if (currentEvaluationContext().isDiscardedStatementContext()) - return false; - return true; -} - -bool Sema::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, - const Stmt *UseStmt, - AnalysisDeclContext &AC) const { - if (!isProfileEnforced(ProfileName)) - return false; - if (isProfileSuppressed(ProfileName, RuleName, UseStmt, AC)) - return false; - return true; -} - -bool Sema::checkProfileViolation(StringRef ProfileName, StringRef RuleName, - SourceLocation Loc, unsigned DiagID) { - if (!shouldEmitProfileViolation(ProfileName, RuleName, Loc)) - return false; - Diag(Loc, DiagID) << ProfileName; - return true; -} - -void Sema::ProfileSuppressScope::push(StringRef ProfileName, - StringRef RuleName) { - S.ProfileSuppressStack.push_back({ProfileName, RuleName}); - ++Count; -} - -Sema::ProfileSuppressScope::ProfileSuppressScope( - Sema &S, const ParsedAttributesView &Attrs) - : S(S) { - if (!S.getLangOpts().Profiles) - return; - for (const auto &AL : Attrs) { - if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) - continue; - const auto &Args = AL.getProfileSuppressArgs(); - if (!Args.Name.empty()) - push(Args.Name, Args.Rule); - } -} - -void Sema::ProfileSuppressScope::addFromDecl(const Decl *D) { - for (const auto *A : D->specific_attrs()) - push(A->getProfileName(), A->getRule()); -} - -Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, - bool WalkLexicalParents) - : S(S) { - if (!S.getLangOpts().Profiles || !D) - return; - addFromDecl(D); - if (WalkLexicalParents) { - for (const DeclContext *DC = D->getLexicalDeclContext(); DC; - DC = DC->getLexicalParent()) - if (const auto *Parent = dyn_cast(DC)) - addFromDecl(Parent); - } -} - -Sema::ProfileSuppressScope::ProfileSuppressScope(Sema &S, - ArrayRef Attrs) - : S(S) { - if (!S.getLangOpts().Profiles) - return; - for (const auto *A : Attrs) - if (const auto *PSA = dyn_cast(A)) - push(PSA->getProfileName(), PSA->getRule()); -} - -Sema::ProfileSuppressScope::~ProfileSuppressScope() { - assert(S.ProfileSuppressStack.size() >= Count); - S.ProfileSuppressStack.pop_back_n(Count); -} diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index 795f0425818df..deec789eac1be 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -25,6 +25,7 @@ #include "clang/Sema/Initialization.h" #include "clang/Sema/SemaHLSL.h" #include "clang/Sema/SemaObjC.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaRISCV.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringExtras.h" @@ -401,7 +402,8 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); // test::type_cast is a built-in test profile; see ProfilesFramework.rst. - checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, + Profiles().checkProfileViolation("test::type_cast", "reinterpret_cast", + OpLoc, diag::err_profile_type_cast_reinterpret); } return Op.complete(CXXReinterpretCastExpr::Create(Context, Op.ResultType, diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 18a7f7f9cbd32..03182fbe1c814 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -50,6 +50,7 @@ #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaHLSL.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenACC.h" #include "clang/Sema/SemaOpenMP.h" @@ -14715,8 +14716,8 @@ void Sema::ActOnUninitializedDecl(Decl *RealDecl) { Var->setInit(RecoveryExpr.get()); } - checkInitProfileUninitDecl(Var); - checkInitProfileStaticMarker(Var); + Profiles().checkInitProfileUninitDecl(Var); + Profiles().checkInitProfileStaticMarker(Var); CheckCompleteVariableDeclaration(Var); } @@ -14821,513 +14822,15 @@ void Sema::addLifetimeBoundToImplicitThis(CXXMethodDecl *MD) { MD->setTypeSourceInfo(TLB.getTypeSourceInfo(Context, AttributedType)); } -static bool defaultInitLeavesScalarIndeterminateImpl( - ASTContext &Ctx, QualType T, bool HonorUninitMarkers, - llvm::SmallPtrSetImpl &Visited) { - if (T->isDependentType() || T->isIncompleteType()) - return false; - if (const ArrayType *AT = Ctx.getAsArrayType(T)) - return defaultInitLeavesScalarIndeterminateImpl( - Ctx, AT->getElementType(), HonorUninitMarkers, Visited); - if (T->isReferenceType()) - return false; - const auto *RD = T->getAsCXXRecordDecl(); - if (!RD) - // Scalars, pointers, and enums are left indeterminate by default-init, - // except std::byte, which the profile permits to be uninitialized - // (paper §4), so a std::byte subobject does not make a record indeterminate. - return T->isScalarType() && !T->isStdByteType(); - if (RD->isInvalidDecl()) - return false; - // A union's members are mutually exclusive, so the per-member walk below does - // not apply. Default-initialization leaves it without an initialized member - // (paper §6.5) unless it has no members, has a user-provided default - // constructor (trusted), or a default member initializer initializes one. - if (RD->isUnion()) { - if (RD->field_empty() || RD->hasUserProvidedDefaultConstructor()) - return false; - for (const FieldDecl *F : RD->fields()) - if (F->hasInClassInitializer()) - return false; - return true; - } - // Break cycles from ill-formed self-containing types (e.g. struct S { S x; }). - if (!Visited.insert(RD->getCanonicalDecl()).second) - return false; - // Trust a user-provided default constructor: ctor_uninit_member checks at its - // definition. - if (RD->hasUserProvidedDefaultConstructor()) - return false; - for (const CXXBaseSpecifier &Base : RD->bases()) - if (defaultInitLeavesScalarIndeterminateImpl(Ctx, Base.getType(), - HonorUninitMarkers, Visited)) - return true; - for (const FieldDecl *F : RD->fields()) { - if (F->isUnnamedBitField() || F->hasInClassInitializer()) - continue; - // A member the type's author marked [[uninit]] is acknowledged as - // intentionally uninitialized, so it does not leave an unacknowledged - // scalar indeterminate (paper §6.2). - if (HonorUninitMarkers && F->hasAttr()) - continue; - if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), - HonorUninitMarkers, Visited)) - return true; - } - return false; -} - -bool Sema::defaultInitLeavesScalarIndeterminate(QualType T, - bool HonorUninitMarkers) { - llvm::SmallPtrSet Visited; - return defaultInitLeavesScalarIndeterminateImpl(Context, T, - HonorUninitMarkers, Visited); -} - -void Sema::checkInitProfileUninitDecl(const VarDecl *Var) { - // std::init / uninit_decl: a definition without any initializer (after - // attempted default-initialization) must either carry [[uninit]] or - // be initialized by a language rule. Static / thread storage duration is - // excluded -- those are zero-initialized; runtime-init concerns are R3's. - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "uninit_decl"; - // The enforcement check gates the (possibly recursive) type walk below so - // it runs only under the profile, not on every default-initialized - // variable. - QualType BaseTy = Context.getBaseElementType(Var->getType()); - if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && - !Var->hasAttr() && - // std::byte may be left uninitialized (paper §4), so it -- and arrays - // of it -- are exempt from this rule. - !BaseTy->isStdByteType() && - shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && - // A definition with no initializer (scalar / pointer / enum, or an - // array of them), or a class/aggregate type -- possibly the element - // type of an array -- whose default-init leaves a scalar subobject - // indeterminate (its synthesized constructor call provides an - // initializer, so the !getInit() test alone misses it). - (!Var->getInit() || - (BaseTy->isRecordType() && - defaultInitLeavesScalarIndeterminate(Var->getType(), - /*HonorUninitMarkers=*/true)))) { - // A union variable cannot carry [[uninit]] (union_marker bans it), - // so it must be initialized; use a message that does not suggest the - // marker as a remedy. - bool IsUnion = BaseTy->isUnionType(); - Diag(Var->getLocation(), - IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) - << Profile << Var->getDeclName(); - } -} - -void Sema::checkInitProfileStaticMarker(const VarDecl *Var) { - // std::init / static_marker: a variable with static or thread storage - // duration is zero-initialized by language rule (paper §3), so it is an - // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an - // initialized object marked [[uninit]] is an error"). The case with a real - // initializer -- explicit, or a constructor that actually runs -- is - // already caught by uninit_with_initializer (R4, in - // CheckCompleteVariableDeclaration); this covers the zero-initialized, - // no-real-initializer case R4 treats as a consistent no-op. The factual - // (HonorUninitMarkers=false) walk matches R4: a static object whose only - // indeterminate scalars are themselves marked is still zero-initialized. - static constexpr StringRef Profile = "std::init"; - QualType BaseTy = Context.getBaseElementType(Var->getType()); - if (!Var->isInvalidDecl() && - (Var->getStorageDuration() == SD_Static || - Var->getStorageDuration() == SD_Thread) && - Var->hasAttr() && - // A union or pointer object marked [[uninit]] is already rejected by - // union_marker / pointer_marker (regardless of storage duration), and - // they retain the marker; do not pile a second diagnostic on top. - !Var->getType()->isUnionType() && !Var->getType()->isPointerType() && - shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), - Var) && - (!Var->getInit() || - (BaseTy->isRecordType() && - defaultInitLeavesScalarIndeterminate(Var->getType(), - /*HonorUninitMarkers=*/false)))) { - bool IsThread = Var->getStorageDuration() == SD_Thread; - Diag(Var->getLocation(), diag::err_init_uninit_static_marker) - << Profile << Var->getDeclName() << IsThread; - } -} - -bool Sema::checkInitProfileStaticRuntimeInit( - const VarDecl *Var, llvm::function_ref CheckConstInit) { - // Thread-locals have thread (not static) storage duration; paper §3 scopes - // this rule to non-local *static* objects (uninit_decl likewise excludes - // thread storage). - if (Var->getTLSKind() != VarDecl::TLS_None) - return false; - // std::init / static_runtime_init: paper says non-local statics must be - // initialized at compile or link time. CheckConstInit() permits trivial - // default initialization (not a constant initializer but needs no global - // constructor), so a zero-initialized aggregate such as - // `struct S { int x; }; S g;` is not a violation. Runs before - // -Wglobal-constructors so the profile error (when enforced) takes - // precedence over the standalone warning. - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "static_runtime_init"; - if (CheckConstInit()) - return false; - if (!shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var)) - return false; - Diag(Var->getLocation(), diag::err_init_static_runtime_init) - << Profile << Var->getDeclName(); - return true; -} - -void Sema::checkInitProfileUninitWithInitializer(const ValueDecl *D, - const Expr *Init) { - // [[uninit]] documents that the entity is intentionally left - // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr - // is a placeholder for an initialization that already failed (e.g. - // default-init of a const scalar), not an initializer the user wrote, so it - // must not trigger this rule. - if (!D->hasAttr() || !Init || - isa(Init->IgnoreParens())) - return; - SourceLocation Loc = D->getLocation(); - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "uninit_with_initializer"; - // Gate the (possibly recursive) type walk below on enforcement. - if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) - return; - // A synthesized default-initialization that leaves the object indeterminate - // (a trivial or aggregate type, no user-written initializer) is consistent - // with the marker: the object really is left uninitialized, to be - // initialized later (e.g. via construct_at), mirroring the scalar case. Only - // a real initializer -- an explicit one, or a constructor that actually runs - // -- contradicts the marker. - if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) - if (CCE->getConstructor()->isDefaultConstructor() && - defaultInitLeavesScalarIndeterminate(D->getType())) - return; - Diag(Loc, diag::err_init_uninit_with_initializer) - << Profile << D->getDeclName(); -} - -void Sema::diagnoseInitUninitMarkerPlacement(const Decl *D) { - const auto *UA = D->getAttr(); - if (!UA) - return; - SourceLocation Loc = UA->getLocation(); - - // std::init / union_marker (paper §5.6): the marker is banned on a union - // object or a union member, because delayed initialization by assigning a - // member would be an erroneous assignment when compiled without the profile. - // std::init / pointer_marker (paper §4.1): "a reference cannot be - // uninitialized. The initialization profile requires the same for pointers." - // A pointer must instead be initialized (e.g. to nullptr). Both are profile - // policy (not a meaningless subject), so they are gated on enforcement; the - // marker is left in place so uninit_decl / ctor_uninit_member treat the - // entity as acknowledged rather than re-diagnosing it. - // - // Passing \p D makes shouldEmitProfileViolation defer on a templated pattern - // (paper / P3589R2: a rule fires on the instantiation, not the template), - // so the parse-time handler skips template members and the rule is re-run on - // the instantiated entity (VisitFieldDecl / VisitVarDecl), once the - // substituted type is known to be a pointer or union. - bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); - bool UnionMember = - isa(D) && cast(D)->getParent()->isUnion(); - if ((UnionVar || UnionMember) && - shouldEmitProfileViolation("std::init", "union_marker", Loc, D)) - Diag(Loc, diag::err_init_union_marker) - << "std::init" << (UnionMember ? 1 : 0); - else if (cast(D)->getType()->isPointerType() && - shouldEmitProfileViolation("std::init", "pointer_marker", Loc, D)) - Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; -} - -// std::init / ref_to_uninit (paper §5). Two mutually-recursive local -// recognizers over the syntactic form of a source expression -- no flow -// analysis and no type-system tracking. Uninitialized storage is only ever -// introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker. -// -// The classification is tri-state: a recognized form is Initialized or -// Uninitialized, while an unrecognized one (pointer arithmetic, an -// integer-to-pointer cast, a call through a function pointer) is Unknown rather -// than assumed Initialized. Callers wanting a plain "is it uninitialized?" -// answer (Sema::refersToUninitializedMemory, the read-through check) treat -// Unknown as not uninitialized. -// -// \p ForRead selects read-through mode (Sema::checkRefToUninitRead): a -// directly named [[uninit]] object is then not treated as uninitialized, -// because a read of such a named object is the flow-based uninit_read pass's -// responsibility; only indirection through a [[ref_to_uninit]] -// pointer/reference still counts. -enum class UninitStorage { Initialized, Uninitialized, Unknown }; - -// Combine the arms of a conditional: Uninitialized dominates (either arm may be -// taken), then Unknown, else Initialized. -static UninitStorage combineArms(UninitStorage A, UninitStorage B) { - if (A == UninitStorage::Uninitialized || B == UninitStorage::Uninitialized) - return UninitStorage::Uninitialized; - if (A == UninitStorage::Unknown || B == UninitStorage::Unknown) - return UninitStorage::Unknown; - return UninitStorage::Initialized; -} - -static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, - bool ForRead = false); - -const ValueDecl *Sema::getDirectlyNamedDecl(const Expr *E) { - E = E->IgnoreParenImpCasts(); - if (const auto *DRE = dyn_cast(E)) - return DRE->getDecl(); - if (const auto *ME = dyn_cast(E)) - return ME->getMemberDecl(); - return nullptr; -} - -// Pass-through forms shared by the pointer and glvalue recognizers, which are -// transparent to their operand: a single-element braced initializer { e } -// binds from e (modeling -// MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); a conditional -// is uninit if either arm is, so a value that may be uninit forces a marked -// target; a comma yields its right operand. \p EmptyListState classifies an -// empty braced list: {} value-initializes a pointer to null (Initialized), -// while a glvalue has no empty-list form (Unknown); a multi-element list is -// Unknown for both. Returns std::nullopt when E is not a pass-through form. -template -static std::optional -classifyUninitPassThrough(const Expr *E, UninitStorage EmptyListState, - RecurseFn Recurse) { - if (const auto *ILE = dyn_cast(E)) { - if (ILE->getNumInits() == 1) - return Recurse(ILE->getInit(0)); - return ILE->getNumInits() == 0 ? EmptyListState : UninitStorage::Unknown; - } - if (const auto *CO = dyn_cast(E)) - return combineArms(Recurse(CO->getTrueExpr()), - Recurse(CO->getFalseExpr())); - if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) - return Recurse(BO->getRHS()); - return std::nullopt; -} - -// A call to a [[ref_to_uninit]]-returning function yields uninitialized -// storage (the pointed-to memory, or the returned referent). An unmarked -// direct callee is trusted Initialized (paper §4.3); a call with no direct -// callee (through a function pointer) is Unknown. Shared by both recognizers. -static UninitStorage classifyRefToUninitCallee(const CallExpr *CE) { - if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->hasAttr() ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - return UninitStorage::Unknown; -} - -// \p E is a pointer prvalue. Classifies whether it points to uninitialized -// storage. -static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, - const Expr *E, - bool ForRead = false) { - if (!E) - return UninitStorage::Unknown; - E = E->IgnoreParenImpCasts(); - - if (auto PassThrough = classifyUninitPassThrough( - E, /*EmptyListState=*/UninitStorage::Initialized, - [&](const Expr *Sub) { - return pointerRefersToUninitStorage(Ctx, Sub, ForRead); - })) - return *PassThrough; - - // Array-to-pointer decay has been stripped above, leaving the array glvalue. - if (E->getType()->isArrayType()) - return glvalueDenotesUninitStorage(Ctx, E, ForRead); - - // &G, where G denotes uninitialized storage. - if (const auto *UO = dyn_cast(E)) - if (UO->getOpcode() == UO_AddrOf) - return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), ForRead); - - // A value of a [[ref_to_uninit]] pointer is Uninitialized; an unmarked - // named pointer is a trusted Initialized pointer (paper §4.3). - if (const ValueDecl *VD = Sema::getDirectlyNamedDecl(E)) - return VD->hasAttr() ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - if (const auto *CE = dyn_cast(E)) - return classifyRefToUninitCallee(CE); - - // A default-initialized new-expression (none init style: no initializer - // written) whose allocated type's default-initialization leaves a scalar - // subobject indeterminate produces uninitialized free-store memory (paper - // §1.2/§4.3), like a [[ref_to_uninit]] allocator. The style gates this rather - // than hasInitializer(), which is also true for new Agg -- default- - // initializing a class synthesizes a (possibly trivial) constructor call. - // new T(...) / new T{} are value- or list-initialized; a user-provided - // default constructor is trusted by defaultInitLeavesScalarIndeterminate. - if (const auto *NE = dyn_cast(E)) { - if (NE->getInitializationStyle() != CXXNewInitializationStyle::None) - return UninitStorage::Initialized; - llvm::SmallPtrSet Visited; - return defaultInitLeavesScalarIndeterminateImpl( - Ctx, NE->getAllocatedType(), /*HonorUninitMarkers=*/true, Visited) - ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - } - - // Paper §4.3: a [[ref_to_uninit]] pointer cast to another pointer type is - // itself [[ref_to_uninit]]. Implicit casts were already stripped above, so - // this only looks through an explicit pointer-to-pointer cast; a pointer - // manufactured from an integer (operand not a pointer) is Unknown. - if (const auto *CE = dyn_cast(E)) - if (CE->getSubExpr()->getType()->isPointerType()) - return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), ForRead); - - return UninitStorage::Unknown; -} - -// \p E is a glvalue. Classifies whether it denotes uninitialized storage. -static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, - const Expr *E, bool ForRead) { - if (!E) - return UninitStorage::Unknown; - E = E->IgnoreParenImpCasts(); - - if (auto PassThrough = classifyUninitPassThrough( - E, /*EmptyListState=*/UninitStorage::Unknown, - [&](const Expr *Sub) { - return glvalueDenotesUninitStorage(Ctx, Sub, ForRead); - })) - return *PassThrough; - - // A named entity denotes uninitialized storage if it is [[uninit]], or - // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, - // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes - // the pointer object itself -- which is initialized -- so it does not count. - // In read mode the [[uninit]] arm is dropped: a direct read of a named - // [[uninit]] object is left to the flow-based uninit_read pass, so only a - // [[ref_to_uninit]] reference (or indirection, handled below) still counts. - auto DeclDenotesUninit = [&](const ValueDecl *VD) { - return (!ForRead && VD->hasAttr()) || - (VD->getType()->isReferenceType() && VD->hasAttr()); - }; - if (const auto *DRE = dyn_cast(E)) - return DeclDenotesUninit(DRE->getDecl()) ? UninitStorage::Uninitialized - : UninitStorage::Initialized; - if (const auto *ME = dyn_cast(E)) { - // a->m reaches m through the pointer a (object *a); a.m through the - // glvalue a. When m does not itself denote uninit storage, the subobject is - // uninit exactly when its base is. - if (DeclDenotesUninit(ME->getMemberDecl())) - return UninitStorage::Uninitialized; - return ME->isArrow() - ? pointerRefersToUninitStorage(Ctx, ME->getBase(), ForRead) - : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead); - } - // A call to a [[ref_to_uninit]]-returning reference function: the referent - // it returns is uninitialized. - if (const auto *CE = dyn_cast(E)) - return classifyRefToUninitCallee(CE); - if (const auto *ASE = dyn_cast(E)) - return pointerRefersToUninitStorage(Ctx, ASE->getBase(), ForRead); - - // *p, where p points to uninitialized storage. - if (const auto *UO = dyn_cast(E)) - if (UO->getOpcode() == UO_Deref) - return pointerRefersToUninitStorage(Ctx, UO->getSubExpr(), ForRead); - - // A reference cast (an explicit cast yielding a glvalue) denotes the same - // storage as its operand; propagate. Symmetric to the pointer-cast arm. - if (const auto *CE = dyn_cast(E)) - if (CE->getSubExpr()->isGLValue()) - return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), ForRead); - - return UninitStorage::Unknown; -} - -// Dispatches a binding source to the pointer or glvalue recognizer. -static UninitStorage classifyUninitSource(ASTContext &Ctx, const Expr *E, - bool IsReference) { - return IsReference ? glvalueDenotesUninitStorage(Ctx, E) - : pointerRefersToUninitStorage(Ctx, E); -} - -bool Sema::refersToUninitializedMemory(const Expr *E, bool IsReference) const { - return classifyUninitSource(Context, E, IsReference) == - UninitStorage::Uninitialized; -} - -// The Decl-less ref_to_uninit check sites (call argument, default argument, -// assignment, return, read-through) can't rely on the D->isTemplated() -// deferral in shouldEmitProfileViolation, but their Build* routines are -// re-run at instantiation, so defer on a template pattern here instead: -// firing on the pattern double-diagnoses and wrongly fires in discarded -// if-constexpr branches / never-instantiated templates. Sites that do pass a -// Decl are unaffected. (This must stay out of shouldEmitProfileViolation: -// its other Decl-less caller, the reinterpret_cast check, is *not* re-run at -// instantiation, so deferring there would lose the diagnostic entirely.) -static bool deferUninitCheckOnTemplatePattern(Sema &S, const Decl *D) { - return !D && S.CurContext && S.CurContext->isDependentContext(); -} - -void Sema::checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, - bool IsReference, const Expr *Src, - const Decl *D) { - // A RecoveryExpr is a placeholder for an initialization that already failed, - // not a source the user wrote, so it must not drive this rule. - if (!Src || isa(Src->IgnoreParens())) - return; - if (deferUninitCheckOnTemplatePattern(*this, D)) - return; - static constexpr StringRef Profile = "std::init"; - static constexpr StringRef Rule = "ref_to_uninit"; - if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) - return; - UninitStorage SrcState = classifyUninitSource(Context, Src, IsReference); - unsigned IsRef = IsReference ? 1 : 0; - // A marked target is a violation only against an affirmatively Initialized - // source: an Unknown one (pointer arithmetic, an integer-to-pointer cast, a - // call through a function pointer) cannot be proven initialized, so rejecting - // it would be a false positive. An unmarked target is diagnosed only against - // an affirmatively Uninitialized source (Unknown stays a missed diagnostic). - if (TargetIsRefToUninit && SrcState == UninitStorage::Initialized) - Diag(Loc, diag::err_init_ref_to_uninit_requires_uninit) << Profile << IsRef; - else if (!TargetIsRefToUninit && SrcState == UninitStorage::Uninitialized) - Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; -} - -void Sema::checkRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, - QualType T, const Expr *Src, - const Decl *D) { - if (!getLangOpts().Profiles || T.isNull() || T->isDependentType() || - (!T->isPointerType() && !T->isReferenceType())) - return; - checkRefToUninitInit(Loc, Target->hasAttr(), - T->isReferenceType(), Src, D); -} - -void Sema::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, - QualType ValueType) { - // A RecoveryExpr is a placeholder for an expression that already failed, not - // a read the user wrote, so it must not drive this rule. - if (!Glvalue || isa(Glvalue->IgnoreParens())) - return; - if (deferUninitCheckOnTemplatePattern(*this, /*D=*/nullptr)) - return; - // Paper §4.5: reading an uninitialized std::byte is permitted. - if (Context.getBaseElementType(ValueType)->isStdByteType()) - return; - if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) - return; - if (glvalueDenotesUninitStorage(Context, Glvalue, /*ForRead=*/true) != - UninitStorage::Uninitialized) - return; - Diag(Loc, diag::err_init_uninit_read_through) << "std::init"; -} - void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { if (var->isInvalidDecl()) return; - checkInitProfileUninitWithInitializer(var, var->getInit()); + Profiles().checkInitProfileUninitWithInitializer(var, var->getInit()); // std::init / ref_to_uninit (paper §5): a pointer or reference variable // must be bound consistently with its [[ref_to_uninit]] marking. - checkRefToUninitBinding(var->getLocation(), var, var->getType(), + Profiles().checkRefToUninitBinding(var->getLocation(), var, var->getType(), + var->getInit(), var); CUDA().MaybeAddConstantAttr(var); @@ -15567,7 +15070,8 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { Diag(it.first, it.second); var->setInvalidDecl(); } else if (IsGlobal && - checkInitProfileStaticRuntimeInit(var, checkConstInit)) { + Profiles().checkInitProfileStaticRuntimeInit(var, + checkConstInit)) { // The profile diagnostic supersedes -Wglobal-constructors below. } else if (IsGlobal && !getDiagnostics().isIgnored(diag::warn_global_constructor, diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 376e09c2419ee..be83b366d7c91 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -42,6 +42,7 @@ #include "clang/Sema/Scope.h" #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaAMDGPU.h" #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaAVR.h" @@ -5485,7 +5486,7 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { SmallVector Names, Designators; SmallVector ArgumentCounts, ArgumentKinds; SmallVector ArgumentKeys, ArgumentValues; - if (!S.processProfilesEnforceAttr(AL, Mod, &Names, &Designators, + if (!S.Profiles().processProfilesEnforceAttr(AL, Mod, &Names, &Designators, &ArgumentCounts, &ArgumentKeys, &ArgumentValues, &ArgumentKinds)) return; @@ -5503,7 +5504,7 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { static void handleProfilesSuppressDeclAttr(Sema &S, Decl *D, const ParsedAttr &AL) { - if (auto *A = S.makeProfilesSuppressAttr(AL)) + if (auto *A = S.Profiles().makeProfilesSuppressAttr(AL)) D->addAttr(A); } @@ -6969,7 +6970,7 @@ static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // std::init / union_marker + pointer_marker (paper §4.1, §5.6). Shared with // the template-instantiation re-check sites (VisitFieldDecl / VisitVarDecl), // since this handler only runs on the pattern. - S.diagnoseInitUninitMarkerPlacement(D); + S.Profiles().diagnoseInitUninitMarkerPlacement(D); } static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index b0bfc0fe6ec59..5033aece1dc41 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -44,6 +44,7 @@ #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenMP.h" #include "clang/Sema/Template.h" @@ -4231,12 +4232,14 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, // the field and its lexical parents. This does not depend on a parse-time // suppress scope still being active (the late-parsed NSDMI finishes parsing // before this finalization runs). - checkInitProfileUninitWithInitializer(FD, FD->getInClassInitializer()); + Profiles().checkInitProfileUninitWithInitializer( + FD, FD->getInClassInitializer()); // std::init / ref_to_uninit (paper §5): a pointer or reference data member // with a default member initializer must be bound consistently with its // [[ref_to_uninit]] marking. - checkRefToUninitBinding(FD->getLocation(), FD, FD->getType(), + Profiles().checkRefToUninitBinding(FD->getLocation(), FD, FD->getType(), + FD->getInClassInitializer(), FD); } @@ -4685,7 +4688,8 @@ Sema::BuildMemberInitializer(ValueDecl *Member, Expr *Init, // BuildMemberInitializer re-runs with the instantiated constructor as // CurContext, matching ctor_uninit_member. if (auto *Ctor = dyn_cast(CurContext)) - checkRefToUninitBinding(IdLoc, Member, Member->getType(), Init, Ctor); + Profiles().checkRefToUninitBinding(IdLoc, Member, Member->getType(), + Init, Ctor); } if (DirectMember) { @@ -5942,7 +5946,7 @@ void Sema::ActOnMemInitializers(Decl *ConstructorDecl, DiagnoseUninitializedFields(*this, Constructor); - checkProfileViolationsAtConstructorFinalization(Constructor); + Profiles().checkProfileViolationsAtConstructorFinalization(Constructor); } void Sema::MarkBaseAndMemberDestructorsReferenced(SourceLocation Location, @@ -6009,7 +6013,7 @@ void Sema::ActOnDefaultCtorInitializers(Decl *CDtorDecl) { } SetCtorInitializers(Constructor, /*AnyErrors=*/false); DiagnoseUninitializedFields(*this, Constructor); - checkProfileViolationsAtConstructorFinalization(Constructor); + Profiles().checkProfileViolationsAtConstructorFinalization(Constructor); } } @@ -7046,151 +7050,6 @@ ReportOverrides(Sema &S, unsigned DiagID, const CXXMethodDecl *MD, return IssuedDiagnostic; } -namespace { -// Row for the unified finalization dispatch shared by class-finalization -// (pattern 3) and constructor-finalization (pattern 4): a profile name plus a -// callback invoked once per finalized, non-dependent, non-invalid Node (a -// CXXRecordDecl or a CXXConstructorDecl). Adding a new profile is a single row -// in the matching table below plus a ProfileRuleError diagnostic in -// DiagnosticSemaKinds.td and a callback that consults -// Sema::shouldEmitProfileViolation before emitting. -template struct FinalizationProfile { - StringRef Name; - void (*Callback)(Sema &, Node *); -}; - -void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { - if (!S.shouldEmitProfileViolation("test::class_final", /*Rule=*/"", - RD->getLocation(), RD)) - return; - S.Diag(RD->getLocation(), diag::err_profile_class_final_test) - << "test::class_final" << RD; -} - -void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { - if (!S.shouldEmitProfileViolation("test::ctor_final", /*Rule=*/"", - Ctor->getLocation(), Ctor)) - return; - S.Diag(Ctor->getLocation(), diag::err_profile_ctor_final_test) - << "test::ctor_final" << Ctor->getParent(); -} - -void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { - // Paper §6.1: a user-provided constructor must initialize every member via - // its member-initializer list or an NSDMI, unless the member is marked - // [[uninit]] (whose body initialization is the deferred R7 check). - // A plain assignment in the constructor body does not count. - if (!Ctor->isUserProvided()) - return; - - // A union's members are mutually exclusive; a constructor initializes at most - // one, so the "every member" rule does not apply (paper §6.5). Whether the - // active member is set is a constructor-body flow question, deferred. - if (Ctor->getParent()->isUnion()) - return; - - // Members and direct bases given a written initializer by this constructor. - llvm::SmallPtrSet Written; - llvm::SmallPtrSet WrittenBases; - for (const CXXCtorInitializer *Init : Ctor->inits()) { - if (!Init->isWritten()) - continue; - if (Init->isAnyMemberInitializer()) { - if (const FieldDecl *F = Init->getAnyMember()) - Written.insert(F); - } else if (Init->isBaseInitializer()) { - if (const Type *T = Init->getBaseClass()) - WrittenBases.insert( - S.Context.getCanonicalType(QualType(T, 0)).getTypePtr()); - } - } - - for (const FieldDecl *F : Ctor->getParent()->fields()) { - // Anonymous aggregate members and unnamed bit-fields are skipped; a named - // bit-field is checked like any other member. Reference and const members - // already have dedicated diagnostics when left uninitialized. - if (F->isUnnamedBitField() || !F->getDeclName() || - F->getType()->isReferenceType() || F->getType().isConstQualified()) - continue; - if (F->hasAttr() || F->hasInClassInitializer() || - Written.count(F)) - continue; - if (!S.defaultInitLeavesScalarIndeterminate(F->getType(), - /*HonorUninitMarkers=*/true)) - continue; - if (!S.shouldEmitProfileViolation("std::init", "ctor_uninit_member", - Ctor->getLocation(), Ctor)) - continue; - S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) - << "std::init" << F->getDeclName(); - S.Diag(F->getLocation(), diag::note_init_uninit_member_here) - << F->getDeclName(); - } - - // The guarantee is over the complete object (paper §5.1, §7.1), so a - // direct base-class subobject left indeterminate is as much a violation as a - // member. A base cannot carry an [[uninit]] marker (the attribute's subjects - // are Var/Field), so an indeterminate base must always be initialized -- there - // is no marker escape. Virtual bases are the most-derived constructor's - // responsibility, not a local property of this constructor, so they are - // deferred. A written base-initializer initializes the base; an implicit - // (non-written) one is default-init, handled by the indeterminate check. - for (const CXXBaseSpecifier &Base : Ctor->getParent()->bases()) { - if (Base.isVirtual()) - continue; - if (WrittenBases.count( - S.Context.getCanonicalType(Base.getType()).getTypePtr())) - continue; - if (!S.defaultInitLeavesScalarIndeterminate(Base.getType(), - /*HonorUninitMarkers=*/true)) - continue; - if (!S.shouldEmitProfileViolation("std::init", "ctor_uninit_member", - Ctor->getLocation(), Ctor)) - continue; - S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_base) - << "std::init" << Base.getType(); - S.Diag(Base.getBeginLoc(), diag::note_init_uninit_base_here) - << Base.getType(); - } -} - -// Class-finalization opt-in table (pattern 3). -constexpr FinalizationProfile ClassFinalizationProfiles[] = { - {"test::class_final", &runTestClassFinalCallback}, -}; - -// Constructor-finalization opt-in table (pattern 4). -constexpr FinalizationProfile - ConstructorFinalizationProfiles[] = { - {"test::ctor_final", &runTestCtorFinalCallback}, - {"std::init", &runStdInitCtorUninitMemberCallback}, -}; - -// Run the enforced finalization-profile callbacks in Table for D. Merges the -// former per-node dispatchers; the per-node filter (dependent, lambda, -// delegating, ...) stays at each call site. Each callback passes D to the -// Decl-aware Sema::shouldEmitProfileViolation, which honors [[profiles::suppress]] -// on D or a lexical parent, so the dispatcher needs no suppress scope of its -// own. The table is taken by reference-to-array, not ArrayRef: deducing Node -// from a C array against an ArrayRef> parameter is not -// possible (no array-to-ArrayRef conversion happens during template argument -// deduction). -template -void dispatchFinalizationProfiles(Sema &S, Node *D, - const FinalizationProfile (&Table)[N]) { - if (!S.anyProfileEnforced(Table)) - return; - // Finalization can run nested in an unrelated instantiation whose - // [[profiles::suppress]] scope is still on the parse-time stack; the callbacks - // must resolve suppression only from D and its lexical parents, not that - // transient stack (P3589R2 s2.4p3). - llvm::SaveAndRestore InFinalization(S.InProfileFinalizationCheck, true); - for (const auto &E : Table) - if (S.isProfileEnforced(E.Name)) - E.Callback(S, D); -} -} // namespace - void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { if (!Record) return; @@ -7611,27 +7470,7 @@ void Sema::CheckCompletedCXXClass(Scope *S, CXXRecordDecl *Record) { CheckMismatchedTypeAwareAllocators(OO_New, OO_Delete); CheckMismatchedTypeAwareAllocators(OO_Array_New, OO_Array_Delete); - checkProfileViolationsAtClassFinalization(Record); -} - -void Sema::checkProfileViolationsAtClassFinalization(CXXRecordDecl *RD) { - if (!getLangOpts().Profiles || !RD) - return; - if (RD->isInvalidDecl() || RD->isDependentType() || RD->isLambda()) - return; - dispatchFinalizationProfiles(*this, RD, ClassFinalizationProfiles); -} - -void Sema::checkProfileViolationsAtConstructorFinalization( - CXXConstructorDecl *Ctor) { - if (!getLangOpts().Profiles || !Ctor) - return; - // A dependent constructor pattern re-fires on instantiation; a delegating - // constructor leaves member initialization to its target. - if (Ctor->isInvalidDecl() || Ctor->isDependentContext() || - Ctor->isDelegatingConstructor()) - return; - dispatchFinalizationProfiles(*this, Ctor, ConstructorFinalizationProfiles); + Profiles().checkProfileViolationsAtClassFinalization(Record); } /// Look up the special member function that would be called by a special diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index fba589e4997ba..7f8b69310afb1 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -53,6 +53,7 @@ #include "clang/Sema/ParsedTemplate.h" #include "clang/Sema/Scope.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaFixItUtils.h" @@ -742,7 +743,7 @@ ExprResult Sema::DefaultLvalueConversion(Expr *E) { // lvalue-to-rvalue chokepoint that by-value reads (copy-init, by-value // arguments, returns, operator operands) all funnel through. if (getLangOpts().Profiles) - checkRefToUninitRead(E->getExprLoc(), E, T); + Profiles().checkRefToUninitRead(E->getExprLoc(), E, T); // C++ [conv.lval]p3: // If T is cv std::nullptr_t, the result is a null pointer constant. @@ -6288,7 +6289,8 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, const Expr *Src = Arg; if (const auto *DAE = dyn_cast(Src)) Src = DAE->getExpr(); - checkRefToUninitBinding(Arg->getExprLoc(), Param, Param->getType(), Src); + Profiles().checkRefToUninitBinding(Arg->getExprLoc(), Param, + Param->getType(), Src); } // Check for array bounds violations for each argument to the call. This @@ -15509,8 +15511,9 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, // unmarked pointer (paper §4.3) and must not be bound to uninitialized // memory. if (getLangOpts().Profiles && LHS.get()->getType()->isPointerType()) { - const ValueDecl *VD = getDirectlyNamedDecl(LHS.get()); - checkRefToUninitInit(OpLoc, VD && VD->hasAttr(), + const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(LHS.get()); + Profiles().checkRefToUninitInit( + OpLoc, VD && VD->hasAttr(), /*IsReference=*/false, RHS.get()); } diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 117ec167eabf9..34383b88d38c5 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -30,6 +30,7 @@ #include "clang/Sema/Ownership.h" #include "clang/Sema/SemaHLSL.h" #include "clang/Sema/SemaObjC.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/APInt.h" #include "llvm/ADT/DenseMap.h" #include "llvm/ADT/FoldingSet.h" @@ -1604,7 +1605,7 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, // stack. (A reference field is routed to CheckReferenceType above.) if (!Result.isInvalid() && Entity.getKind() == InitializedEntity::EK_Member) - SemaRef.checkRefToUninitBinding(expr->getExprLoc(), + SemaRef.Profiles().checkRefToUninitBinding(expr->getExprLoc(), Entity.getDecl(), ElemType, expr); UpdateStructuredListElement(StructuredList, StructuredIndex, @@ -1915,8 +1916,8 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, // checkRefToUninitInit and suppression from the parse-time stack. if (!VerifyOnly && !Result.isInvalid() && Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent()) - SemaRef.checkRefToUninitBinding(Src->getExprLoc(), Entity.getDecl(), - DeclType, Src); + SemaRef.Profiles().checkRefToUninitBinding(Src->getExprLoc(), + Entity.getDecl(), DeclType, Src); UpdateStructuredListElement(StructuredList, StructuredIndex, expr); ++Index; diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index 803c14b78dc61..abe00e92a857c 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -24,6 +24,7 @@ #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaOpenMP.h" #include "clang/Sema/SemaSYCL.h" #include "clang/Sema/Template.h" @@ -1506,9 +1507,10 @@ void Sema::ActOnStartOfLambdaDefinition(LambdaIntroducer &Intro, // that generic lambda instantiation (which walks lexical Decl parents, not // the enclosing stmt tree) can recover them. if (getLangOpts().Profiles) - for (const auto &E : ProfileSuppressStack) + for (const auto &E : Profiles().ProfileSuppressStack) Method->addAttr( - makeImplicitProfilesSuppressAttr(E.ProfileName, E.RuleName)); + Profiles().makeImplicitProfilesSuppressAttr(E.ProfileName, + E.RuleName)); if (Context.getTargetInfo().getTriple().isAArch64()) ARM().CheckSMEFunctionDefAttributes(Method); diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index fc38213709ec9..ca99eb08732bb 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -18,6 +18,7 @@ #include "clang/Lex/Preprocessor.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" @@ -469,7 +470,7 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, for (const auto &AL : Attrs) if (AL.getKind() == ParsedAttr::AT_ProfilesEnforce && AL.diagnoseLangOpts(*this)) - processProfilesEnforceAttr(AL, ExportMod, nullptr, nullptr); + Profiles().processProfilesEnforceAttr(AL, ExportMod, nullptr, nullptr); } // We are in the module purview, but before any other (non import) @@ -485,7 +486,8 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, // implementation unit. if (Interface) { for (const auto &EP : Interface->EnforcedProfileDesignators) - addProfileEnforcement(EP.ProfileName, EP.Designator, ModuleLoc); + Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, + ModuleLoc); } else if (getLangOpts().Profiles && MDK == ModuleDeclKind::PartitionImplementation) { // A partition implementation unit is a module implementation unit of M, so @@ -499,7 +501,8 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, if (Module *Primary = PP.getHeaderSearchInfo().getModuleMap().findModule( Mod->getPrimaryModuleInterfaceName())) for (const auto &EP : Primary->EnforcedProfileDesignators) - addProfileEnforcement(EP.ProfileName, EP.Designator, ModuleLoc); + Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, + ModuleLoc); } // We already potentially made an implicit import (in the case of a module diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp new file mode 100644 index 0000000000000..e08e19c851069 --- /dev/null +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -0,0 +1,1027 @@ +//===----- SemaProfiles.cpp --- C++ profiles framework --------*- C++ -*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +/// \file +/// This file implements semantic analysis for the C++ profiles framework +/// (P3589R2) and the built-in std::init initialization profile (P4222R1.1): +/// profile enforcement and suppression state, the shared violation gate, and +/// the parse-time std::init rule checks. The CFG-based std::init checks live +/// in AnalysisBasedWarnings.cpp. +/// +//===----------------------------------------------------------------------===// + +#include "clang/Sema/SemaProfiles.h" +#include "clang/AST/Attr.h" +#include "clang/AST/DeclCXX.h" +#include "clang/AST/ExprCXX.h" +#include "clang/AST/ParentMap.h" +#include "clang/Analysis/AnalysisDeclContext.h" +#include "clang/Basic/Module.h" +#include "clang/Sema/Attr.h" +#include "clang/Sema/ParsedAttr.h" +#include "clang/Sema/Sema.h" +#include "llvm/Support/SaveAndRestore.h" + +using namespace clang; + +SemaProfiles::SemaProfiles(Sema &S) : SemaBase(S) {} + + +bool SemaProfiles::isProfileEnforced(StringRef ProfileName) const { + if (!getLangOpts().Profiles) + return false; + // The built-in test:: profiles only exercise the framework; keep them inert + // unless the test suite opts in via -fprofiles-test-profiles. + if (!getLangOpts().ProfilesTestProfiles && ProfileName.starts_with("test::")) + return false; + return getProfileEnforcement(ProfileName) != nullptr; +} + +const SemaProfiles::ProfileEnforcement * +SemaProfiles::getProfileEnforcement(StringRef ProfileName) const { + for (const auto &E : EnforcedProfiles) + if (E.ProfileName == ProfileName) + return &E; + return nullptr; +} + +bool SemaProfiles::addProfileEnforcement(StringRef Name, StringRef Designator, + SourceLocation Loc) { + if (const auto *Existing = getProfileEnforcement(Name)) { + if (Existing->Designator != Designator) { + Diag(Loc, diag::err_profiles_enforce_mismatch) << Name; + Diag(Existing->EnforceLoc, diag::note_previous_attribute); + return false; + } + return true; + } + EnforcedProfiles.push_back({{Name.str(), Designator.str()}, Loc}); + return true; +} + +// Unzip profile arguments into the parallel key/value/kind arrays that the +// semantic attributes store (Attr.td cannot hold structured arguments). +static void unzipProfileArguments(ArrayRef Arguments, + SmallVectorImpl &Keys, + SmallVectorImpl &Values, + SmallVectorImpl &Kinds) { + for (const auto &Arg : Arguments) { + Keys.push_back(Arg.Key); + Values.push_back(Arg.Value); + Kinds.push_back(static_cast(Arg.Kind)); + } +} + +static void appendProfileArgumentData( + ArrayRef Arguments, + SmallVectorImpl *ArgumentCounts, + SmallVectorImpl *ArgumentKeys, + SmallVectorImpl *ArgumentValues, + SmallVectorImpl *ArgumentKinds) { + if (!ArgumentCounts) + return; + + assert(ArgumentKeys && ArgumentValues && ArgumentKinds); + ArgumentCounts->push_back(Arguments.size()); + unzipProfileArguments(Arguments, *ArgumentKeys, *ArgumentValues, + *ArgumentKinds); +} + +bool SemaProfiles::processProfilesEnforceAttr( + const ParsedAttr &AL, Module *Mod, SmallVectorImpl *NewNames, + SmallVectorImpl *NewDesignators, + SmallVectorImpl *NewArgumentCounts, + SmallVectorImpl *NewArgumentKeys, + SmallVectorImpl *NewArgumentValues, + SmallVectorImpl *NewArgumentKinds) { + const auto &Args = AL.getProfileEnforceArgs(); + if (Args.Designators.empty()) { + Diag(AL.getLoc(), diag::err_attribute_too_few_arguments) << AL << 1; + return false; + } + + for (const auto &D : Args.Designators) { + StringRef Name = D.Name; + StringRef Spelling = D.Spelling; + + bool IsNew = !isProfileEnforced(Name); + if (!addProfileEnforcement(Name, Spelling, AL.getLoc())) + continue; + + if (Mod && !llvm::any_of(Mod->EnforcedProfileDesignators, + [&](const Module::EnforcedProfile &EP) { + return EP.ProfileName == Name; + })) + Mod->EnforcedProfileDesignators.push_back({Name.str(), Spelling.str()}); + + if (IsNew) { + if (NewNames) + NewNames->push_back(Name); + if (NewDesignators) + NewDesignators->push_back(Spelling); + appendProfileArgumentData(D.Arguments, NewArgumentCounts, + NewArgumentKeys, NewArgumentValues, + NewArgumentKinds); + } + } + return true; +} + +ProfilesSuppressAttr * +SemaProfiles::makeProfilesSuppressAttr(const ParsedAttr &AL) { + const auto &Args = AL.getProfileSuppressArgs(); + if (Args.Name.empty()) + return nullptr; + + SmallVector RawArgs; + for (const auto &Arg : Args.RawArguments) + RawArgs.push_back(Arg); + SmallVector RawArgumentKeys; + SmallVector RawArgumentValues; + SmallVector RawArgumentKinds; + unzipProfileArguments(Args.Arguments, RawArgumentKeys, RawArgumentValues, + RawArgumentKinds); + + return ::new (getASTContext()) ProfilesSuppressAttr( + getASTContext(), AL, Args.Name, Args.Justification, Args.Rule, + RawArgs.data(), RawArgs.size(), RawArgumentKeys.data(), + RawArgumentKeys.size(), RawArgumentValues.data(), + RawArgumentValues.size(), RawArgumentKinds.data(), + RawArgumentKinds.size()); +} + +ProfilesSuppressAttr * +SemaProfiles::makeImplicitProfilesSuppressAttr(StringRef ProfileName, + StringRef RuleName) { + return ProfilesSuppressAttr::CreateImplicit( + getASTContext(), ProfileName, /*Justification=*/"", RuleName, + /*RawArguments=*/nullptr, /*RawArgumentsSize=*/0, + /*RawArgumentKeys=*/nullptr, /*RawArgumentKeysSize=*/0, + /*RawArgumentValues=*/nullptr, /*RawArgumentValuesSize=*/0, + /*RawArgumentKinds=*/nullptr, /*RawArgumentKindsSize=*/0); +} + +static bool profileSuppressMatches(StringRef EntryProfile, StringRef EntryRule, + StringRef Profile, StringRef Rule) { + return EntryProfile == Profile && + (EntryRule.empty() || EntryRule == Rule); +} + +bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName) const { + for (const auto &E : ProfileSuppressStack) + if (profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, + RuleName)) + return true; + return false; +} + +bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName, + const Decl *D) const { + for (; D;) { + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + const DeclContext *DC = D->getLexicalDeclContext(); + D = DC ? dyn_cast(DC) : nullptr; + } + return false; +} + +bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, + StringRef RuleName, const Stmt *S, + AnalysisDeclContext &AC) const { + ParentMap &PM = AC.getParentMap(); + for (const Stmt *Cur = S; Cur; Cur = PM.getParent(Cur)) { + if (const auto *AS = dyn_cast(Cur)) + for (const Attr *A : AS->getAttrs()) + if (const auto *PSA = dyn_cast(A)) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + // [[profiles::suppress]] on a local variable attaches to the VarDecl, + // not the enclosing DeclStmt. Walk the declared decls so the post-parse + // walker matches the parse-time ProfileSuppressForInit RAII behavior. + if (const auto *DS = dyn_cast(Cur)) + for (const Decl *D : DS->decls()) + for (const auto *PSA : D->specific_attrs()) + if (profileSuppressMatches(PSA->getProfileName(), PSA->getRule(), + ProfileName, RuleName)) + return true; + } + return isProfileSuppressed(ProfileName, RuleName, AC.getDecl()); +} + +bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, + StringRef RuleName, + SourceLocation Loc) { + return shouldEmitProfileViolation(ProfileName, RuleName, Loc, /*D=*/nullptr); +} + +bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, + StringRef RuleName, + SourceLocation Loc, + const Decl *D) { + if (!isProfileEnforced(ProfileName)) + return false; + // Honor [[profiles::suppress]] from the parse-time stack and, when a Decl is + // available, from the declaration and its lexical parents. The latter does + // not depend on a parse-time scope still being active, so finalization checks + // that run after the parse scope is torn down still respect suppression. + // + // Finalization callbacks skip the parse-time stack: they can fire while an + // unrelated entity's instantiation ProfileSuppressScope is still active, and + // that scope does not lexically enclose the finalized declaration (token- + // based dominion, P3589R2 s2.4p3). Their decl-aware walk already covers a + // suppression on the declaration or a lexical parent. + if ((!InProfileFinalizationCheck && + isProfileSuppressed(ProfileName, RuleName)) || + isProfileSuppressed(ProfileName, RuleName, D)) + return false; + // P3589R2 Section 1.1: "its static semantic effects are as-if applied only + // after translation phase 7. It is not possible for a profile to change the + // outcome of overload resolution or template instantiation, nor is it + // possible to 'SFINAE out' failure of a program to satisfy a profile + // requirement." + // + // A templated entity is not yet a phase-7 entity, so a profile rule must fire + // only on its instantiation -- where D is the instantiated, non-templated + // declaration -- not on the template pattern. Checking the pattern too would + // diagnose never-instantiated templates and double-fire (once when the + // pattern is parsed and again at each instantiation). + // + // Decl-less expression check sites whose Build* routine is re-run at + // instantiation (the ref_to_uninit binding checks: call argument, pointer + // assignment, return) instead defer in a dependent context from their own + // wrapper, checkRefToUninitInit, since no Decl is available here. The + // [[uninit]] marker checks pass D (via diagnoseInitUninitMarkerPlacement) and + // are re-run on the instantiated field / variable, so they defer here too. + // The reinterpret_cast check still passes D == nullptr and is not re-checked + // at instantiation, so it keeps running once at parse time (a separate gap). + if (D && D->isTemplated()) + return false; + if (SemaRef.isUnevaluatedContext()) + return false; + if (SemaRef.currentEvaluationContext().isDiscardedStatementContext()) + return false; + return true; +} + +bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, + StringRef RuleName, + const Stmt *UseStmt, + AnalysisDeclContext &AC) const { + if (!isProfileEnforced(ProfileName)) + return false; + if (isProfileSuppressed(ProfileName, RuleName, UseStmt, AC)) + return false; + return true; +} + +bool SemaProfiles::checkProfileViolation(StringRef ProfileName, + StringRef RuleName, SourceLocation Loc, + unsigned DiagID) { + if (!shouldEmitProfileViolation(ProfileName, RuleName, Loc)) + return false; + Diag(Loc, DiagID) << ProfileName; + return true; +} + +void SemaProfiles::ProfileSuppressScope::push(StringRef ProfileName, + StringRef RuleName) { + S.Profiles().ProfileSuppressStack.push_back({ProfileName, RuleName}); + ++Count; +} + +SemaProfiles::ProfileSuppressScope::ProfileSuppressScope( + Sema &S, const ParsedAttributesView &Attrs) + : S(S) { + if (!S.getLangOpts().Profiles) + return; + for (const auto &AL : Attrs) { + if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) + continue; + const auto &Args = AL.getProfileSuppressArgs(); + if (!Args.Name.empty()) + push(Args.Name, Args.Rule); + } +} + +void SemaProfiles::ProfileSuppressScope::addFromDecl(const Decl *D) { + for (const auto *A : D->specific_attrs()) + push(A->getProfileName(), A->getRule()); +} + +SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, + bool WalkLexicalParents) + : S(S) { + if (!S.getLangOpts().Profiles || !D) + return; + addFromDecl(D); + if (WalkLexicalParents) { + for (const DeclContext *DC = D->getLexicalDeclContext(); DC; + DC = DC->getLexicalParent()) + if (const auto *Parent = dyn_cast(DC)) + addFromDecl(Parent); + } +} + +SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, + ArrayRef Attrs) + : S(S) { + if (!S.getLangOpts().Profiles) + return; + for (const auto *A : Attrs) + if (const auto *PSA = dyn_cast(A)) + push(PSA->getProfileName(), PSA->getRule()); +} + +SemaProfiles::ProfileSuppressScope::~ProfileSuppressScope() { + assert(S.Profiles().ProfileSuppressStack.size() >= Count); + S.Profiles().ProfileSuppressStack.pop_back_n(Count); +} + +static bool defaultInitLeavesScalarIndeterminateImpl( + ASTContext &Ctx, QualType T, bool HonorUninitMarkers, + llvm::SmallPtrSetImpl &Visited) { + if (T->isDependentType() || T->isIncompleteType()) + return false; + if (const ArrayType *AT = Ctx.getAsArrayType(T)) + return defaultInitLeavesScalarIndeterminateImpl( + Ctx, AT->getElementType(), HonorUninitMarkers, Visited); + if (T->isReferenceType()) + return false; + const auto *RD = T->getAsCXXRecordDecl(); + if (!RD) + // Scalars, pointers, and enums are left indeterminate by default-init, + // except std::byte, which the profile permits to be uninitialized + // (paper §4), so a std::byte subobject does not make a record + // indeterminate. + return T->isScalarType() && !T->isStdByteType(); + if (RD->isInvalidDecl()) + return false; + // A union's members are mutually exclusive, so the per-member walk below does + // not apply. Default-initialization leaves it without an initialized member + // (paper §6.5) unless it has no members, has a user-provided default + // constructor (trusted), or a default member initializer initializes one. + if (RD->isUnion()) { + if (RD->field_empty() || RD->hasUserProvidedDefaultConstructor()) + return false; + for (const FieldDecl *F : RD->fields()) + if (F->hasInClassInitializer()) + return false; + return true; + } + // Break cycles from ill-formed self-containing types (e.g. struct S {S x;}). + if (!Visited.insert(RD->getCanonicalDecl()).second) + return false; + // Trust a user-provided default constructor: ctor_uninit_member checks at its + // definition. + if (RD->hasUserProvidedDefaultConstructor()) + return false; + for (const CXXBaseSpecifier &Base : RD->bases()) + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, Base.getType(), + HonorUninitMarkers, Visited)) + return true; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField() || F->hasInClassInitializer()) + continue; + // A member the type's author marked [[uninit]] is acknowledged as + // intentionally uninitialized, so it does not leave an unacknowledged + // scalar indeterminate (paper §6.2). + if (HonorUninitMarkers && F->hasAttr()) + continue; + if (defaultInitLeavesScalarIndeterminateImpl(Ctx, F->getType(), + HonorUninitMarkers, Visited)) + return true; + } + return false; +} + +bool SemaProfiles::defaultInitLeavesScalarIndeterminate(QualType T, + bool HonorUninitMarkers) { + llvm::SmallPtrSet Visited; + return defaultInitLeavesScalarIndeterminateImpl(getASTContext(), T, + HonorUninitMarkers, Visited); +} + +void SemaProfiles::checkInitProfileUninitDecl(const VarDecl *Var) { + // std::init / uninit_decl: a definition without any initializer (after + // attempted default-initialization) must either carry [[uninit]] or + // be initialized by a language rule. Static / thread storage duration is + // excluded -- those are zero-initialized; runtime-init concerns are R3's. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_decl"; + // The enforcement check gates the (possibly recursive) type walk below so + // it runs only under the profile, not on every default-initialized + // variable. + QualType BaseTy = getASTContext().getBaseElementType(Var->getType()); + if (!Var->isInvalidDecl() && Var->getStorageDuration() == SD_Automatic && + !Var->hasAttr() && + // std::byte may be left uninitialized (paper §4), so it -- and arrays + // of it -- are exempt from this rule. + !BaseTy->isStdByteType() && + shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var) && + // A definition with no initializer (scalar / pointer / enum, or an + // array of them), or a class/aggregate type -- possibly the element + // type of an array -- whose default-init leaves a scalar subobject + // indeterminate (its synthesized constructor call provides an + // initializer, so the !getInit() test alone misses it). + (!Var->getInit() || + (BaseTy->isRecordType() && + defaultInitLeavesScalarIndeterminate(Var->getType(), + /*HonorUninitMarkers=*/true)))) { + // A union variable cannot carry [[uninit]] (union_marker bans it), + // so it must be initialized; use a message that does not suggest the + // marker as a remedy. + bool IsUnion = BaseTy->isUnionType(); + Diag(Var->getLocation(), + IsUnion ? diag::err_init_uninit_union : diag::err_init_uninit_decl) + << Profile << Var->getDeclName(); + } +} + +void SemaProfiles::checkInitProfileStaticMarker(const VarDecl *Var) { + // std::init / static_marker: a variable with static or thread storage + // duration is zero-initialized by language rule (paper §3), so it is an + // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an + // initialized object marked [[uninit]] is an error"). The case with a real + // initializer -- explicit, or a constructor that actually runs -- is + // already caught by uninit_with_initializer (R4, in + // CheckCompleteVariableDeclaration); this covers the zero-initialized, + // no-real-initializer case R4 treats as a consistent no-op. The factual + // (HonorUninitMarkers=false) walk matches R4: a static object whose only + // indeterminate scalars are themselves marked is still zero-initialized. + static constexpr StringRef Profile = "std::init"; + QualType BaseTy = getASTContext().getBaseElementType(Var->getType()); + if (!Var->isInvalidDecl() && + (Var->getStorageDuration() == SD_Static || + Var->getStorageDuration() == SD_Thread) && + Var->hasAttr() && + // A union or pointer object marked [[uninit]] is already rejected by + // union_marker / pointer_marker (regardless of storage duration), and + // they retain the marker; do not pile a second diagnostic on top. + !Var->getType()->isUnionType() && !Var->getType()->isPointerType() && + shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), + Var) && + (!Var->getInit() || + (BaseTy->isRecordType() && + defaultInitLeavesScalarIndeterminate(Var->getType(), + /*HonorUninitMarkers=*/false)))) { + bool IsThread = Var->getStorageDuration() == SD_Thread; + Diag(Var->getLocation(), diag::err_init_uninit_static_marker) + << Profile << Var->getDeclName() << IsThread; + } +} + +bool SemaProfiles::checkInitProfileStaticRuntimeInit( + const VarDecl *Var, llvm::function_ref CheckConstInit) { + // Thread-locals have thread (not static) storage duration; paper §3 scopes + // this rule to non-local *static* objects (uninit_decl likewise excludes + // thread storage). + if (Var->getTLSKind() != VarDecl::TLS_None) + return false; + // std::init / static_runtime_init: paper says non-local statics must be + // initialized at compile or link time. CheckConstInit() permits trivial + // default initialization (not a constant initializer but needs no global + // constructor), so a zero-initialized aggregate such as + // `struct S { int x; }; S g;` is not a violation. Runs before + // -Wglobal-constructors so the profile error (when enforced) takes + // precedence over the standalone warning. + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "static_runtime_init"; + if (CheckConstInit()) + return false; + if (!shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var)) + return false; + Diag(Var->getLocation(), diag::err_init_static_runtime_init) + << Profile << Var->getDeclName(); + return true; +} + +void SemaProfiles::checkInitProfileUninitWithInitializer(const ValueDecl *D, + const Expr *Init) { + // [[uninit]] documents that the entity is intentionally left + // uninitialized, so it contradicts an explicit initializer. A RecoveryExpr + // is a placeholder for an initialization that already failed (e.g. + // default-init of a const scalar), not an initializer the user wrote, so it + // must not trigger this rule. + if (!D->hasAttr() || !Init || + isa(Init->IgnoreParens())) + return; + SourceLocation Loc = D->getLocation(); + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "uninit_with_initializer"; + // Gate the (possibly recursive) type walk below on enforcement. + if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) + return; + // A synthesized default-initialization that leaves the object indeterminate + // (a trivial or aggregate type, no user-written initializer) is consistent + // with the marker: the object really is left uninitialized, to be + // initialized later (e.g. via construct_at), mirroring the scalar case. Only + // a real initializer -- an explicit one, or a constructor that actually runs + // -- contradicts the marker. + if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) + if (CCE->getConstructor()->isDefaultConstructor() && + defaultInitLeavesScalarIndeterminate(D->getType())) + return; + Diag(Loc, diag::err_init_uninit_with_initializer) + << Profile << D->getDeclName(); +} + +void SemaProfiles::diagnoseInitUninitMarkerPlacement(const Decl *D) { + const auto *UA = D->getAttr(); + if (!UA) + return; + SourceLocation Loc = UA->getLocation(); + + // std::init / union_marker (paper §5.6): the marker is banned on a union + // object or a union member, because delayed initialization by assigning a + // member would be an erroneous assignment when compiled without the profile. + // std::init / pointer_marker (paper §4.1): "a reference cannot be + // uninitialized. The initialization profile requires the same for pointers." + // A pointer must instead be initialized (e.g. to nullptr). Both are profile + // policy (not a meaningless subject), so they are gated on enforcement; the + // marker is left in place so uninit_decl / ctor_uninit_member treat the + // entity as acknowledged rather than re-diagnosing it. + // + // Passing \p D makes shouldEmitProfileViolation defer on a templated pattern + // (paper / P3589R2: a rule fires on the instantiation, not the template), + // so the parse-time handler skips template members and the rule is re-run on + // the instantiated entity (VisitFieldDecl / VisitVarDecl), once the + // substituted type is known to be a pointer or union. + bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); + bool UnionMember = + isa(D) && cast(D)->getParent()->isUnion(); + if ((UnionVar || UnionMember) && + shouldEmitProfileViolation("std::init", "union_marker", Loc, D)) + Diag(Loc, diag::err_init_union_marker) + << "std::init" << (UnionMember ? 1 : 0); + else if (cast(D)->getType()->isPointerType() && + shouldEmitProfileViolation("std::init", "pointer_marker", Loc, D)) + Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; +} + +// std::init / ref_to_uninit (paper §5). Two mutually-recursive local +// recognizers over the syntactic form of a source expression -- no flow +// analysis and no type-system tracking. Uninitialized storage is only ever +// introduced by an explicit [[uninit]] / [[ref_to_uninit]] marker. +// +// The classification is tri-state: a recognized form is Initialized or +// Uninitialized, while an unrecognized one (pointer arithmetic, an +// integer-to-pointer cast, a call through a function pointer) is Unknown rather +// than assumed Initialized. Callers wanting a plain "is it uninitialized?" +// answer (SemaProfiles::refersToUninitializedMemory, the read-through check) +// treat +// Unknown as not uninitialized. +// +// \p ForRead selects read-through mode (SemaProfiles::checkRefToUninitRead): a +// directly named [[uninit]] object is then not treated as uninitialized, +// because a read of such a named object is the flow-based uninit_read pass's +// responsibility; only indirection through a [[ref_to_uninit]] +// pointer/reference still counts. +enum class UninitStorage { Initialized, Uninitialized, Unknown }; + +// Combine the arms of a conditional: Uninitialized dominates (either arm may be +// taken), then Unknown, else Initialized. +static UninitStorage combineArms(UninitStorage A, UninitStorage B) { + if (A == UninitStorage::Uninitialized || B == UninitStorage::Uninitialized) + return UninitStorage::Uninitialized; + if (A == UninitStorage::Unknown || B == UninitStorage::Unknown) + return UninitStorage::Unknown; + return UninitStorage::Initialized; +} + +static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + bool ForRead = false); + +const ValueDecl *SemaProfiles::getDirectlyNamedDecl(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (const auto *DRE = dyn_cast(E)) + return DRE->getDecl(); + if (const auto *ME = dyn_cast(E)) + return ME->getMemberDecl(); + return nullptr; +} + +// Pass-through forms shared by the pointer and glvalue recognizers, which are +// transparent to their operand: a single-element braced initializer { e } +// binds from e (modeling +// MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); a conditional +// is uninit if either arm is, so a value that may be uninit forces a marked +// target; a comma yields its right operand. \p EmptyListState classifies an +// empty braced list: {} value-initializes a pointer to null (Initialized), +// while a glvalue has no empty-list form (Unknown); a multi-element list is +// Unknown for both. Returns std::nullopt when E is not a pass-through form. +template +static std::optional +classifyUninitPassThrough(const Expr *E, UninitStorage EmptyListState, + RecurseFn Recurse) { + if (const auto *ILE = dyn_cast(E)) { + if (ILE->getNumInits() == 1) + return Recurse(ILE->getInit(0)); + return ILE->getNumInits() == 0 ? EmptyListState : UninitStorage::Unknown; + } + if (const auto *CO = dyn_cast(E)) + return combineArms(Recurse(CO->getTrueExpr()), + Recurse(CO->getFalseExpr())); + if (const auto *BO = dyn_cast(E); BO && BO->isCommaOp()) + return Recurse(BO->getRHS()); + return std::nullopt; +} + +// A call to a [[ref_to_uninit]]-returning function yields uninitialized +// storage (the pointed-to memory, or the returned referent). An unmarked +// direct callee is trusted Initialized (paper §4.3); a call with no direct +// callee (through a function pointer) is Unknown. Shared by both recognizers. +static UninitStorage classifyRefToUninitCallee(const CallExpr *CE) { + if (const FunctionDecl *FD = CE->getDirectCallee()) + return FD->hasAttr() ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + return UninitStorage::Unknown; +} + +// \p E is a pointer prvalue. Classifies whether it points to uninitialized +// storage. +static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, + const Expr *E, + bool ForRead = false) { + if (!E) + return UninitStorage::Unknown; + E = E->IgnoreParenImpCasts(); + + if (auto PassThrough = classifyUninitPassThrough( + E, /*EmptyListState=*/UninitStorage::Initialized, + [&](const Expr *Sub) { + return pointerRefersToUninitStorage(Ctx, Sub, ForRead); + })) + return *PassThrough; + + // Array-to-pointer decay has been stripped above, leaving the array glvalue. + if (E->getType()->isArrayType()) + return glvalueDenotesUninitStorage(Ctx, E, ForRead); + + // &G, where G denotes uninitialized storage. + if (const auto *UO = dyn_cast(E)) + if (UO->getOpcode() == UO_AddrOf) + return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), ForRead); + + // A value of a [[ref_to_uninit]] pointer is Uninitialized; an unmarked + // named pointer is a trusted Initialized pointer (paper §4.3). + if (const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(E)) + return VD->hasAttr() ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + if (const auto *CE = dyn_cast(E)) + return classifyRefToUninitCallee(CE); + + // A default-initialized new-expression (none init style: no initializer + // written) whose allocated type's default-initialization leaves a scalar + // subobject indeterminate produces uninitialized free-store memory (paper + // §1.2/§4.3), like a [[ref_to_uninit]] allocator. The style gates this + // rather + // than hasInitializer(), which is also true for new Agg -- default- + // initializing a class synthesizes a (possibly trivial) constructor call. + // new T(...) / new T{} are value- or list-initialized; a user-provided + // default constructor is trusted by defaultInitLeavesScalarIndeterminate. + if (const auto *NE = dyn_cast(E)) { + if (NE->getInitializationStyle() != CXXNewInitializationStyle::None) + return UninitStorage::Initialized; + llvm::SmallPtrSet Visited; + return defaultInitLeavesScalarIndeterminateImpl(Ctx, NE->getAllocatedType(), + /*HonorUninitMarkers=*/true, + Visited) + ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + } + + // Paper §4.3: a [[ref_to_uninit]] pointer cast to another pointer type is + // itself [[ref_to_uninit]]. Implicit casts were already stripped above, so + // this only looks through an explicit pointer-to-pointer cast; a pointer + // manufactured from an integer (operand not a pointer) is Unknown. + if (const auto *CE = dyn_cast(E)) + if (CE->getSubExpr()->getType()->isPointerType()) + return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), ForRead); + + return UninitStorage::Unknown; +} + +// \p E is a glvalue. Classifies whether it denotes uninitialized storage. +static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, + const Expr *E, bool ForRead) { + if (!E) + return UninitStorage::Unknown; + E = E->IgnoreParenImpCasts(); + + if (auto PassThrough = classifyUninitPassThrough( + E, /*EmptyListState=*/UninitStorage::Unknown, + [&](const Expr *Sub) { + return glvalueDenotesUninitStorage(Ctx, Sub, ForRead); + })) + return *PassThrough; + + // A named entity denotes uninitialized storage if it is [[uninit]], or + // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, + // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes + // the pointer object itself -- which is initialized -- so it does not count. + // In read mode the [[uninit]] arm is dropped: a direct read of a named + // [[uninit]] object is left to the flow-based uninit_read pass, so only a + // [[ref_to_uninit]] reference (or indirection, handled below) still counts. + auto DeclDenotesUninit = [&](const ValueDecl *VD) { + return (!ForRead && VD->hasAttr()) || + (VD->getType()->isReferenceType() && VD->hasAttr()); + }; + if (const auto *DRE = dyn_cast(E)) + return DeclDenotesUninit(DRE->getDecl()) ? UninitStorage::Uninitialized + : UninitStorage::Initialized; + if (const auto *ME = dyn_cast(E)) { + // a->m reaches m through the pointer a (object *a); a.m through the + // glvalue a. When m does not itself denote uninit storage, the subobject is + // uninit exactly when its base is. + if (DeclDenotesUninit(ME->getMemberDecl())) + return UninitStorage::Uninitialized; + return ME->isArrow() + ? pointerRefersToUninitStorage(Ctx, ME->getBase(), ForRead) + : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead); + } + // A call to a [[ref_to_uninit]]-returning reference function: the referent + // it returns is uninitialized. + if (const auto *CE = dyn_cast(E)) + return classifyRefToUninitCallee(CE); + if (const auto *ASE = dyn_cast(E)) + return pointerRefersToUninitStorage(Ctx, ASE->getBase(), ForRead); + + // *p, where p points to uninitialized storage. + if (const auto *UO = dyn_cast(E)) + if (UO->getOpcode() == UO_Deref) + return pointerRefersToUninitStorage(Ctx, UO->getSubExpr(), ForRead); + + // A reference cast (an explicit cast yielding a glvalue) denotes the same + // storage as its operand; propagate. Symmetric to the pointer-cast arm. + if (const auto *CE = dyn_cast(E)) + if (CE->getSubExpr()->isGLValue()) + return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), ForRead); + + return UninitStorage::Unknown; +} + +// Dispatches a binding source to the pointer or glvalue recognizer. +static UninitStorage classifyUninitSource(ASTContext &Ctx, const Expr *E, + bool IsReference) { + return IsReference ? glvalueDenotesUninitStorage(Ctx, E) + : pointerRefersToUninitStorage(Ctx, E); +} + +bool SemaProfiles::refersToUninitializedMemory(const Expr *E, + bool IsReference) const { + return classifyUninitSource(getASTContext(), E, IsReference) == + UninitStorage::Uninitialized; +} + +// The Decl-less ref_to_uninit check sites (call argument, default argument, +// assignment, return, read-through) can't rely on the D->isTemplated() +// deferral in shouldEmitProfileViolation, but their Build* routines are +// re-run at instantiation, so defer on a template pattern here instead: +// firing on the pattern double-diagnoses and wrongly fires in discarded +// if-constexpr branches / never-instantiated templates. Sites that do pass a +// Decl are unaffected. (This must stay out of shouldEmitProfileViolation: +// its other Decl-less caller, the reinterpret_cast check, is *not* re-run at +// instantiation, so deferring there would lose the diagnostic entirely.) +static bool deferUninitCheckOnTemplatePattern(Sema &S, const Decl *D) { + return !D && S.CurContext && S.CurContext->isDependentContext(); +} + +void SemaProfiles::checkRefToUninitInit(SourceLocation Loc, + bool TargetIsRefToUninit, + bool IsReference, const Expr *Src, + const Decl *D) { + // A RecoveryExpr is a placeholder for an initialization that already failed, + // not a source the user wrote, so it must not drive this rule. + if (!Src || isa(Src->IgnoreParens())) + return; + if (deferUninitCheckOnTemplatePattern(SemaRef, D)) + return; + static constexpr StringRef Profile = "std::init"; + static constexpr StringRef Rule = "ref_to_uninit"; + if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) + return; + UninitStorage SrcState = + classifyUninitSource(getASTContext(), Src, IsReference); + unsigned IsRef = IsReference ? 1 : 0; + // A marked target is a violation only against an affirmatively Initialized + // source: an Unknown one (pointer arithmetic, an integer-to-pointer cast, a + // call through a function pointer) cannot be proven initialized, so rejecting + // it would be a false positive. An unmarked target is diagnosed only against + // an affirmatively Uninitialized source (Unknown stays a missed diagnostic). + if (TargetIsRefToUninit && SrcState == UninitStorage::Initialized) + Diag(Loc, diag::err_init_ref_to_uninit_requires_uninit) << Profile << IsRef; + else if (!TargetIsRefToUninit && SrcState == UninitStorage::Uninitialized) + Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; +} + +void SemaProfiles::checkRefToUninitBinding(SourceLocation Loc, + const ValueDecl *Target, QualType T, + const Expr *Src, const Decl *D) { + if (!getLangOpts().Profiles || T.isNull() || T->isDependentType() || + (!T->isPointerType() && !T->isReferenceType())) + return; + checkRefToUninitInit(Loc, Target->hasAttr(), + T->isReferenceType(), Src, D); +} + +void SemaProfiles::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, + QualType ValueType) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // a read the user wrote, so it must not drive this rule. + if (!Glvalue || isa(Glvalue->IgnoreParens())) + return; + if (deferUninitCheckOnTemplatePattern(SemaRef, /*D=*/nullptr)) + return; + // Paper §4.5: reading an uninitialized std::byte is permitted. + if (getASTContext().getBaseElementType(ValueType)->isStdByteType()) + return; + if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) + return; + if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, /*ForRead=*/true) != + UninitStorage::Uninitialized) + return; + Diag(Loc, diag::err_init_uninit_read_through) << "std::init"; +} + + +namespace { +// Row for the unified finalization dispatch shared by class-finalization +// (pattern 3) and constructor-finalization (pattern 4): a profile name plus a +// callback invoked once per finalized, non-dependent, non-invalid Node (a +// CXXRecordDecl or a CXXConstructorDecl). Adding a new profile is a single row +// in the matching table below plus a ProfileRuleError diagnostic in +// DiagnosticSemaKinds.td and a callback that consults +// SemaProfiles::shouldEmitProfileViolation before emitting. +template struct FinalizationProfile { + StringRef Name; + void (*Callback)(Sema &, Node *); +}; + +void runTestClassFinalCallback(Sema &S, CXXRecordDecl *RD) { + if (!S.Profiles().shouldEmitProfileViolation("test::class_final", /*Rule=*/"", + RD->getLocation(), RD)) + return; + S.Diag(RD->getLocation(), diag::err_profile_class_final_test) + << "test::class_final" << RD; +} + +void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { + if (!S.Profiles().shouldEmitProfileViolation("test::ctor_final", /*Rule=*/"", + Ctor->getLocation(), Ctor)) + return; + S.Diag(Ctor->getLocation(), diag::err_profile_ctor_final_test) + << "test::ctor_final" << Ctor->getParent(); +} + +void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { + // Paper §6.1: a user-provided constructor must initialize every member via + // its member-initializer list or an NSDMI, unless the member is marked + // [[uninit]] (whose body initialization is the deferred R7 check). + // A plain assignment in the constructor body does not count. + if (!Ctor->isUserProvided()) + return; + + // A union's members are mutually exclusive; a constructor initializes at most + // one, so the "every member" rule does not apply (paper §6.5). Whether the + // active member is set is a constructor-body flow question, deferred. + if (Ctor->getParent()->isUnion()) + return; + + // Members and direct bases given a written initializer by this constructor. + llvm::SmallPtrSet Written; + llvm::SmallPtrSet WrittenBases; + for (const CXXCtorInitializer *Init : Ctor->inits()) { + if (!Init->isWritten()) + continue; + if (Init->isAnyMemberInitializer()) { + if (const FieldDecl *F = Init->getAnyMember()) + Written.insert(F); + } else if (Init->isBaseInitializer()) { + if (const Type *T = Init->getBaseClass()) + WrittenBases.insert( + S.Context.getCanonicalType(QualType(T, 0)).getTypePtr()); + } + } + + for (const FieldDecl *F : Ctor->getParent()->fields()) { + // Anonymous aggregate members and unnamed bit-fields are skipped; a named + // bit-field is checked like any other member. Reference and const members + // already have dedicated diagnostics when left uninitialized. + if (F->isUnnamedBitField() || !F->getDeclName() || + F->getType()->isReferenceType() || F->getType().isConstQualified()) + continue; + if (F->hasAttr() || F->hasInClassInitializer() || + Written.count(F)) + continue; + if (!S.Profiles().defaultInitLeavesScalarIndeterminate(F->getType(), + /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) + << "std::init" << F->getDeclName(); + S.Diag(F->getLocation(), diag::note_init_uninit_member_here) + << F->getDeclName(); + } + + // The guarantee is over the complete object (paper §5.1, §7.1), so a + // direct base-class subobject left indeterminate is as much a violation as a + // member. A base cannot carry an [[uninit]] marker (the attribute's subjects + // are Var/Field), so an indeterminate base must always be initialized -- + // there + // is no marker escape. Virtual bases are the most-derived constructor's + // responsibility, not a local property of this constructor, so they are + // deferred. A written base-initializer initializes the base; an implicit + // (non-written) one is default-init, handled by the indeterminate check. + for (const CXXBaseSpecifier &Base : Ctor->getParent()->bases()) { + if (Base.isVirtual()) + continue; + if (WrittenBases.count( + S.Context.getCanonicalType(Base.getType()).getTypePtr())) + continue; + if (!S.Profiles().defaultInitLeavesScalarIndeterminate(Base.getType(), + /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_base) + << "std::init" << Base.getType(); + S.Diag(Base.getBeginLoc(), diag::note_init_uninit_base_here) + << Base.getType(); + } +} + +// Class-finalization opt-in table (pattern 3). +constexpr FinalizationProfile ClassFinalizationProfiles[] = { + {"test::class_final", &runTestClassFinalCallback}, +}; + +// Constructor-finalization opt-in table (pattern 4). +constexpr FinalizationProfile + ConstructorFinalizationProfiles[] = { + {"test::ctor_final", &runTestCtorFinalCallback}, + {"std::init", &runStdInitCtorUninitMemberCallback}, +}; + +// Run the enforced finalization-profile callbacks in Table for D. Merges the +// former per-node dispatchers; the per-node filter (dependent, lambda, +// delegating, ...) stays at each call site. Each callback passes D to the +// Decl-aware SemaProfiles::shouldEmitProfileViolation, which honors +// [[profiles::suppress]] +// on D or a lexical parent, so the dispatcher needs no suppress scope of its +// own. The table is taken by reference-to-array, not ArrayRef: deducing Node +// from a C array against an ArrayRef> parameter is +// not +// possible (no array-to-ArrayRef conversion happens during template argument +// deduction). +template +void dispatchFinalizationProfiles(Sema &S, Node *D, + const FinalizationProfile (&Table)[N]) { + if (!S.Profiles().anyProfileEnforced(Table)) + return; + // Finalization can run nested in an unrelated instantiation whose + // [[profiles::suppress]] scope is still on the parse-time stack; the + // callbacks + // must resolve suppression only from D and its lexical parents, not that + // transient stack (P3589R2 s2.4p3). + llvm::SaveAndRestore InFinalization( + S.Profiles().InProfileFinalizationCheck, true); + for (const auto &E : Table) + if (S.Profiles().isProfileEnforced(E.Name)) + E.Callback(S, D); +} +} // namespace + +void SemaProfiles::checkProfileViolationsAtClassFinalization( + CXXRecordDecl *RD) { + if (!getLangOpts().Profiles || !RD) + return; + if (RD->isInvalidDecl() || RD->isDependentType() || RD->isLambda()) + return; + dispatchFinalizationProfiles(SemaRef, RD, ClassFinalizationProfiles); +} + +void SemaProfiles::checkProfileViolationsAtConstructorFinalization( + CXXConstructorDecl *Ctor) { + if (!getLangOpts().Profiles || !Ctor) + return; + // A dependent constructor pattern re-fires on instantiation; a delegating + // constructor leaves member initialization to its target. + if (Ctor->isInvalidDecl() || Ctor->isDependentContext() || + Ctor->isDelegatingConstructor()) + return; + dispatchFinalizationProfiles(SemaRef, Ctor, ConstructorFinalizationProfiles); +} + diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp index eaeb6fb0d7645..e3f178c5f76ec 100644 --- a/clang/lib/Sema/SemaStmt.cpp +++ b/clang/lib/Sema/SemaStmt.cpp @@ -33,6 +33,7 @@ #include "clang/Sema/Ownership.h" #include "clang/Sema/Scope.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenMP.h" @@ -4120,7 +4121,7 @@ StmtResult Sema::BuildReturnStmt(SourceLocation ReturnLoc, Expr *RetValExp, // must match the function's [[ref_to_uninit]] return marking. if (RetValExp && getLangOpts().Profiles) if (const FunctionDecl *FD = getCurFunctionDecl()) - checkRefToUninitBinding(RetValExp->getExprLoc(), FD, FnRetType, + Profiles().checkRefToUninitBinding(RetValExp->getExprLoc(), FD, FnRetType, RetValExp); const VarDecl *NRVOCandidate = getCopyElisionCandidate(NRInfo, FnRetType); diff --git a/clang/lib/Sema/SemaStmtAttr.cpp b/clang/lib/Sema/SemaStmtAttr.cpp index 620b06c4c102a..61f1ef12df96a 100644 --- a/clang/lib/Sema/SemaStmtAttr.cpp +++ b/clang/lib/Sema/SemaStmtAttr.cpp @@ -16,6 +16,7 @@ #include "clang/Sema/DelayedDiagnostic.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include using namespace clang; @@ -74,7 +75,7 @@ static Attr *handleSuppressAttr(Sema &S, Stmt *St, const ParsedAttr &A, static Attr *handleProfilesSuppressStmtAttr(Sema &S, Stmt *St, const ParsedAttr &A, SourceRange Range) { - return S.makeProfilesSuppressAttr(A); + return S.Profiles().makeProfilesSuppressAttr(A); } static Attr *handleLoopHintAttr(Sema &S, Stmt *St, const ParsedAttr &A, diff --git a/clang/lib/Sema/SemaTemplateInstantiate.cpp b/clang/lib/Sema/SemaTemplateInstantiate.cpp index e658288a1271a..2f12186b60796 100644 --- a/clang/lib/Sema/SemaTemplateInstantiate.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiate.cpp @@ -29,6 +29,7 @@ #include "clang/Sema/EnterExpressionEvaluationContext.h" #include "clang/Sema/Initialization.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaConcept.h" #include "clang/Sema/SemaInternal.h" #include "clang/Sema/Template.h" @@ -3842,7 +3843,7 @@ bool Sema::InstantiateInClassInitializer( ActOnStartCXXInClassMemberInitializer(); CXXThisScopeRAII ThisScope(*this, Instantiation->getParent(), Qualifiers()); - ProfileSuppressScope ProfileSuppressGuard( + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( *this, Pattern, /*WalkLexicalParents=*/true); ExprResult NewInit = SubstInitializer(OldInit, TemplateArgs, diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index 7c09d1b3e5279..506149b19c9b6 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -25,6 +25,7 @@ #include "clang/Sema/Initialization.h" #include "clang/Sema/Lookup.h" #include "clang/Sema/ScopeInfo.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaAMDGPU.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaHLSL.h" @@ -1837,7 +1838,7 @@ Decl *TemplateDeclInstantiator::VisitVarDecl(VarDecl *D, // the instantiated variable now that its type is known (the parse-time // handler deferred on the template pattern). if (!Var->isInvalidDecl()) - SemaRef.diagnoseInitUninitMarkerPlacement(Var); + SemaRef.Profiles().diagnoseInitUninitMarkerPlacement(Var); return Var; } @@ -1927,7 +1928,7 @@ Decl *TemplateDeclInstantiator::VisitFieldDecl(FieldDecl *D) { // on the (dependent) template member; re-check now that the substituted type // is known. if (!Field->isInvalidDecl()) - SemaRef.diagnoseInitUninitMarkerPlacement(Field); + SemaRef.Profiles().diagnoseInitUninitMarkerPlacement(Field); return Field; } @@ -5943,7 +5944,7 @@ void Sema::InstantiateFunctionDefinition(SourceLocation PointOfInstantiation, // PushDeclContext because we don't have a scope. Sema::ContextRAII savedContext(*this, Function); - ProfileSuppressScope ProfileSuppressGuard( + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( *this, PatternDecl, /*WalkLexicalParents=*/true); FPFeaturesStateRAII SavedFPFeatures(*this); @@ -6241,7 +6242,7 @@ void Sema::InstantiateVariableInitializer( Var->setImplicitlyInline(); ContextRAII SwitchContext(*this, Var->getDeclContext()); - ProfileSuppressScope ProfileSuppressGuard( + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( *this, OldVar, /*WalkLexicalParents=*/true); EnterExpressionEvaluationContext Evaluated( diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h index 0f7f20e460a0b..bb49f93714de4 100644 --- a/clang/lib/Sema/TreeTransform.h +++ b/clang/lib/Sema/TreeTransform.h @@ -40,6 +40,7 @@ #include "clang/Sema/ScopeInfo.h" #include "clang/Sema/SemaDiagnostic.h" #include "clang/Sema/SemaInternal.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/SemaOpenACC.h" #include "clang/Sema/SemaOpenMP.h" @@ -8311,7 +8312,8 @@ template StmtResult TreeTransform::TransformAttributedStmt(AttributedStmt *S, StmtDiscardKind SDK) { - Sema::ProfileSuppressScope ProfileSuppressGuard(getSema(), S->getAttrs()); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(getSema(), + S->getAttrs()); StmtResult SubStmt = getDerived().TransformStmt(S->getSubStmt(), SDK); @@ -8652,7 +8654,7 @@ TreeTransform::TransformDeclStmt(DeclStmt *S) { SmallVector Decls; LambdaScopeInfo *LSI = getSema().getCurLambda(); for (auto *D : S->decls()) { - Sema::ProfileSuppressScope ProfileSuppressGuard(getSema(), D); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(getSema(), D); Decl *Transformed = getDerived().TransformDefinition(D->getLocation(), D); diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index d24252cb81503..28cc392e0244d 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -78,6 +78,7 @@ #include "clang/Sema/ObjCMethodList.h" #include "clang/Sema/Scope.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/Weak.h" @@ -9294,7 +9295,7 @@ void ASTReader::UpdateSema() { // Restore enforced profile designators (P3589R2). for (const auto &EP : SerializedEnforcedProfiles) - SemaObj->addProfileEnforcement(EP.ProfileName, EP.Designator, + SemaObj->Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, SourceLocation()); SerializedEnforcedProfiles.clear(); diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 53fd993acbf8c..1945ace6b551f 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -68,6 +68,7 @@ #include "clang/Sema/IdentifierResolver.h" #include "clang/Sema/ObjCMethodList.h" #include "clang/Sema/Sema.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" #include "clang/Sema/Weak.h" @@ -5321,11 +5322,11 @@ void ASTWriter::WriteRISCVIntrinsicPragmas(Sema &SemaRef) { void ASTWriter::WriteEnforcedProfiles(Sema &SemaRef) { if (WritingModule) return; - if (SemaRef.EnforcedProfiles.empty()) + if (SemaRef.Profiles().EnforcedProfiles.empty()) return; unsigned AbbrevID = createEnforcedProfileAbbrev(Stream, ENFORCED_PROFILES); - for (const auto &EP : SemaRef.EnforcedProfiles) + for (const auto &EP : SemaRef.Profiles().EnforcedProfiles) emitEnforcedProfile(Stream, AbbrevID, ENFORCED_PROFILES, EP); } From bd7c5d1d00f49d52e7d5bbb2a5738277fae2cea7 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 13:23:40 -0400 Subject: [PATCH 182/289] Normalize the std::init rule entry-point names Adopt the checkInitProfile convention the other rule checks already follow: diagnoseInitUninitMarkerPlacement becomes checkInitProfileMarkerPlacement, checkRefToUninitInit becomes checkInitProfileRefToUninit, checkRefToUninitBinding becomes checkInitProfileRefToUninitBinding, and checkRefToUninitRead becomes checkInitProfileReadThrough. Table callbacks keep their run*Callback convention. --- clang/docs/ProfilesFramework.rst | 12 +++++------ clang/include/clang/Sema/SemaProfiles.h | 13 ++++++------ clang/lib/Sema/SemaDecl.cpp | 5 ++--- clang/lib/Sema/SemaDeclAttr.cpp | 2 +- clang/lib/Sema/SemaDeclCXX.cpp | 9 ++++----- clang/lib/Sema/SemaExpr.cpp | 8 ++++---- clang/lib/Sema/SemaInit.cpp | 13 ++++++------ clang/lib/Sema/SemaProfiles.cpp | 20 ++++++++++--------- clang/lib/Sema/SemaStmt.cpp | 4 ++-- .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 4 ++-- 10 files changed, 46 insertions(+), 44 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 471632c3823bb..be7ee712c4850 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -183,7 +183,7 @@ or in an ``if constexpr`` branch discarded only at instantiation. ``test::type_cast`` accepts this (it is a test-only profile). A profile whose ``Build*`` routine *is* re-run at instantiation can instead defer on a template pattern with a ``CurContext->isDependentContext()`` guard, as the ``std::init`` -ref_to_uninit binding checks do (see ``checkRefToUninitInit``). +ref_to_uninit binding checks do (see ``checkInitProfileRefToUninit``). Suppression for parse-time check sites is consulted via the ``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` @@ -955,7 +955,7 @@ assignment when compiled without the profile. [[profiles::suppress(std::init)]] U e [[uninit]]; // OK - Diagnostic: ``err_init_union_marker``. -- Check site: the shared helper ``SemaProfiles::diagnoseInitUninitMarkerPlacement``, +- Check site: the shared helper ``SemaProfiles::checkInitProfileMarkerPlacement``, called from the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp`` and re-run on the instantiated entity from ``VisitFieldDecl`` / ``VisitVarDecl`` in ``clang/lib/Sema/SemaTemplateInstantiateDecl.cpp``. Unlike the reference / @@ -1007,7 +1007,7 @@ recognizers symmetric. initialized source) and ``err_init_uninit_requires_ref_to_uninit`` (unmarked target, uninitialized source). - Recognizer + shared check: ``SemaProfiles::refersToUninitializedMemory`` and - ``SemaProfiles::checkRefToUninitInit`` in ``clang/lib/Sema/SemaProfiles.cpp``. + ``SemaProfiles::checkInitProfileRefToUninit`` in ``clang/lib/Sema/SemaProfiles.cpp``. - A ``new`` expression is recognised only when it default-initializes its object (none init style, no written initializer); ``new T(...)`` and ``new T{...}`` are value- or list-initialized and excluded. Whether the @@ -1033,7 +1033,7 @@ recognizers symmetric. ``ctor_uninit_member``. The Decl-less call-argument, assignment, return, and aggregate-field sites -- whose ``Build*`` / ``InitListChecker`` routine is re-run at instantiation -- instead defer via a - ``CurContext->isDependentContext()`` guard in ``checkRefToUninitInit``, so + ``CurContext->isDependentContext()`` guard in ``checkInitProfileRefToUninit``, so they neither double-fire nor fire in a discarded ``if constexpr`` branch or a never-instantiated template. The aggregate-field hooks (``CheckSubElementType`` for a pointer field, ``CheckReferenceType`` for a @@ -1044,7 +1044,7 @@ recognizers symmetric. - Read-through enforcement (paper §4.5): a scalar *read* through a ``[[ref_to_uninit]]`` pointer or reference loads an uninitialized value and is diagnosed at the single lvalue-to-rvalue chokepoint - (``Sema::DefaultLvalueConversion`` calling ``SemaProfiles::checkRefToUninitRead``), + (``Sema::DefaultLvalueConversion`` calling ``SemaProfiles::checkInitProfileReadThrough``), which by-value reads -- copy-initialization, by-value arguments, returns, and operator/condition operands -- all funnel through. It reuses the recognizer in a read-only mode (a ``ForRead`` flag that drops the directly-named @@ -1098,7 +1098,7 @@ A pointer must instead be initialized (e.g. to ``nullptr``). [[profiles::suppress(std::init, rule: "pointer_marker")]] int *x [[uninit]]; // OK - Diagnostic: ``err_init_uninit_pointer_marker``. -- Check site: ``SemaProfiles::diagnoseInitUninitMarkerPlacement``, alongside +- Check site: ``SemaProfiles::checkInitProfileMarkerPlacement``, alongside ``union_marker`` (see R6 for the shared parse-time handler and the re-check on the instantiated field / variable). Like that rule it is gated on enforcement -- a pointer may legitimately carry the marker without the profile diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index e64998c3d9e46..7cb19b94c597c 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -191,7 +191,7 @@ class SemaProfiles : public SemaBase { /// a marked target must refer to uninitialized memory, and an unmarked /// target must not. Shared by the variable, data-member, assignment, /// argument, and return check sites; gated by shouldEmitProfileViolation. - void checkRefToUninitInit(SourceLocation Loc, bool TargetIsRefToUninit, + void checkInitProfileRefToUninit(SourceLocation Loc, bool TargetIsRefToUninit, bool IsReference, const Expr *Src, const Decl *D = nullptr); @@ -203,9 +203,10 @@ class SemaProfiles : public SemaBase { /// dependent type defers to instantiation, where the check site re-runs /// with the concrete type). \p D, when available, is the declaration used /// for suppression lookup and template-pattern deferral. - void checkRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, - QualType T, const Expr *Src, - const Decl *D = nullptr); + void checkInitProfileRefToUninitBinding(SourceLocation Loc, + const ValueDecl *Target, QualType T, + const Expr *Src, + const Decl *D = nullptr); /// std::init / uninit_read (paper §4.5): diagnose a read *through* a /// [[ref_to_uninit]] pointer or reference, whose result is itself @@ -215,7 +216,7 @@ class SemaProfiles : public SemaBase { /// read-only mode, so a direct read of a named [[uninit]] object is left to /// the flow-based uninit_read pass. A std::byte read is exempt (paper /// §4.5). - void checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, + void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType); /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose @@ -223,7 +224,7 @@ class SemaProfiles : public SemaBase { /// \p D must already carry the UninitAttr (the marker location is taken /// from it). Decl-aware via shouldEmitProfileViolation, so it defers on a /// templated pattern and is re-checked on the instantiated entity. - void diagnoseInitUninitMarkerPlacement(const Decl *D); + void checkInitProfileMarkerPlacement(const Decl *D); class ProfileSuppressScope { Sema &S; diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 03182fbe1c814..3b362e44131ee 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -14829,9 +14829,8 @@ void Sema::CheckCompleteVariableDeclaration(VarDecl *var) { // std::init / ref_to_uninit (paper §5): a pointer or reference variable // must be bound consistently with its [[ref_to_uninit]] marking. - Profiles().checkRefToUninitBinding(var->getLocation(), var, var->getType(), - - var->getInit(), var); + Profiles().checkInitProfileRefToUninitBinding( + var->getLocation(), var, var->getType(), var->getInit(), var); CUDA().MaybeAddConstantAttr(var); diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index be83b366d7c91..8d515bf2ab434 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6970,7 +6970,7 @@ static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // std::init / union_marker + pointer_marker (paper §4.1, §5.6). Shared with // the template-instantiation re-check sites (VisitFieldDecl / VisitVarDecl), // since this handler only runs on the pattern. - S.Profiles().diagnoseInitUninitMarkerPlacement(D); + S.Profiles().checkInitProfileMarkerPlacement(D); } static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index 5033aece1dc41..fe20076e67a07 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4238,9 +4238,8 @@ void Sema::ActOnFinishCXXInClassMemberInitializer(Decl *D, // std::init / ref_to_uninit (paper §5): a pointer or reference data member // with a default member initializer must be bound consistently with its // [[ref_to_uninit]] marking. - Profiles().checkRefToUninitBinding(FD->getLocation(), FD, FD->getType(), - - FD->getInClassInitializer(), FD); + Profiles().checkInitProfileRefToUninitBinding( + FD->getLocation(), FD, FD->getType(), FD->getInClassInitializer(), FD); } /// Find the direct and/or virtual base specifiers that @@ -4688,8 +4687,8 @@ Sema::BuildMemberInitializer(ValueDecl *Member, Expr *Init, // BuildMemberInitializer re-runs with the instantiated constructor as // CurContext, matching ctor_uninit_member. if (auto *Ctor = dyn_cast(CurContext)) - Profiles().checkRefToUninitBinding(IdLoc, Member, Member->getType(), - Init, Ctor); + Profiles().checkInitProfileRefToUninitBinding( + IdLoc, Member, Member->getType(), Init, Ctor); } if (DirectMember) { diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 7f8b69310afb1..b569137d8d881 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -743,7 +743,7 @@ ExprResult Sema::DefaultLvalueConversion(Expr *E) { // lvalue-to-rvalue chokepoint that by-value reads (copy-init, by-value // arguments, returns, operator operands) all funnel through. if (getLangOpts().Profiles) - Profiles().checkRefToUninitRead(E->getExprLoc(), E, T); + Profiles().checkInitProfileReadThrough(E->getExprLoc(), E, T); // C++ [conv.lval]p3: // If T is cv std::nullptr_t, the result is a null pointer constant. @@ -6289,8 +6289,8 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, const Expr *Src = Arg; if (const auto *DAE = dyn_cast(Src)) Src = DAE->getExpr(); - Profiles().checkRefToUninitBinding(Arg->getExprLoc(), Param, - Param->getType(), Src); + Profiles().checkInitProfileRefToUninitBinding(Arg->getExprLoc(), Param, + Param->getType(), Src); } // Check for array bounds violations for each argument to the call. This @@ -15512,7 +15512,7 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, // memory. if (getLangOpts().Profiles && LHS.get()->getType()->isPointerType()) { const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(LHS.get()); - Profiles().checkRefToUninitInit( + Profiles().checkInitProfileRefToUninit( OpLoc, VD && VD->hasAttr(), /*IsReference=*/false, RHS.get()); } diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 34383b88d38c5..4053991b6fe04 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -1601,12 +1601,13 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, // handles a braced source via the recognizer. No Decl is passed: the // init list can appear in a template independently of whether the // aggregate is one, so deferral comes from the dependent-context - // guard in checkRefToUninitInit and suppression from the parse-time - // stack. (A reference field is routed to CheckReferenceType above.) + // guard in checkInitProfileRefToUninit and suppression from the + // parse-time stack. (A reference field is routed to + // CheckReferenceType above.) if (!Result.isInvalid() && Entity.getKind() == InitializedEntity::EK_Member) - SemaRef.Profiles().checkRefToUninitBinding(expr->getExprLoc(), - Entity.getDecl(), ElemType, expr); + SemaRef.Profiles().checkInitProfileRefToUninitBinding( + expr->getExprLoc(), Entity.getDecl(), ElemType, expr); UpdateStructuredListElement(StructuredList, StructuredIndex, Result.getAs()); @@ -1913,10 +1914,10 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, // the parent guard prevents a double diagnostic there. No Decl is passed: the // init list can appear in a template independently of whether the aggregate // is one, so deferral comes from the dependent-context guard in - // checkRefToUninitInit and suppression from the parse-time stack. + // checkInitProfileRefToUninit and suppression from the parse-time stack. if (!VerifyOnly && !Result.isInvalid() && Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent()) - SemaRef.Profiles().checkRefToUninitBinding(Src->getExprLoc(), + SemaRef.Profiles().checkInitProfileRefToUninitBinding(Src->getExprLoc(), Entity.getDecl(), DeclType, Src); UpdateStructuredListElement(StructuredList, StructuredIndex, expr); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index e08e19c851069..9392b30a350d6 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -259,8 +259,8 @@ bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, // Decl-less expression check sites whose Build* routine is re-run at // instantiation (the ref_to_uninit binding checks: call argument, pointer // assignment, return) instead defer in a dependent context from their own - // wrapper, checkRefToUninitInit, since no Decl is available here. The - // [[uninit]] marker checks pass D (via diagnoseInitUninitMarkerPlacement) and + // wrapper, checkInitProfileRefToUninit, since no Decl is available here. The + // [[uninit]] marker checks pass D (via checkInitProfileMarkerPlacement) and // are re-run on the instantiated field / variable, so they defer here too. // The reinterpret_cast check still passes D == nullptr and is not re-checked // at instantiation, so it keeps running once at parse time (a separate gap). @@ -535,7 +535,7 @@ void SemaProfiles::checkInitProfileUninitWithInitializer(const ValueDecl *D, << Profile << D->getDeclName(); } -void SemaProfiles::diagnoseInitUninitMarkerPlacement(const Decl *D) { +void SemaProfiles::checkInitProfileMarkerPlacement(const Decl *D) { const auto *UA = D->getAttr(); if (!UA) return; @@ -581,7 +581,8 @@ void SemaProfiles::diagnoseInitUninitMarkerPlacement(const Decl *D) { // treat // Unknown as not uninitialized. // -// \p ForRead selects read-through mode (SemaProfiles::checkRefToUninitRead): a +// \p ForRead selects read-through mode +// (SemaProfiles::checkInitProfileReadThrough): a // directly named [[uninit]] object is then not treated as uninitialized, // because a read of such a named object is the flow-based uninit_read pass's // responsibility; only indirection through a [[ref_to_uninit]] @@ -796,7 +797,7 @@ static bool deferUninitCheckOnTemplatePattern(Sema &S, const Decl *D) { return !D && S.CurContext && S.CurContext->isDependentContext(); } -void SemaProfiles::checkRefToUninitInit(SourceLocation Loc, +void SemaProfiles::checkInitProfileRefToUninit(SourceLocation Loc, bool TargetIsRefToUninit, bool IsReference, const Expr *Src, const Decl *D) { @@ -824,18 +825,19 @@ void SemaProfiles::checkRefToUninitInit(SourceLocation Loc, Diag(Loc, diag::err_init_uninit_requires_ref_to_uninit) << Profile << IsRef; } -void SemaProfiles::checkRefToUninitBinding(SourceLocation Loc, +void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, const ValueDecl *Target, QualType T, const Expr *Src, const Decl *D) { if (!getLangOpts().Profiles || T.isNull() || T->isDependentType() || (!T->isPointerType() && !T->isReferenceType())) return; - checkRefToUninitInit(Loc, Target->hasAttr(), + checkInitProfileRefToUninit(Loc, Target->hasAttr(), T->isReferenceType(), Src, D); } -void SemaProfiles::checkRefToUninitRead(SourceLocation Loc, const Expr *Glvalue, - QualType ValueType) { +void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, + const Expr *Glvalue, + QualType ValueType) { // A RecoveryExpr is a placeholder for an expression that already failed, not // a read the user wrote, so it must not drive this rule. if (!Glvalue || isa(Glvalue->IgnoreParens())) diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp index e3f178c5f76ec..51cbef34c4354 100644 --- a/clang/lib/Sema/SemaStmt.cpp +++ b/clang/lib/Sema/SemaStmt.cpp @@ -4121,8 +4121,8 @@ StmtResult Sema::BuildReturnStmt(SourceLocation ReturnLoc, Expr *RetValExp, // must match the function's [[ref_to_uninit]] return marking. if (RetValExp && getLangOpts().Profiles) if (const FunctionDecl *FD = getCurFunctionDecl()) - Profiles().checkRefToUninitBinding(RetValExp->getExprLoc(), FD, FnRetType, - RetValExp); + Profiles().checkInitProfileRefToUninitBinding(RetValExp->getExprLoc(), + FD, FnRetType, RetValExp); const VarDecl *NRVOCandidate = getCopyElisionCandidate(NRInfo, FnRetType); diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index 506149b19c9b6..23a3f8b211851 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -1838,7 +1838,7 @@ Decl *TemplateDeclInstantiator::VisitVarDecl(VarDecl *D, // the instantiated variable now that its type is known (the parse-time // handler deferred on the template pattern). if (!Var->isInvalidDecl()) - SemaRef.Profiles().diagnoseInitUninitMarkerPlacement(Var); + SemaRef.Profiles().checkInitProfileMarkerPlacement(Var); return Var; } @@ -1928,7 +1928,7 @@ Decl *TemplateDeclInstantiator::VisitFieldDecl(FieldDecl *D) { // on the (dependent) template member; re-check now that the substituted type // is known. if (!Field->isInvalidDecl()) - SemaRef.Profiles().diagnoseInitUninitMarkerPlacement(Field); + SemaRef.Profiles().checkInitProfileMarkerPlacement(Field); return Field; } From 934297b08d939d11756e3d1657db9f13a33446ed Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:24:07 -0400 Subject: [PATCH 183/289] Don't treat const ref/pointer uses as profile uninit reads They are ref_to_uninit binding territory, checked at the binding site. --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 4 ++++ clang/test/SemaCXX/safety-profile-init-read.cpp | 16 ++++++++++++++++ .../test/SemaCXX/safety-profile-uninit-read.cpp | 11 +++++++++++ 3 files changed, 31 insertions(+) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index fd219eec568f9..e2fe8978ee870 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2024,6 +2024,10 @@ class UninitValsDiagReporter : public UninitVariablesHandler { // use site and is not suppressed there, emit the profile diagnostic // and skip the default warning path entirely. for (const auto &U : *vec) { + // A const-reference binding or address-taking use is not a read; it is + // ref_to_uninit (R7) binding territory, checked at the binding site. + if (U.isConstRefOrPtrUse()) + continue; for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { // std::byte may be read while uninitialized (paper §4). if (E.ExemptStdByte && diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index 635accd27f92b..f836a6b24daa2 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -124,6 +124,22 @@ void test_byte_read_exempt() { std::byte c = b; (void)c; } + +void take_const_ref(const int &); +void take_const_ptr(const int *); + +// A const-reference or const-pointer use of an uninitialized variable is a +// binding (ref_to_uninit territory, diagnosed at the binding site), not a +// read: uninit_read must not fire on it. +void test_const_ref_use_is_not_a_read() { + int x [[uninit]]; + take_const_ref(x); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +void test_const_ptr_use_is_not_a_read() { + int x [[uninit]]; + take_const_ptr(&x); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} #endif #ifdef DEMOTE diff --git a/clang/test/SemaCXX/safety-profile-uninit-read.cpp b/clang/test/SemaCXX/safety-profile-uninit-read.cpp index 3c480057d77b3..695b6969c3ad4 100644 --- a/clang/test/SemaCXX/safety-profile-uninit-read.cpp +++ b/clang/test/SemaCXX/safety-profile-uninit-read.cpp @@ -123,3 +123,14 @@ void test_decl_suppress_does_not_extend() { int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} (void)y; } + +void take_const_ref(const int &); +void take_const_ptr(const int *); + +// A const-reference or const-pointer use of an uninitialized variable is a +// binding, not a read: the profile must not report it as one. +void test_const_ref_use_not_diagnosed() { + int x; + take_const_ref(x); + take_const_ptr(&x); +} From eb366ce4bd222350bcdff7f5959807756e699a4f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:35:24 -0400 Subject: [PATCH 184/289] Defer [[ref_to_uninit]] type validation to instantiation A dependent subject is checked once its substituted type is known; an invalid marker is dropped, silently in SFINAE contexts. --- clang/include/clang/Sema/SemaProfiles.h | 13 +++++++ clang/lib/Sema/SemaDeclAttr.cpp | 13 ++----- clang/lib/Sema/SemaProfiles.cpp | 23 +++++++++++++ .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 13 +++++++ .../safety-profile-init-ref-to-uninit.cpp | 10 ++++++ .../safety-profile-ref-to-uninit-marker.cpp | 34 +++++++++++++++++++ 6 files changed, 96 insertions(+), 10 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 7cb19b94c597c..72e11190f92f1 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -226,6 +226,19 @@ class SemaProfiles : public SemaBase { /// templated pattern and is re-checked on the instantiated entity. void checkInitProfileMarkerPlacement(const Decl *D); + /// [[ref_to_uninit]] is only meaningful on a pointer or reference to an + /// object (for a function, its return type). Returns true when \p D's type + /// is invalid for the marker, diagnosing err_ref_to_uninit_attr_invalid_type + /// at \p AttrLoc unless \p Diagnose is false. A dependent type returns + /// false: validation defers to the instantiation re-check in + /// Sema::InstantiateAttrs, which drops the marker when the substituted type + /// is invalid -- silently in a SFINAE context (\p Diagnose false there), so + /// the marker can never affect overload resolution; a dropped marker is + /// inert. Not profile policy -- fires regardless of -fprofiles, like the + /// parse-time handler it serves. + bool diagnoseInvalidRefToUninitMarker(const Decl *D, SourceLocation AttrLoc, + bool Diagnose = true); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 8d515bf2ab434..37d228297daa9 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6981,16 +6981,9 @@ static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // never uninitialized storage, so the marker could never be satisfied; reject // it too (like a pointer-to-member, which is not a pointer type here). Like // the [[uninit]] subject checks, this is not profile policy and so fires - // regardless of -fprofiles. - QualType T; - if (const auto *FD = dyn_cast(D)) - T = FD->getReturnType(); - else - T = cast(D)->getType(); - - if ((!T->isPointerType() && !T->isReferenceType()) || - T->isFunctionPointerType() || T->isFunctionReferenceType()) { - S.Diag(AL.getLoc(), diag::err_ref_to_uninit_attr_invalid_type); + // regardless of -fprofiles. A dependent subject defers to the instantiation + // re-check in Sema::InstantiateAttrs, once the substituted type is known. + if (S.Profiles().diagnoseInvalidRefToUninitMarker(D, AL.getLoc())) { AL.setInvalid(); return; } diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 9392b30a350d6..1aaf7343be8c1 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -568,6 +568,29 @@ void SemaProfiles::checkInitProfileMarkerPlacement(const Decl *D) { Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; } +bool SemaProfiles::diagnoseInvalidRefToUninitMarker(const Decl *D, + SourceLocation AttrLoc, + bool Diagnose) { + QualType T; + if (const auto *FD = dyn_cast(D)) + T = FD->getReturnType(); + else + T = cast(D)->getType(); + + // A dependent subject is validated at instantiation instead, once the + // substituted type is known (Sema::InstantiateAttrs). + if (T->isDependentType()) + return false; + + if ((!T->isPointerType() && !T->isReferenceType()) || + T->isFunctionPointerType() || T->isFunctionReferenceType()) { + if (Diagnose) + Diag(AttrLoc, diag::err_ref_to_uninit_attr_invalid_type); + return true; + } + return false; +} + // std::init / ref_to_uninit (paper §5). Two mutually-recursive local // recognizers over the syntactic form of a source expression -- no flow // analysis and no type-system tracking. Uninitialized storage is only ever diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index 23a3f8b211851..82393dc492cba 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -872,6 +872,19 @@ void Sema::InstantiateAttrs(const MultiLevelTemplateArgumentList &TemplateArgs, if (!isRelevantAttr(*this, New, TmplAttr)) continue; + // The [[ref_to_uninit]] handler deferred type validation on a dependent + // subject; re-check against the substituted type and drop the marker when + // it is invalid (the diagnostic points at the pattern's attribute, with + // the instantiation note locating the culprit). In a SFINAE context + // (declaration substitution during deduction) the invalid marker is + // dropped silently: the marker must not affect overload resolution, and a + // dropped marker is inert -- the ref_to_uninit rule never consults a + // marker on a non-pointer/reference entity. + if (isa(TmplAttr) && + Profiles().diagnoseInvalidRefToUninitMarker( + New, TmplAttr->getLocation(), /*Diagnose=*/!isSFINAEContext())) + continue; + // FIXME: This should be generalized to more than just the AlignedAttr. const AlignedAttr *Aligned = dyn_cast(TmplAttr); if (Aligned && Aligned->isAlignmentDependent()) { diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 3dcf26b0d9d1a..d38102a934325 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -384,6 +384,16 @@ void template_nondependent_bad() { } template void template_nondependent_bad(); // expected-note {{in instantiation of function template specialization 'template_nondependent_bad' requested here}} +// A dependent [[ref_to_uninit]] parameter defers marker validation to +// instantiation (the pattern is accepted); the instantiated parameter carries +// the marker and drives this rule at the call. +template +void dependent_marked_fill(T p [[ref_to_uninit]]) { (void)p; } +void test_dependent_marked_param() { + dependent_marked_fill(&g_uninit); // OK: marked target, uninit source + dependent_marked_fill(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + // A default-initialized new-expression that leaves a scalar subobject // indeterminate (e.g. new int, new int[n], paper §1.2 / §4.3) is a source of // uninitialized free-store memory; new T(...), new T{...}, and a type with a diff --git a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp index 60e92f3298427..0d490967d7b5c 100644 --- a/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-ref-to-uninit-marker.cpp @@ -58,3 +58,37 @@ struct BadFnPtrMember { void (*m [[ref_to_uninit]])(); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} }; + +// A dependent subject is not validated on the template pattern; the check +// runs at instantiation, once the substituted type is known, and the marker +// is dropped when it is invalid there. +template struct DependentMember { + T m [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +}; +template struct DependentMember; +template struct DependentMember; // expected-note {{in instantiation of template class 'DependentMember' requested here}} \ + // no-profiles-note {{in instantiation of template class 'DependentMember' requested here}} + +// A dependent *parameter* is substituted during template argument deduction, +// inside the SFINAE trap. Diagnosing there would let the marker affect +// overload resolution, so an invalid parameter marker is instead dropped +// silently: no diagnostic, and the dropped marker is inert (the ref_to_uninit +// rule never consults a marker on a non-pointer/reference parameter). +template void dependent_param(T p [[ref_to_uninit]]) {} +template void dependent_param(int *); +template void dependent_param(int); + +template [[ref_to_uninit]] T dependent_return() { return T{}; } // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +template int *dependent_return(); +template int dependent_return(); // expected-note {{in instantiation of function template specialization 'dependent_return' requested here}} \ + // no-profiles-note {{in instantiation of function template specialization 'dependent_return' requested here}} + +template void dependent_local() { + T v [[ref_to_uninit]]; // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} +} +template void dependent_local(); +template void dependent_local(); // expected-note {{in instantiation of function template specialization 'dependent_local' requested here}} \ + // no-profiles-note {{in instantiation of function template specialization 'dependent_local' requested here}} From 978bf2603f90c2d088939a23949b0151f0602190 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:38:30 -0400 Subject: [PATCH 185/289] Resolve anonymous members to their field in ctor ref_to_uninit checks An IndirectFieldDecl never carries the marker; it lives on the anon field. --- clang/lib/Sema/SemaDeclCXX.cpp | 8 +++-- .../safety-profile-init-ref-to-uninit.cpp | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/SemaDeclCXX.cpp b/clang/lib/Sema/SemaDeclCXX.cpp index fe20076e67a07..2203343547c52 100644 --- a/clang/lib/Sema/SemaDeclCXX.cpp +++ b/clang/lib/Sema/SemaDeclCXX.cpp @@ -4685,10 +4685,14 @@ Sema::BuildMemberInitializer(ValueDecl *Member, Expr *Init, // Pass the enclosing constructor as the Decl so a class-template pattern // defers (via D->isTemplated()) and fires once at instantiation, where // BuildMemberInitializer re-runs with the instantiated constructor as - // CurContext, matching ctor_uninit_member. + // CurContext, matching ctor_uninit_member. A member of an anonymous + // struct/union arrives as an IndirectFieldDecl, which never carries the + // marker; the [[ref_to_uninit]] attribute lives on the underlying field. + const ValueDecl *MarkerTarget = + IndirectMember ? IndirectMember->getAnonField() : Member; if (auto *Ctor = dyn_cast(CurContext)) Profiles().checkInitProfileRefToUninitBinding( - IdLoc, Member, Member->getType(), Init, Ctor); + IdLoc, MarkerTarget, MarkerTarget->getType(), Init, Ctor); } if (DirectMember) { diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index d38102a934325..5db8fdf85fb54 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -648,6 +648,38 @@ struct CtorMemberRefOK { CtorMemberRefOK() : r(g_init), s(g_uninit) {} // OK }; +// A member of an anonymous struct/union reaches the member-initializer site as +// an IndirectFieldDecl; the [[ref_to_uninit]] marking lives on the underlying +// field and is read from there. +struct CtorAnonStructMarkedOK { + struct { + int *p [[ref_to_uninit]]; + }; + CtorAnonStructMarkedOK() : p(&g_uninit) {} // OK: marked target, uninit source +}; + +struct CtorAnonStructMarkedBad { + struct { + int *p [[ref_to_uninit]]; + }; + CtorAnonStructMarkedBad() : p(&g_init) {} // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +}; + +struct CtorAnonStructUnmarkedBad { + struct { + int *p; + }; + CtorAnonStructUnmarkedBad() : p(&g_uninit) {} // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +struct CtorAnonUnionMarkedOK { + union { + int *p [[ref_to_uninit]]; + long *q; + }; + CtorAnonUnionMarkedOK() : p(&g_uninit) {} // OK: marked target, uninit source +}; + // A braced member-initializer is looked through to its single element, exactly // like the variable-init site. struct CtorBracedPtr { From 88580c8b8d8f551dd6b4ffcd59b843be17e6c142 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:43:55 -0400 Subject: [PATCH 186/289] Warn instead of erroring on unknown module/import attributes Matches ProhibitCXX11Attributes; also mark diagnosed attrs invalid. --- clang/lib/Parse/Parser.cpp | 14 +++++++++++--- .../safety-profile-framework-modules.cppm | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/clang/lib/Parse/Parser.cpp b/clang/lib/Parse/Parser.cpp index 3db4729ac8124..f60bd980589e1 100644 --- a/clang/lib/Parse/Parser.cpp +++ b/clang/lib/Parse/Parser.cpp @@ -2314,10 +2314,18 @@ void Parser::ProhibitModuleAttributesExcept(const ParsedAttributesView &Attrs, for (const ParsedAttr &AL : Attrs) { if (AL.getKind() == AllowedKind) continue; - if (AL.isRegularKeywordAttribute()) + if (AL.isRegularKeywordAttribute()) { Diag(AL.getLoc(), KeywordDiagID) << AL; - else if (AL.isStandardAttributeSyntax()) - Diag(AL.getLoc(), AttrDiagID) << AL; + AL.setInvalid(); + } else if (AL.isStandardAttributeSyntax()) { + // An unknown attribute stays a warning, exactly as in + // ProhibitCXX11Attributes. + if (AL.getKind() == ParsedAttr::UnknownAttribute) + Actions.DiagnoseUnknownAttribute(AL); + else + Diag(AL.getLoc(), AttrDiagID) << AL; + AL.setInvalid(); + } } } diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index c46b4b665ac5a..3152d6956488c 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -28,6 +28,8 @@ // RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_noflag_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/mod_enforce_no_args.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_require_no_args.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_mod.cppm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_import.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // =================================================================== // Module with enforced profiles @@ -270,3 +272,19 @@ export void f(); // =================================================================== //--- import_require_no_args.cpp import TestMod [[profiles::require]]; // expected-error {{'require' attribute requires an argument clause}} + +// =================================================================== +// An unknown attribute on a module-declaration or import-declaration +// is a warning (exactly as in ProhibitCXX11Attributes), while a known +// but non-module attribute stays an error. +// =================================================================== +//--- unknown_attr_mod.cppm +export module UnknownAttrMod [[vendor::unknown]] [[deprecated]]; +// expected-warning@-1 {{unknown attribute 'vendor::unknown' ignored}} +// expected-error@-2 {{'deprecated' attribute cannot be applied to a module}} + +export void f(); + +//--- unknown_attr_import.cpp +import TestMod [[vendor::unknown]]; // expected-warning {{unknown attribute 'vendor::unknown' ignored}} +import TestMod [[deprecated]]; // expected-error {{'deprecated' attribute cannot be applied to a module import}} From 7a82e90a813857850ccc6d7db6d90f9997de9386 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:48:00 -0400 Subject: [PATCH 187/289] Honor field suppress scopes in late-parsed member initializers Checks fired from ActOnFinishCXXInClassMemberInitializer run after ParseCXXMemberInitializer's own scope has been popped. --- clang/lib/Parse/ParseCXXInlineMethods.cpp | 9 +++++++++ .../safety-profile-init-ref-to-uninit.cpp | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp index b741dfc9381ee..d04e0e788fb7e 100644 --- a/clang/lib/Parse/ParseCXXInlineMethods.cpp +++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp @@ -673,6 +673,15 @@ void Parser::ParseLexedMemberInitializer(LateParsedMemberInitializer &MI) { if (!MI.Field || MI.Field->isInvalidDecl()) return; + // The suppression dominion of a member declaration includes its initializer + // tokens (P3589R2 s2.4p3). ParseCXXMemberInitializer pushes its own scope + // around the parse, but the checks run from + // ActOnFinishCXXInClassMemberInitializer (e.g. the read-through check, at + // the lvalue-to-rvalue conversion the initialization sequence performs) + // fire after that scope is gone, so cover the whole late-parse here. + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + Actions, MI.Field, /*WalkLexicalParents=*/true); + ParenBraceBracketBalancer BalancerRAIIObj(*this); // Append the current token at the end of the new token stream so that it diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 5db8fdf85fb54..e8c8f3ad3901c 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -574,6 +574,26 @@ void test_read_suppress(int *p [[ref_to_uninit]]) { [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(*p); } // OK: rule-targeted suppress } +// The suppression dominion of a member declaration includes its initializer +// tokens (P3589R2 s2.4p3). The read-through check fires from +// ActOnFinishCXXInClassMemberInitializer during the late parse of an NSDMI, +// so a suppression on the field (or, via the lexical parent walk, on the +// class) must cover it; a sibling member's suppression must not leak. +int *nsdmi_rtu [[ref_to_uninit]] = &g_uninit; + +struct NsdmiReadSuppress { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int x = *nsdmi_rtu; // OK: field suppress + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] int y = *nsdmi_rtu; // OK: rule-targeted + int z = *nsdmi_rtu; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] NsdmiClassReadSuppress { + int x = *nsdmi_rtu; // OK: class-level suppression via the lexical parent walk +}; + // None of these is a read through the marker: a discarded-value expression and // an address-of apply no lvalue-to-rvalue conversion, a write targets the // glvalue without loading it, a reference binding is not a load, and copying From 54eb52e2fa8fc204f5a194b564fb4c3d0d7f9506 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:49:39 -0400 Subject: [PATCH 188/289] Gate static_runtime_init's const-init evaluation on enforcement --- clang/lib/Sema/SemaProfiles.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 1aaf7343be8c1..b352d1c49006e 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -496,10 +496,14 @@ bool SemaProfiles::checkInitProfileStaticRuntimeInit( // precedence over the standalone warning. static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "static_runtime_init"; - if (CheckConstInit()) - return false; + // Gate on enforcement before evaluating the initializer: this call site + // sits ahead of -Wglobal-constructors' isIgnored guard, so evaluating + // first would charge every global with a non-constant initializer for the + // constant-initializer evaluation even with profiles disabled. if (!shouldEmitProfileViolation(Profile, Rule, Var->getLocation(), Var)) return false; + if (CheckConstInit()) + return false; Diag(Var->getLocation(), diag::err_init_static_runtime_init) << Profile << Var->getDeclName(); return true; From 7216d3201ac5800242d4339a4c0da9ccc471ad49 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 15:58:09 -0400 Subject: [PATCH 189/289] Stop the enforce placement check deserializing the whole PCH Serialize a has-non-empty-decl bit and walk only noload_decls. --- clang/include/clang/Sema/SemaProfiles.h | 7 +++++++ .../include/clang/Serialization/ASTBitCodes.h | 5 +++++ clang/include/clang/Serialization/ASTReader.h | 4 ++++ clang/lib/Sema/SemaDeclAttr.cpp | 13 ++++++++++-- clang/lib/Serialization/ASTReader.cpp | 6 ++++++ clang/lib/Serialization/ASTWriter.cpp | 21 +++++++++++++++++++ clang/test/PCH/cxx-profiles-enforce.cpp | 17 +++++++++++++++ 7 files changed, 71 insertions(+), 2 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 72e11190f92f1..6ca97af1d7748 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -40,6 +40,13 @@ class SemaProfiles : public SemaBase { }; SmallVector EnforcedProfiles; + /// True if an included AST file (PCH) contributed a non-empty top-level + /// declaration to this TU. The [[profiles::enforce]] placement check + /// (P3589R2 [decl.attr.enforce]p1) consults this instead of deserializing + /// the PCH's declarations; ASTWriter ORs it forward so chained PCHs + /// propagate the bit. + bool TUPrecededByNonEmptyDecl = false; + struct ProfileSuppressEntry { StringRef ProfileName; StringRef RuleName; diff --git a/clang/include/clang/Serialization/ASTBitCodes.h b/clang/include/clang/Serialization/ASTBitCodes.h index 1181e55252185..8e7ea9b3520d8 100644 --- a/clang/include/clang/Serialization/ASTBitCodes.h +++ b/clang/include/clang/Serialization/ASTBitCodes.h @@ -751,6 +751,11 @@ enum ASTRecordTypes { /// Record code for enforced profile designators (P3589R2). ENFORCED_PROFILES = 79, + + /// Record code for whether the TU contains a non-empty top-level + /// declaration, consulted by the [[profiles::enforce]] placement check + /// (P3589R2 [decl.attr.enforce]p1) so it need not deserialize the PCH. + PROFILES_TU_HAS_NONEMPTY_DECL = 80, }; /// Record types used within a source manager block. diff --git a/clang/include/clang/Serialization/ASTReader.h b/clang/include/clang/Serialization/ASTReader.h index 9ac06f57640bc..8b39b8e037809 100644 --- a/clang/include/clang/Serialization/ASTReader.h +++ b/clang/include/clang/Serialization/ASTReader.h @@ -1011,6 +1011,10 @@ class ASTReader /// Enforced profile designators from PCH (P3589R2). SmallVector SerializedEnforcedProfiles; + /// Whether an included AST file recorded a non-empty top-level declaration + /// (PROFILES_TU_HAS_NONEMPTY_DECL, P3589R2 enforce placement). + bool SerializedTUHasNonEmptyDecl = false; + /// The pragma clang optimize location (if the pragma state is "off"). SourceLocation OptimizeOffPragmaLocation; diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 37d228297daa9..0ad1134e34e0d 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -5464,9 +5464,18 @@ static void handleProfilesEnforceAttr(Sema &S, Decl *D, const ParsedAttr &AL) { } // P3589R2 [decl.attr.enforce]p1: enforce on empty-declaration shall precede - // any non-empty-declaration. + // any non-empty-declaration. Declarations in an included PCH precede the + // main file but must not be deserialized just for this check: the PCH + // records whether it contains any (PROFILES_TU_HAS_NONEMPTY_DECL), and the + // walk below covers only the parsed declarations (noload_decls). auto *TU = cast(D->getDeclContext()); - for (const auto *Prev : TU->decls()) { + if (S.Profiles().TUPrecededByNonEmptyDecl) { + // The preceding declaration lives in the PCH; without deserializing it + // there is no Decl to point a note at. + S.Diag(AL.getLoc(), diag::err_profiles_enforce_after_decl); + return; + } + for (const auto *Prev : TU->noload_decls()) { if (Prev->isImplicit() || !Prev->getLocation().isValid()) continue; if (!isa(Prev)) { diff --git a/clang/lib/Serialization/ASTReader.cpp b/clang/lib/Serialization/ASTReader.cpp index 28cc392e0244d..5a0928241569f 100644 --- a/clang/lib/Serialization/ASTReader.cpp +++ b/clang/lib/Serialization/ASTReader.cpp @@ -4378,6 +4378,10 @@ llvm::Error ASTReader::ReadASTBlock(ModuleFile &F, SerializedEnforcedProfiles.push_back(readEnforcedProfile(Record, Blob)); break; + case PROFILES_TU_HAS_NONEMPTY_DECL: + SerializedTUHasNonEmptyDecl = true; + break; + case DECLS_WITH_EFFECTS_TO_VERIFY: for (unsigned I = 0, N = Record.size(); I != N; /*in loop*/) DeclsWithEffectsToVerify.push_back(ReadDeclID(F, Record, I)); @@ -9298,6 +9302,8 @@ void ASTReader::UpdateSema() { SemaObj->Profiles().addProfileEnforcement(EP.ProfileName, EP.Designator, SourceLocation()); SerializedEnforcedProfiles.clear(); + if (SerializedTUHasNonEmptyDecl) + SemaObj->Profiles().TUPrecededByNonEmptyDecl = true; // For non-modular AST files, restore visiblity of modules. for (auto &Import : PendingImportedModulesSema) { diff --git a/clang/lib/Serialization/ASTWriter.cpp b/clang/lib/Serialization/ASTWriter.cpp index 1945ace6b551f..968aa35ad08a5 100644 --- a/clang/lib/Serialization/ASTWriter.cpp +++ b/clang/lib/Serialization/ASTWriter.cpp @@ -979,6 +979,7 @@ void ASTWriter::WriteBlockInfoBlock() { RECORD(VTABLES_TO_EMIT); RECORD(RISCV_VECTOR_INTRINSICS_PRAGMA); RECORD(ENFORCED_PROFILES); + RECORD(PROFILES_TU_HAS_NONEMPTY_DECL); // SourceManager Block. BLOCK(SOURCE_MANAGER_BLOCK); @@ -5322,6 +5323,26 @@ void ASTWriter::WriteRISCVIntrinsicPragmas(Sema &SemaRef) { void ASTWriter::WriteEnforcedProfiles(Sema &SemaRef) { if (WritingModule) return; + + // Record whether the TU contains a non-empty top-level declaration, so an + // including compile's [[profiles::enforce]] placement check (P3589R2 + // [decl.attr.enforce]p1) can consult the bit instead of deserializing this + // PCH's declarations. OR in the flag restored from a base PCH so chains + // propagate it; the walk itself covers only the decls parsed here. + bool HasNonEmptyDecl = SemaRef.Profiles().TUPrecededByNonEmptyDecl; + if (!HasNonEmptyDecl) + for (const auto *D : + SemaRef.getASTContext().getTranslationUnitDecl()->noload_decls()) + if (!D->isImplicit() && D->getLocation().isValid() && + !isa(D)) { + HasNonEmptyDecl = true; + break; + } + if (HasNonEmptyDecl) { + RecordData::value_type Record[] = {1}; + Stream.EmitRecord(PROFILES_TU_HAS_NONEMPTY_DECL, Record); + } + if (SemaRef.Profiles().EnforcedProfiles.empty()) return; diff --git a/clang/test/PCH/cxx-profiles-enforce.cpp b/clang/test/PCH/cxx-profiles-enforce.cpp index b809aba39d2aa..75918e6ce081e 100644 --- a/clang/test/PCH/cxx-profiles-enforce.cpp +++ b/clang/test/PCH/cxx-profiles-enforce.cpp @@ -5,13 +5,30 @@ // RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -emit-pch -o %t // RUN: %clang_cc1 %s -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include-pch %t -verify +// A header containing a non-empty declaration must fail the main file's +// enforce placement check -- through the parsed-decl walk (with a note) when +// textually included, and through the PCH's has-non-empty-decl bit (no +// deserialization, so no note) when included as a PCH. +// RUN: %clang_cc1 %s -DPCH_DECL -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include %s -verify=expected,after,afternote +// RUN: %clang_cc1 %s -DPCH_DECL -fprofiles -fprofiles-test-profiles -std=c++20 -emit-pch -o %t.decl +// RUN: %clang_cc1 %s -DPCH_DECL -fprofiles -fprofiles-test-profiles -std=c++20 -fsyntax-only -include-pch %t.decl -verify=expected,after + #ifndef HEADER #define HEADER [[profiles::enforce(test::type_cast)]]; +#ifdef PCH_DECL +int decl_in_pch; // afternote-note {{declaration declared here}} +#endif + #else +// Repeating the enforcement is valid after a header that contains only +// empty-declarations, and diagnosed when the header contributed a real +// declaration. +[[profiles::enforce(test::type_cast)]]; // after-error {{'profiles::enforce' attribute on empty-declaration must precede all non-empty declarations}} + void test() { int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} } From abfe8287197c904d9297353f7a986b980b14b6ec Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 16:36:27 -0400 Subject: [PATCH 190/289] Check ref_to_uninit on arguments at parameter copy-initialization Covers overloaded-operator, functor, and lambda calls, which never reach GatherArgumentsForCall. --- clang/docs/ProfilesFramework.rst | 10 +++- clang/lib/Sema/SemaExpr.cpp | 19 +++---- clang/lib/Sema/SemaInit.cpp | 12 ++++ .../safety-profile-init-ref-to-uninit.cpp | 55 +++++++++++++++++++ 4 files changed, 83 insertions(+), 13 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index be7ee712c4850..09033216eb7f9 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1021,9 +1021,13 @@ recognizers symmetric. (``Sema::ActOnFinishCXXInClassMemberInitializer``), constructor member-initializers (``Sema::BuildMemberInitializer``), aggregate/list field initialization (``InitListChecker``), pointer assignment - (``Sema::CreateBuiltinBinOp``), call arguments -- including arguments - supplied by a parameter's default argument - (``Sema::GatherArgumentsForCall``), and return statements + (``Sema::CreateBuiltinBinOp``), call arguments at parameter + copy-initialization (``Sema::PerformCopyInitialization``, the funnel for + every call form with a declared callee -- plain calls, constructor calls, + overloaded operators, and calls to objects of class type such as functors + and lambdas), arguments supplied by a parameter's default argument + (``Sema::GatherArgumentsForCall``, which reuses the pre-built expression + rather than re-running copy-initialization), and return statements (``Sema::BuildReturnStmt``). Every site defers on a template pattern and fires once, at instantiation. The variable, data-member, and constructor member-initializer sites pass the instantiated ``Decl`` (deferred by the diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index b569137d8d881..af03dd96e76fc 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6282,16 +6282,15 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, } // std::init / ref_to_uninit (paper §5): a pointer or reference argument - // must match the [[ref_to_uninit]] marking of its parameter. The - // recognizers don't see through the CXXDefaultArgExpr wrapper, so check - // the underlying default-argument expression. - if (Param && getLangOpts().Profiles) { - const Expr *Src = Arg; - if (const auto *DAE = dyn_cast(Src)) - Src = DAE->getExpr(); - Profiles().checkInitProfileRefToUninitBinding(Arg->getExprLoc(), Param, - Param->getType(), Src); - } + // must match the [[ref_to_uninit]] marking of its parameter. A real + // argument is checked once, by the shared hook in + // PerformCopyInitialization; a default argument does not re-run + // copy-initialization here, so check its underlying expression (the + // recognizers don't see through the CXXDefaultArgExpr wrapper). + if (Param && getLangOpts().Profiles) + if (const auto *DAE = dyn_cast(Arg)) + Profiles().checkInitProfileRefToUninitBinding( + Arg->getExprLoc(), Param, Param->getType(), DAE->getExpr()); // Check for array bounds violations for each argument to the call. This // check only triggers warnings when the argument isn't a more complex Expr diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 4053991b6fe04..4332b5f065b81 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -10135,6 +10135,18 @@ Sema::PerformCopyInitialization(const InitializedEntity &Entity, if (ShouldTrackCopy) CurrentParameterCopyTypes.pop_back(); + // std::init / ref_to_uninit (paper §5): parameter copy-initialization is the + // funnel for call arguments from every call form -- GatherArgumentsForCall, + // overloaded operators, and calls to objects of class type -- so the binding + // check runs here, exactly once per argument. A type-only parameter entity + // (variadic promotion, a call with no declared callee) has no declaration to + // read the marking from and is skipped. A default argument does not re-run + // copy-initialization at the call site; GatherArgumentsForCall checks those. + if (!Result.isInvalid() && Entity.isParameterKind()) + if (const auto *Parm = dyn_cast_or_null(Entity.getDecl())) + Profiles().checkInitProfileRefToUninitBinding(InitE->getExprLoc(), Parm, + Parm->getType(), InitE); + return Result; } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index e8c8f3ad3901c..1b7ab38aac431 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -267,6 +267,61 @@ void test_default_arguments() { [[profiles::suppress(std::init)]] { def_ptr_bad(); } // OK: suppressed } +// Call arguments are checked at parameter copy-initialization, which also +// covers call forms that never reach GatherArgumentsForCall: calls to objects +// of class type (functors, lambdas) and overloaded operators (member and +// non-member). +struct MarkedFunctor { + void operator()(int *p [[ref_to_uninit]]); +}; +struct UnmarkedFunctor { + void operator()(int *p); +}; + +void test_functor_arguments() { + MarkedFunctor mf; + mf(&g_uninit); // OK + mf(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + UnmarkedFunctor uf; + uf(&g_init); // OK + uf(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { uf(&g_uninit); } // OK: suppressed +} + +void test_lambda_arguments() { + auto l = [](int *p [[ref_to_uninit]]) {}; + l(&g_uninit); // OK + l(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +struct OpTag {}; +OpTag operator+(OpTag, int *p [[ref_to_uninit]]); + +struct Assignable { + Assignable &operator=(int *p [[ref_to_uninit]]); +}; + +void test_operator_arguments() { + OpTag t; + (void)(t + &g_uninit); // OK + (void)(t + &g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + + Assignable a; + a = &g_uninit; // OK + a = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A functor-call argument inside a template body defers on the pattern and +// fires once, at instantiation, like every other Decl-less binding site. +template +void template_functor_bad() { + MarkedFunctor mf; + mf(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +template void template_functor_bad(); // expected-note {{in instantiation of function template specialization 'template_functor_bad' requested here}} + // Pass-through sources reach the recognizer at the call-argument site too. A // braced scalar pointer argument additionally warns (braces around scalar // initializer), so the braced cases here use references; the pointer recognizer From 17c22c15d7bdce1bab3b7c5bd0d95f6368cdd3f8 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 16:40:25 -0400 Subject: [PATCH 191/289] Check member-initializer reads in the std::init ctor pass Written initializers now assign at their CFGInitializer element, so init-expr reads are checked in execution order instead of being filtered. --- clang/docs/ProfilesFramework.rst | 8 ++- clang/lib/Sema/AnalysisBasedWarnings.cpp | 67 ++++++++++++------- .../SemaCXX/safety-profile-init-ctor-body.cpp | 43 ++++++++++++ 3 files changed, 91 insertions(+), 27 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 09033216eb7f9..1c364f9a7ee4a 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -808,8 +808,12 @@ read-then-write of that member. Details: covers it, and the ``std::byte`` exemption applies. - Target members are ``[[uninit]]`` built-in scalar (arithmetic or enum) members; a member given a value by the *written* member-initializer list is - assigned at body entry (no false positive and no spurious "marker + list-init" - contradiction). + assigned at its own initializer, in execution (declaration) order, so a later + body read is fine (no spurious "marker + list-init" contradiction) while an + *earlier* member initializer -- or a base initializer -- that reads it is a + read-before-init (``X() : o(m) {}``). An NSDMI's subexpressions are not + expanded into the CFG, so a read of a tracked member inside another member's + default initializer stays undetected. - A member access on the current object is recognized whether spelled ``this->m``, implicitly as ``m``, or as the equivalent ``(*this).m``; an access through any other object (``other.m``) is not the current object's diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index e2fe8978ee870..4df6c325896fd 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1782,12 +1782,23 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, return; const unsigned N = Members.size(); - // Statements lexically in the constructor body. Member-initializer-list - // expressions are excluded so the check concerns body reads only; a member - // initialized in the written init list instead seeds the entry state below. + // Statements the pass may see: the constructor body plus each *written* + // member/base initializer expression (the CFG is built with + // AddInitializers=true, so those run as CFG elements in execution order -- + // member-initializer reads such as `X() : o(m) {}` are checked exactly like + // body reads). A written initializer's own member becomes assigned at its + // CFGInitializer element below, so declaration order decides what an + // initializer may read. Not covered: an NSDMI's subexpressions + // (CXXDefaultInitExpr is not expanded into the CFG), so a read of a tracked + // member inside another member's default initializer stays undetected. llvm::SmallPtrSet BodyStmts; - if (const Stmt *Body = Ctor->getBody()) { - SmallVector Stack(1, Body); + { + SmallVector Stack; + if (const Stmt *Body = Ctor->getBody()) + Stack.push_back(Body); + for (const CXXCtorInitializer *Init : Ctor->inits()) + if (Init->isWritten()) + Stack.push_back(Init->getInit()); while (!Stack.empty()) { const Stmt *Cur = Stack.pop_back_val(); if (!Cur || !BodyStmts.insert(Cur).second) @@ -1797,20 +1808,6 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, } } - // A member given a value by the written member-initializer list is assigned - // at body entry, so a later body read is fine and no "marker + list-init" - // contradiction is introduced (that is not specified by the paper). - llvm::BitVector Seed(N, false); - for (const CXXCtorInitializer *Init : Ctor->inits()) { - if (!Init->isWritten() || !Init->isAnyMemberInitializer()) - continue; - if (const FieldDecl *F = Init->getAnyMember()) { - auto It = Index.find(F); - if (It != Index.end()) - Seed.set(It->second); - } - } - // Per-block ordered events recovered from the linearized CFG: a member load // (an lvalue-to-rvalue conversion of `this->m`) is a Read; `m = e` is a Write // that marks m assigned after the RHS is evaluated; a compound assignment @@ -1827,6 +1824,24 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, for (const CFGBlock *B : *cfg) { auto &BlockEvents = Events[B->getBlockID()]; for (const CFGElement &Elem : *B) { + // A written member initializer assigns its member at this point in + // execution order, after its init expression's events above it. (The + // former entry-state seeding could not order the initializers' own + // reads against these writes.) + if (auto OptInit = Elem.getAs()) { + const CXXCtorInitializer *CI = OptInit->getInitializer(); + if (!CI->isWritten() || !CI->isAnyMemberInitializer()) + continue; + const FieldDecl *F = CI->getAnyMember(); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({Write, It->second, CI->getInit()}); + Gen[B->getBlockID()].set(It->second); + continue; + } auto OptStmt = Elem.getAs(); if (!OptStmt) continue; @@ -1873,11 +1888,13 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, } } - // Forward "definitely assigned" dataflow: entry state is the seed; a block's - // entry is the intersection over its predecessors' exits (a member is - // definitely assigned only if assigned on every incoming path); a block's - // exit adds the members it assigns. Unprocessed (unreachable) predecessors - // keep the all-assigned top, so unreachable code is never flagged. + // Forward "definitely assigned" dataflow: nothing is assigned at function + // entry (written initializers generate their writes at their CFGInitializer + // elements); a block's entry is the intersection over its predecessors' + // exits (a member is definitely assigned only if assigned on every incoming + // path); a block's exit adds the members it assigns. Unprocessed + // (unreachable) predecessors keep the all-assigned top, so unreachable code + // is never flagged. std::vector EntryState(NumBlocks, llvm::BitVector(N, true)); std::vector ExitState(NumBlocks, llvm::BitVector(N, true)); const CFGBlock &CFGEntry = cfg->getEntry(); @@ -1886,7 +1903,7 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, while (const CFGBlock *B = Worklist.dequeue()) { llvm::BitVector In(N, true); if (B == &CFGEntry) { - In = Seed; + In = llvm::BitVector(N, false); } else { bool First = true; for (const CFGBlock *Pred : B->preds()) { diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index 536af59bab6e5..daa258411d948 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -168,6 +168,49 @@ struct MarkerWithListInit { MarkerWithListInit() : m(0) { int y = m; (void)y; } }; +// Reads inside the written member-initializer list are checked in execution +// order (declaration order): a member becomes assigned at its own +// initializer, so an earlier member initializer -- or a base initializer, +// which runs before all member initializers -- reading it is a +// read-before-init. +struct InitListReadBefore { + int o; + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListReadBefore() : o(m), m(5) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct InitListReadAfter { + int m [[uninit]]; + int o; + InitListReadAfter() : m(5), o(m) {} // OK: m's initializer runs first +}; + +struct InitListReadNoInit { + int o; + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListReadNoInit() : o(m) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct InitListSelfRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListSelfRead() : m(m + 1) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +struct InitBase { + InitBase(int); +}; +struct InitListBaseRead : InitBase { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + InitListBaseRead() : InitBase(m) {} // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +}; + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] InitListReadSuppressed { + int o; + int m [[uninit]]; + InitListReadSuppressed() : o(m) {} // OK: suppressed +}; + // A delegating constructor's target initializes the members before the body // runs (paper §5.1), so its body is not analyzed. struct Delegating { From ef6c353bf1033703a35d65eb965bfe9fac288644 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 16:52:18 -0400 Subject: [PATCH 192/289] Treat this-capturing lambdas as member reads in the ctor pass The lambda may run immediately; body writes earn no assignment credit. --- clang/docs/ProfilesFramework.rst | 7 ++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 44 +++++++++- .../SemaCXX/safety-profile-init-ctor-body.cpp | 85 +++++++++++++++++++ 3 files changed, 135 insertions(+), 1 deletion(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 1c364f9a7ee4a..20e2fecc0d446 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -818,6 +818,13 @@ read-then-write of that member. Details: ``this->m``, implicitly as ``m``, or as the equivalent ``(*this).m``; an access through any other object (``other.m``) is not the current object's member. +- A ``this``-capturing lambda created in the body may run immediately, so a + member read in its body (or a nested lambda's) counts as a read at the point + the lambda is created; a body write earns no assignment credit (the lambda + may never run). A lambda stored now but called only after the member is + assigned is flagged all the same -- an accepted imprecision. An + init-capture's initializer runs at lambda creation and is checked as an + ordinary read. - There is **no** constructor-exit requirement: a ``[[uninit]]`` member that is simply never read is left as-is (paper §5.1/§5.3), exactly as R5 structurally excuses a marked member -- the two checks are complementary. diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 4df6c325896fd..c053a899d14ae 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1884,6 +1884,44 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, continue; BlockEvents.push_back({ReadWrite, It->second, UO}); Gen[B->getBlockID()].set(It->second); + } else if (const auto *LE = dyn_cast(St)) { + // The lambda body is a separate function and never appears in this + // CFG, but a this-capturing lambda can read members the moment it is + // created (it may be invoked immediately). Treat every member read in + // its body -- and in nested lambda bodies, reached through children() + // -- as a Read at the LambdaExpr's program point. Writes in the body + // earn no assignment credit (the lambda may never run), consistent + // with the intersection semantics; a lambda stored now and called + // only after the member is assigned is still flagged (accepted + // imprecision). Capture initializers are ordinary CFG elements, + // already handled by the arms above. + if (llvm::none_of(LE->captures(), [](const LambdaCapture &C) { + return C.capturesThis(); + })) + continue; + SmallVector Stack(1, LE->getBody()); + while (!Stack.empty()) { + const Stmt *Cur = Stack.pop_back_val(); + if (!Cur) + continue; + const FieldDecl *F = nullptr; + if (const auto *BodyICE = dyn_cast(Cur); + BodyICE && BodyICE->getCastKind() == CK_LValueToRValue) + F = getCurrentObjectMember(BodyICE->getSubExpr()); + else if (const auto *BodyBO = dyn_cast(Cur); + BodyBO && BodyBO->isCompoundAssignmentOp()) + F = getCurrentObjectMember(BodyBO->getLHS()); + else if (const auto *BodyUO = dyn_cast(Cur); + BodyUO && BodyUO->isIncrementDecrementOp()) + F = getCurrentObjectMember(BodyUO->getSubExpr()); + if (F) { + auto It = Index.find(F); + if (It != Index.end()) + BlockEvents.push_back({Read, It->second, cast(Cur)}); + } + for (const Stmt *Child : Cur->children()) + Stack.push_back(Child); + } } } } @@ -3168,7 +3206,11 @@ static void addNonLinearizedAlwaysAddClasses(AnalysisDeclContext &AC) { .setAlwaysAdd(Stmt::CStyleCastExprClass) .setAlwaysAdd(Stmt::DeclRefExprClass) .setAlwaysAdd(Stmt::ImplicitCastExprClass) - .setAlwaysAdd(Stmt::UnaryOperatorClass); + .setAlwaysAdd(Stmt::UnaryOperatorClass) + // The std::init ctor-body pass scans this-capturing lambda bodies from + // the LambdaExpr's element; without always-add, a lambda in a + // non-statement position never gets its own CFG element. + .setAlwaysAdd(Stmt::LambdaExprClass); } // std::init: diagnose a read of a [[uninit]] scalar member before it is diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index daa258411d948..bf3e59bc5a4aa 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -211,6 +211,91 @@ struct [[profiles::suppress(std::init)]] InitListReadSuppressed { InitListReadSuppressed() : o(m) {} // OK: suppressed }; +// A this-capturing lambda created in the ctor body may run immediately, so a +// member read in its body counts as a read at the point the lambda is +// created. Writes in the body earn no assignment credit (the lambda may never +// run), and a lambda stored now but called only after assignment is flagged +// all the same (accepted imprecision). +struct LambdaReadInvoked { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaReadInvoked() { + int y = [this] { return m; }(); // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +struct LambdaReadAfterAssign { + int m [[uninit]]; + LambdaReadAfterAssign() { + m = 1; + auto l = [this] { return m; }; // OK: m assigned on every path here + (void)l; + } +}; + +struct LambdaWriteNoCredit { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaWriteNoCredit() { + auto l = [this] { m = 1; }; // OK: a body write is not a read + l(); + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)y; + } +}; + +struct LambdaNoMemberUse { + int m [[uninit]]; + LambdaNoMemberUse() { + auto l = [this] { return 42; }; // OK: body touches no member + m = l(); + } +}; + +struct LambdaCompoundRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaCompoundRead() { + auto l = [this] { m += 1; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +struct LambdaNestedRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaNestedRead() { + auto l = [this] { return [this] { return m; }; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +struct LambdaImplicitThisCapture { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaImplicitThisCapture() { + auto l = [&] { return m; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +// An init-capture's initializer is an ordinary CFG element of the ctor and is +// checked by the plain read arm, independent of the body scan. +struct LambdaCaptureInitRead { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + LambdaCaptureInitRead() { + auto l = [v = m] { return v; }; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)l; + } +}; + +struct LambdaReadSuppressed { + int m [[uninit]]; + LambdaReadSuppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + auto l = [this] { return m; }; // OK: suppressed + (void)l; + } + } +}; + // A delegating constructor's target initializes the members before the body // runs (paper §5.1), so its body is not analyzed. struct Delegating { From 02d3987ea08bcbbac16db93827805871e5c56fe0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 16:55:45 -0400 Subject: [PATCH 193/289] Check ref_to_uninit on lambda init-capture bindings Init-captures never reach the variable-completion check sites. --- clang/lib/Sema/SemaLambda.cpp | 10 +++++++ .../safety-profile-init-ref-to-uninit.cpp | 27 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index abe00e92a857c..7cafc2ad9b02a 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -911,6 +911,16 @@ VarDecl *Sema::createLambdaInitCaptureVarDecl( NewVD->setInitStyle(static_cast(InitStyle)); NewVD->markUsed(Context); NewVD->setInit(Init); + + // std::init / ref_to_uninit (paper §5): an init-capture binds like a + // variable initialization but never reaches AddInitializerToDecl / + // CheckCompleteVariableDeclaration, so check the binding here. A capture + // cannot carry [[ref_to_uninit]], so only the unmarked-target direction can + // fire. Passing NewVD defers on a templated pattern; TreeTransform re-runs + // this path at instantiation. + Profiles().checkInitProfileRefToUninitBinding(Loc, NewVD, NewVD->getType(), + Init, NewVD); + if (NewVD->isParameterPack()) getCurLambda()->LocalPacks.push_back(NewVD); return NewVD; diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 1b7ab38aac431..d769bb4c498ea 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -296,6 +296,33 @@ void test_lambda_arguments() { l(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} } +// An init-capture is a binding: a capture cannot carry [[ref_to_uninit]], so +// capturing a pointer or reference to uninitialized memory is always the +// unmarked-direction violation. +void test_init_captures() { + auto c1 = [p = &g_init] { (void)p; }; // OK + auto c2 = [p = &g_uninit] { (void)p; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + auto c3 = [&r = *rtu] { (void)r; }; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + auto c4 = [q = rtu] { (void)q; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + auto c5 = [v = g_init] { (void)v; }; // OK: a by-value int copy is not a binding + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + auto c6 = [p = &g_uninit] { (void)p; }; // OK: suppressed + (void)c6; + } + (void)c1; (void)c2; (void)c3; (void)c4; (void)c5; +} + +// An init-capture inside a template body defers on the pattern and fires +// once, at instantiation. +template +void template_init_capture_bad() { + auto c = [p = &g_uninit] { (void)p; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)c; +} +template void template_init_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_init_capture_bad' requested here}} + struct OpTag {}; OpTag operator+(OpTag, int *p [[ref_to_uninit]]); From 755cb66ccfa461f239759655b362a78cb0e79fa9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 16:59:23 -0400 Subject: [PATCH 194/289] Track inherited [[uninit]] members from ctor-less bases Nothing can have assigned them before the derived body; a base with a user-provided constructor stays trusted. --- clang/docs/ProfilesFramework.rst | 7 ++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 76 +++++++++++++++---- .../SemaCXX/safety-profile-init-ctor-body.cpp | 49 ++++++++++++ 3 files changed, 118 insertions(+), 14 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 20e2fecc0d446..0fcf2c1af2ebd 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -818,6 +818,13 @@ read-then-write of that member. Details: ``this->m``, implicitly as ``m``, or as the equivalent ``(*this).m``; an access through any other object (``other.m``) is not the current object's member. +- ``[[uninit]]`` members inherited from a non-virtual base with no + user-provided constructor are tracked like the class's own members (nothing + can have assigned them before the derived body runs); a written base + initializer (``: Base{1}``) counts as assigning that base subtree's tracked + members. A base *with* a user-provided constructor is trusted (paper §5.1) + -- its constructor body may have assigned the member, which this local + analysis cannot see -- so its members are not tracked. - A ``this``-capturing lambda created in the body may run immediately, so a member read in its body (or a nested lambda's) counts as a read at the point the lambda is created; a body write earns no assignment credit (the lambda diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index c053a899d14ae..946d494870af1 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1764,20 +1764,53 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, // (§4.5), matching R1/R2. Class-type and array members (which would need // construct_at flow modeling) and pointers (banned with [[uninit]] by R8) are // out of scope here. + // + // Members inherited from a non-virtual base with NO user-provided + // constructor are tracked too: nothing can have assigned them before the + // derived body runs, so tracking them is sound. A base with a user-provided + // constructor is trusted (paper §5.1) -- its constructor body may have + // assigned the member, which this local analysis cannot see -- and its + // members are left alone. + auto HasUserProvidedCtor = [](const CXXRecordDecl *RD) { + return llvm::any_of(RD->ctors(), [](const CXXConstructorDecl *C) { + return C->isUserProvided(); + }); + }; + // Visit the candidate fields of RD and of its non-virtual, constructor-less + // base classes, recursively. + auto ForEachCandidateField = [&](const CXXRecordDecl *RD, auto &&Visit) { + SmallVector RecordStack(1, RD); + while (!RecordStack.empty()) { + const CXXRecordDecl *Cur = RecordStack.pop_back_val(); + for (const FieldDecl *F : Cur->fields()) + Visit(F); + for (const CXXBaseSpecifier &BS : Cur->bases()) { + if (BS.isVirtual()) + continue; + const CXXRecordDecl *BRD = BS.getType()->getAsCXXRecordDecl(); + if (BRD && BRD->hasDefinition() && + !HasUserProvidedCtor(BRD->getDefinition())) + RecordStack.push_back(BRD->getDefinition()); + } + } + }; + SmallVector Members; llvm::DenseMap Index; - for (const FieldDecl *F : Ctor->getParent()->fields()) { + ForEachCandidateField(Ctor->getParent(), [&](const FieldDecl *F) { if (!F->hasAttr() || !F->getDeclName() || F->hasInClassInitializer()) - continue; + return; QualType T = F->getType(); if (!T->isIntegralOrEnumerationType() && !T->isFloatingType()) - continue; + return; if (S.Context.getBaseElementType(T)->isStdByteType()) - continue; + return; + if (Index.count(F)) + return; Index[F] = Members.size(); Members.push_back(F); - } + }); if (Members.empty()) return; const unsigned N = Members.size(); @@ -1830,16 +1863,31 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, // reads against these writes.) if (auto OptInit = Elem.getAs()) { const CXXCtorInitializer *CI = OptInit->getInitializer(); - if (!CI->isWritten() || !CI->isAnyMemberInitializer()) - continue; - const FieldDecl *F = CI->getAnyMember(); - if (!F) - continue; - auto It = Index.find(F); - if (It == Index.end()) + if (!CI->isWritten()) continue; - BlockEvents.push_back({Write, It->second, CI->getInit()}); - Gen[B->getBlockID()].set(It->second); + if (CI->isAnyMemberInitializer()) { + const FieldDecl *F = CI->getAnyMember(); + if (!F) + continue; + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({Write, It->second, CI->getInit()}); + Gen[B->getBlockID()].set(It->second); + } else if (CI->isBaseInitializer()) { + // A written base initializer (e.g. `: Base{1}`) gives the tracked + // members of that constructor-less base subtree their values. + const auto *BRD = CI->getBaseClass()->getAsCXXRecordDecl(); + if (!BRD || !BRD->hasDefinition()) + continue; + ForEachCandidateField(BRD->getDefinition(), [&](const FieldDecl *F) { + auto It = Index.find(F); + if (It == Index.end()) + return; + BlockEvents.push_back({Write, It->second, CI->getInit()}); + Gen[B->getBlockID()].set(It->second); + }); + } continue; } auto OptStmt = Elem.getAs(); diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index bf3e59bc5a4aa..0c66d83acf245 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -296,6 +296,55 @@ struct LambdaReadSuppressed { } }; +// [[uninit]] members inherited from a non-virtual base with no user-provided +// constructor are tracked like the class's own: nothing can have assigned +// them before the derived body runs. A base with a user-provided constructor +// is trusted (paper §5.1) and its members left alone. +struct BaseRead { + int bm [[uninit]]; // expected-note {{member 'bm' declared here}} +}; +struct DerivedRead : BaseRead { + DerivedRead() { int y = bm; (void)y; } // expected-error {{member 'bm' is read before initialization under profile 'std::init'}} +}; + +struct BaseAssign { + int bm [[uninit]]; +}; +struct DerivedAssign : BaseAssign { + DerivedAssign() { bm = 1; int y = bm; (void)y; } // OK +}; + +struct BaseWritten { + int bm [[uninit]]; +}; +struct DerivedWrittenBaseInit : BaseWritten { + DerivedWrittenBaseInit() : BaseWritten{1} { int y = bm; (void)y; } // OK: written base initializer +}; + +struct BaseTrusted { + int bm [[uninit]]; + BaseTrusted() {} +}; +struct DerivedTrustedBase : BaseTrusted { + DerivedTrustedBase() { int y = bm; (void)y; } // OK: base ctor trusted (paper §5.1) +}; + +struct GrandBase { + int gm [[uninit]]; // expected-note {{member 'gm' declared here}} +}; +struct MidBase : GrandBase {}; +struct DerivedTwoLevels : MidBase { + DerivedTwoLevels() { int y = gm; (void)y; } // expected-error {{member 'gm' is read before initialization under profile 'std::init'}} +}; + +struct BaseSup { + int bm [[uninit]]; +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] DerivedSup : BaseSup { + DerivedSup() { int y = bm; (void)y; } // OK: suppressed +}; + // A delegating constructor's target initializes the members before the body // runs (paper §5.1), so its body is not analyzed. struct Delegating { From 838e29546228e93f15931c317f5d5ab5e52408b0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 17:02:49 -0400 Subject: [PATCH 195/289] Check ref_to_uninit on new-expression initializers A heap pointer cannot carry the marker, so binding it to uninitialized memory is always the unmarked-direction violation. --- clang/lib/Sema/SemaExprCXX.cpp | 13 ++++++++ .../safety-profile-init-ref-to-uninit.cpp | 31 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index 5a5bbf4d900dc..a8f0739d77d05 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -2607,6 +2607,19 @@ ExprResult Sema::BuildCXXNew(SourceRange Range, bool UseGlobal, Initializer = FullInit.get(); + // std::init / ref_to_uninit (paper §5): a written initializer for an + // allocated pointer binds it like a variable initialization -- but a heap + // pointer object cannot carry [[ref_to_uninit]], so binding it to + // uninitialized memory is always the unmarked-direction violation. A + // braced `new T*{&x}` presents the InitListExpr, which the recognizer's + // single-element pass-through looks through. Dependent operands are + // outside this block; BuildCXXNew re-runs at instantiation, and the + // Decl-less wrapper defers on a template pattern. + if (AllocType->isPointerType() && Exprs.size() == 1) + Profiles().checkInitProfileRefToUninit(Exprs[0]->getExprLoc(), + /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Exprs[0]); + // FIXME: If we have a KnownArraySize, check that the array bound of the // initializer is no greater than that constant value. diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index d769bb4c498ea..d3fddb5856ecd 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -530,6 +530,37 @@ void test_new_assignment() { (void)p; (void)q; } +// A written initializer for an *allocated pointer* is itself a binding: the +// heap pointer object cannot carry [[ref_to_uninit]], so it must not be bound +// to uninitialized memory. Both the parenthesized and the braced form are +// checked; copying the value of a marked pointer is the same violation, as at +// variable scope. +void test_new_pointer_init() { + int **n1 = new (int *)(&g_init); // OK + int **n2 = new (int *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int **n3 = new (int *){&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + int **n4 = new (int *)(rtu); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int **n5 [[ref_to_uninit]] = new (int *); // OK: no written initializer -- the + // allocated pointer is indeterminate, + // so the marked target accepts it + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { + int **s = new (int *)(&g_uninit); // OK: suppressed + (void)s; + } + (void)n1; (void)n2; (void)n3; (void)n4; (void)n5; +} + +// A dependent allocated type defers on the pattern and fires once, at +// instantiation. +template +void template_new_pointer_bad() { + T **p = new (T *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_new_pointer_bad(); // expected-note {{in instantiation of function template specialization 'template_new_pointer_bad' requested here}} + void test_new_call_arguments() { take_uninit_ptr(new int); // OK take_uninit_ptr(new int(5)); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} From b50537118e0a72bdbcf1c97c69462e5560f2d7ff Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 17:07:43 -0400 Subject: [PATCH 196/289] Check ref_to_uninit in parenthesized aggregate initialization Mirrors the braced-list field hooks for the C++20 paren form. --- clang/lib/Sema/SemaInit.cpp | 9 ++++++++ .../safety-profile-init-ref-to-uninit.cpp | 22 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 4332b5f065b81..8084c795c816f 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -6139,6 +6139,15 @@ static void TryOrBuildParenListInitialization( if (!HandleInitializedEntity(SubEntity, SubKind, E)) return; + // std::init / ref_to_uninit (paper §5): a pointer or reference field + // initialized from a C++20 parenthesized aggregate list is a member + // binding, checked exactly like the braced-list hooks in + // CheckSubElementType / CheckReferenceType. Only in the build phase, + // so the verify pass does not double-diagnose. + if (!VerifyOnly) + S.Profiles().checkInitProfileRefToUninitBinding(E->getExprLoc(), FD, + FD->getType(), E); + // Unions should have only one initializer expression, so we bail out // after processing the first field. If there are more initializers then // it will be caught when we later check whether EntityIndexToProcess is diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index d3fddb5856ecd..e5b88eadfd5a8 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -963,6 +963,28 @@ void template_aggregate_never() { (void)a; } +// C++20 parenthesized aggregate initialization performs the same per-field +// bindings as the braced form and is checked identically. +void test_aggregate_paren() { + AggPtr a1(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr a2(&g_init); // OK + AggPtrMarked m1(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggPtrMarked m2(&g_uninit); // OK + AggRef r1(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggRefMarked r2(g_init); // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + AggNested n1((AggPtr(&g_uninit))); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] AggPtr s(&g_uninit); // OK: suppressed + (void)a1; (void)a2; (void)m1; (void)m2; (void)r1; (void)r2; (void)n1; (void)s; +} + +template +void template_aggregate_paren_bad() { + AggPtr a(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} +template void template_aggregate_paren_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_paren_bad' requested here}} + // A plain pointer *variable* with a braced initializer is checked once at its // own variable site (EK_Variable); the aggregate field hooks are scoped to a // member subobject, so this fires exactly once with no new duplicate. From b53fa12d2e0f84e7181d13b74842a3a4d38c0b99 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 17:12:45 -0400 Subject: [PATCH 197/289] Re-check [[uninit]] against reference types at instantiation A dependent subject substituting to a reference now drops the marker with the parse-time rejection's diagnostic. --- clang/include/clang/Sema/SemaProfiles.h | 12 +++++++++ clang/lib/Sema/SemaDeclAttr.cpp | 9 +++++-- clang/lib/Sema/SemaProfiles.cpp | 21 ++++++++++++++++ .../lib/Sema/SemaTemplateInstantiateDecl.cpp | 7 ++++++ .../safety-profile-init-field-marker.cpp | 25 +++++++++++++++++++ 5 files changed, 72 insertions(+), 2 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 6ca97af1d7748..15e527e967b16 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -246,6 +246,18 @@ class SemaProfiles : public SemaBase { bool diagnoseInvalidRefToUninitMarker(const Decl *D, SourceLocation AttrLoc, bool Diagnose = true); + /// [[uninit]] is meaningless on a reference (it must bind when declared). + /// Returns true when \p D's type is a reference, diagnosing + /// err_uninit_attr_invalid_subject at \p AttrLoc unless \p Diagnose is + /// false. A dependent type returns false: validation defers to the + /// instantiation re-check in Sema::InstantiateAttrs, which drops the marker + /// when the substituted type is a reference (silently in a SFINAE context). + /// The parameter / structured-binding rejections stay in the parse-time + /// handler -- they do not depend on the type. Not profile policy -- fires + /// regardless of -fprofiles. + bool diagnoseInvalidUninitMarker(const Decl *D, SourceLocation AttrLoc, + bool Diagnose = true); + class ProfileSuppressScope { Sema &S; unsigned Count = 0; diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 0ad1134e34e0d..2b14bd19ebd1e 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -6964,8 +6964,6 @@ static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { Invalid = Parameter; else if (isa(D)) Invalid = StructuredBinding; - else if (cast(D)->getType()->isReferenceType()) - Invalid = Reference; if (Invalid) { S.Diag(AL.getLoc(), diag::err_uninit_attr_invalid_subject) @@ -6974,6 +6972,13 @@ static void handleUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { return; } + // A reference subject is rejected by the shared helper, which defers on a + // dependent type to the instantiation re-check in Sema::InstantiateAttrs. + if (S.Profiles().diagnoseInvalidUninitMarker(D, AL.getLoc())) { + AL.setInvalid(); + return; + } + D->addAttr(::new (S.Context) UninitAttr(S.Context, AL)); // std::init / union_marker + pointer_marker (paper §4.1, §5.6). Shared with diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index b352d1c49006e..3430d0662d664 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -572,6 +572,27 @@ void SemaProfiles::checkInitProfileMarkerPlacement(const Decl *D) { Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; } +bool SemaProfiles::diagnoseInvalidUninitMarker(const Decl *D, + SourceLocation AttrLoc, + bool Diagnose) { + const auto *VD = dyn_cast(D); + if (!VD) + return false; + QualType T = VD->getType(); + + // A dependent subject is validated at instantiation instead, once the + // substituted type is known (Sema::InstantiateAttrs). + if (T->isDependentType()) + return false; + + if (T->isReferenceType()) { + if (Diagnose) + Diag(AttrLoc, diag::err_uninit_attr_invalid_subject) << /*Reference=*/0u; + return true; + } + return false; +} + bool SemaProfiles::diagnoseInvalidRefToUninitMarker(const Decl *D, SourceLocation AttrLoc, bool Diagnose) { diff --git a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp index 82393dc492cba..35cba95de5aa1 100644 --- a/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp +++ b/clang/lib/Sema/SemaTemplateInstantiateDecl.cpp @@ -885,6 +885,13 @@ void Sema::InstantiateAttrs(const MultiLevelTemplateArgumentList &TemplateArgs, New, TmplAttr->getLocation(), /*Diagnose=*/!isSFINAEContext())) continue; + // Likewise for [[uninit]]: the handler deferred the reference-type + // rejection on a dependent subject. + if (isa(TmplAttr) && + Profiles().diagnoseInvalidUninitMarker(New, TmplAttr->getLocation(), + /*Diagnose=*/!isSFINAEContext())) + continue; + // FIXME: This should be generalized to more than just the AlignedAttr. const AlignedAttr *Aligned = dyn_cast(TmplAttr); if (Aligned && Aligned->isAlignmentDependent()) { diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 9c2ccd18aff09..8ac28808d210d 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -64,6 +64,31 @@ struct DependentFieldNeverInstantiated { T m [[uninit]]; }; +// A dependent member instantiating to a *reference* type is rejected at +// instantiation and the marker dropped, mirroring the parse-time rejection of +// a non-dependent reference member. Like that rejection -- and unlike the +// profile-gated pointer/union rules -- this fires regardless of -fprofiles. +template +struct DependentRefField { + T m [[uninit]]; // #dependent-ref-field +}; +template struct DependentRefField; // OK +template struct DependentRefField; // expected-note {{in instantiation of template class 'DependentRefField' requested here}} \ + // no-profiles-note {{in instantiation of template class 'DependentRefField' requested here}} +// expected-error@#dependent-ref-field {{'uninit' attribute cannot be applied to a reference}} +// no-profiles-error@#dependent-ref-field {{'uninit' attribute cannot be applied to a reference}} + +int g_ref_target = 0; +template +void dependent_ref_local() { + T v [[uninit]] = g_ref_target; // #dependent-ref-local + (void)v; +} +template void dependent_ref_local(); // expected-note {{in instantiation of function template specialization 'dependent_ref_local' requested here}} \ + // no-profiles-note {{in instantiation of function template specialization 'dependent_ref_local' requested here}} +// expected-error@#dependent-ref-local {{'uninit' attribute cannot be applied to a reference}} +// no-profiles-error@#dependent-ref-local {{'uninit' attribute cannot be applied to a reference}} + // Suppression on the dependent member carries through instantiation. template struct DependentFieldSuppressed { From 756bb00fdec5e9dc442457a6b1eb5c1fd1bcc237 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 17:15:24 -0400 Subject: [PATCH 198/289] Check ref_to_uninit on thrown pointer operands The exception object cannot carry the marker. --- clang/lib/Sema/SemaExprCXX.cpp | 12 +++++++++ .../safety-profile-init-ref-to-uninit.cpp | 25 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index a8f0739d77d05..ef9793bf20fa3 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -905,6 +905,18 @@ ExprResult Sema::BuildCXXThrow(SourceLocation OpLoc, Expr *Ex, ExprResult Res = PerformMoveOrCopyInitialization(Entity, NRInfo, Ex); if (Res.isInvalid()) return ExprError(); + + // std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes + // the exception object, which cannot carry [[ref_to_uninit]], so throwing + // a pointer to uninitialized memory is always the unmarked-direction + // violation. (Reads like `throw *p` funnel through the read-through check + // instead.) Dependent operands are outside this block; the Decl-less + // wrapper defers on a template pattern. + if (ExceptionObjectTy->isPointerType()) + Profiles().checkInitProfileRefToUninit(Ex->getExprLoc(), + /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Ex); + Ex = Res.get(); } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index e5b88eadfd5a8..c514fb028a2fc 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1,5 +1,5 @@ -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s -// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fcxx-exceptions -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -fcxx-exceptions -std=c++23 %s // std::init / ref_to_uninit (paper §5): a [[ref_to_uninit]] pointer or // reference must be bound to uninitialized memory, and an unmarked pointer or @@ -963,6 +963,27 @@ void template_aggregate_never() { (void)a; } +// A thrown pointer copy-initializes the exception object, which cannot carry +// [[ref_to_uninit]], so it must not point to uninitialized memory. A read +// like `throw *p` is the read-through check's territory instead. +void throw_ptr_ok() { throw &g_init; } // OK +void throw_ptr_bad() { throw &g_uninit; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +void throw_marked_ptr_bad(int *p [[ref_to_uninit]]) { throw p; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +void throw_ptr_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { throw &g_uninit; } // OK: suppressed +} + +// A dependent thrown operand is rebuilt at instantiation, where the check +// fires. A fully non-dependent throw inside a template is not rebuilt by +// TreeTransform, so -- like the reinterpret_cast site -- the pattern defers +// and the case goes undiagnosed; a known limitation. +template +void template_throw_bad() { + throw (T *)&g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_throw_bad(); // expected-note {{in instantiation of function template specialization 'template_throw_bad' requested here}} + // C++20 parenthesized aggregate initialization performs the same per-field // bindings as the braced form and is checked identically. void test_aggregate_paren() { From bf078b905d0b957432b41f53b34e06bf54c67cd2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 17:19:17 -0400 Subject: [PATCH 199/289] Diagnose self-initialization reads under CFG uninit profiles A self-init records no uses-vec entry; report it at the root cause. --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 19 ++++++++++++++++++ .../test/SemaCXX/safety-profile-init-read.cpp | 20 +++++++++++++++++++ .../SemaCXX/safety-profile-uninit-read.cpp | 10 +++++++--- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 946d494870af1..01373f92abf19 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2123,6 +2123,25 @@ class UninitValsDiagReporter : public UninitVariablesHandler { // diagnostic is printed, further diagnostics for this variable are skipped. void diagnoseUnitializedVar(const VarDecl *vd, bool hasSelfInit, UsesVec *vec) { + // A self-init (`int x = x;`) reads the uninitialized variable in its own + // initializer, but records no entry in the uses vec, so check it first -- + // at the root cause, mirroring the default path's self-init preference + // below. + if (hasSelfInit && vd->getInit()) { + const Expr *Init = vd->getInit()->IgnoreParenCasts(); + for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + if (E.ExemptStdByte && + S.Context.getBaseElementType(vd->getType())->isStdByteType()) + continue; + if (!S.Profiles().shouldEmitProfileViolation(E.Name, E.Rule, Init, AC)) + continue; + S.Diag(Init->getBeginLoc(), E.DiagID) << E.Name << vd->getDeclName(); + S.Diag(vd->getLocation(), diag::note_var_declared_here) + << vd->getDeclName(); + return; + } + } + // If a CFG-uninit-analysis-requesting profile is enforced at any real // use site and is not suppressed there, emit the profile diagnostic // and skip the default warning path entirely. diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index f836a6b24daa2..f34c01dc8df92 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -125,6 +125,26 @@ void test_byte_read_exempt() { (void)c; } +// A self-init reads the uninitialized variable in its own initializer and is +// reported at the root cause, even with no later use. +void test_self_init() { + int x = x; // expected-error {{variable 'x' is read before initialization under profile 'std::init'}} \ + // expected-note {{variable 'x' is declared here}} + (void)&x; +} + +void test_self_init_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int x = x; // OK: suppressed + (void)&x; +} + +// A std::byte self-init stays exempt (paper section 4). +void test_self_init_byte_exempt() { + std::byte b = b; + (void)&b; +} + void take_const_ref(const int &); void take_const_ptr(const int *); diff --git a/clang/test/SemaCXX/safety-profile-uninit-read.cpp b/clang/test/SemaCXX/safety-profile-uninit-read.cpp index 695b6969c3ad4..a6b8e4f48aa9b 100644 --- a/clang/test/SemaCXX/safety-profile-uninit-read.cpp +++ b/clang/test/SemaCXX/safety-profile-uninit-read.cpp @@ -63,8 +63,11 @@ void test_global() { (void)y; } +// A self-init reads the uninitialized variable in its own initializer; the +// profile reports it at the root cause even when there is no later use. void test_self_init_no_use() { - int x = x; + int x = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} \ + // expected-note {{variable 'x' is declared here}} (void)&x; } @@ -101,8 +104,9 @@ void instantiate_template_uninit() { } void test_self_init_with_use() { - int x = x; // expected-note {{variable 'x' is declared here}} - int y = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} + int x = x; // expected-error {{variable 'x' is read before initialization under profile 'test::uninit_read'}} \ + // expected-note {{variable 'x' is declared here}} + int y = x; (void)y; } From 7989eb82f4f58013b5c3f60481def7bee3ecb712 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 18:25:18 -0400 Subject: [PATCH 200/289] Check [[uninit]] marker placement against the base element type --- clang/docs/ProfilesFramework.rst | 9 +++++++ .../clang/Basic/DiagnosticSemaKinds.td | 2 +- clang/lib/Sema/SemaProfiles.cpp | 25 +++++++++++++------ .../test/SemaCXX/safety-profile-init-decl.cpp | 19 ++++++++++++++ .../SemaCXX/safety-profile-init-static.cpp | 5 ++++ .../SemaCXX/safety-profile-init-union.cpp | 23 +++++++++++++++++ 6 files changed, 74 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 0fcf2c1af2ebd..8136febf3841e 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -982,6 +982,11 @@ assignment when compiled without the profile. profile. Being Decl-aware it defers on a templated pattern and fires once on the instantiation (a dependent member or local that substitutes to a union), consistent with the other ``std::init`` rules. +- The rule keys on the *base element type*, so an array of unions + (``[[uninit]] U a[2];``) is banned exactly like a single union object. A + union-typed data member of a non-union class is banned as well -- delayed + initialization by assigning one of its members is just as erroneous there -- + in addition to the members *of* a union shown above. - The banned marker is retained on the declaration after it is diagnosed, so the ``uninit_decl`` / ``ctor_uninit_member`` rules treat the entity as acknowledged and do not emit a second, contradictory diagnostic. @@ -1126,6 +1131,10 @@ A pointer must instead be initialized (e.g. to ``nullptr``). enforcement -- a pointer may legitimately carry the marker without the profile -- and the marker is retained after the diagnostic so ``uninit_decl`` does not also fire. +- The check keys on the base element type, so an array of pointers + (``[[uninit]] int *a[2];``) is banned exactly like a single pointer -- the + marker cannot smuggle uninitialized pointers past ``uninit_decl`` + element-wise. - A pointer parameter is rejected earlier and unconditionally (the marker is meaningless on a parameter); a pointer-to-member is not a pointer type and is out of scope. diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 6e273f7de2b1d..56d46b314aeae 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14166,7 +14166,7 @@ def err_ref_to_uninit_attr_invalid_type : Error< "functions returning them">; def err_init_union_marker : ProfileRuleError< "'[[uninit]]' cannot be applied to %select{a variable of union type|" - "a union member}1 under profile '%0'">; + "a union member|a data member of union type}1 under profile '%0'">; def err_init_uninit_pointer_marker : ProfileRuleError< "'[[uninit]]' cannot be applied to a pointer under profile '%0'; " "initialize the pointer (for example to 'nullptr')">; diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 3430d0662d664..3b10ad6faa204 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -464,10 +464,11 @@ void SemaProfiles::checkInitProfileStaticMarker(const VarDecl *Var) { (Var->getStorageDuration() == SD_Static || Var->getStorageDuration() == SD_Thread) && Var->hasAttr() && - // A union or pointer object marked [[uninit]] is already rejected by - // union_marker / pointer_marker (regardless of storage duration), and - // they retain the marker; do not pile a second diagnostic on top. - !Var->getType()->isUnionType() && !Var->getType()->isPointerType() && + // A union or pointer object -- or an array of them -- marked [[uninit]] + // is already rejected by union_marker / pointer_marker (regardless of + // storage duration, and keyed on the same base element type), and they + // retain the marker; do not pile a second diagnostic on top. + !BaseTy->isUnionType() && !BaseTy->isPointerType() && shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), Var) && (!Var->getInit() || @@ -560,14 +561,22 @@ void SemaProfiles::checkInitProfileMarkerPlacement(const Decl *D) { // so the parse-time handler skips template members and the rule is re-run on // the instantiated entity (VisitFieldDecl / VisitVarDecl), once the // substituted type is known to be a pointer or union. - bool UnionVar = isa(D) && cast(D)->getType()->isUnionType(); + // + // Both rules key on the base element type: an array of unions or pointers + // leaves the same uninitialized elements as a single one, and the marker + // would otherwise slip past uninit_decl (which trusts marked declarations) + // entirely. The union rule also covers a union-typed data member of a + // non-union class -- delayed initialization by assigning its member is just + // as erroneous there (paper §5.6). + QualType BaseTy = + getASTContext().getBaseElementType(cast(D)->getType()); bool UnionMember = isa(D) && cast(D)->getParent()->isUnion(); - if ((UnionVar || UnionMember) && + if ((BaseTy->isUnionType() || UnionMember) && shouldEmitProfileViolation("std::init", "union_marker", Loc, D)) Diag(Loc, diag::err_init_union_marker) - << "std::init" << (UnionMember ? 1 : 0); - else if (cast(D)->getType()->isPointerType() && + << "std::init" << (UnionMember ? 1 : isa(D) ? 2 : 0); + else if (BaseTy->isPointerType() && shouldEmitProfileViolation("std::init", "pointer_marker", Loc, D)) Diag(Loc, diag::err_init_uninit_pointer_marker) << "std::init"; } diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index f91dad93f8275..440daf15f8356 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -45,6 +45,25 @@ struct PtrMember { int* p [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} }; +// The marker checks key on the base element type: an array of pointers is +// banned exactly like a single pointer (paper section 4.1) -- the marker must +// not smuggle uninitialized pointers past uninit_decl element-wise. +void test_pointer_array() { + [[uninit]] int* a[2]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + [[uninit]] int* b[2][3]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + (void)a; (void)b; +} + +struct PtrArrayMember { + [[uninit]] int* a[2]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +}; + +void test_pointer_array_marker_suppressed() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "pointer_marker")]] [[uninit]] int* a[2]; + (void)a; +} + void test_pointer_marker_suppressed() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] int* p [[uninit]]; diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp index 9a49f776c8dea..12d1af1fdb85f 100644 --- a/clang/test/SemaCXX/safety-profile-init-static.cpp +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -96,6 +96,11 @@ union U { int x; float y; }; static int *g_ptr_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} static U g_union_marked [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} +// Arrays of pointers / unions key on the base element type: still owned by +// pointer_marker / union_marker, not static_marker (one diagnostic each). +[[uninit]] static int *g_ptr_arr_marked[2]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +[[uninit]] static U g_union_arr_marked[2]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + // Unmarked statics / thread-locals are fine (zero-initialized, nothing to mark). int g_unmarked; static int g_static_unmarked; diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index da4753c8b3936..cc65b6abf2d76 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -24,6 +24,24 @@ union MarkedMember { float y; }; +// The marker checks key on the base element type: an array of unions is +// banned exactly like a single union object (paper section 5.6). +[[uninit]] U g_union_arr[2]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + +void test_union_array_var() { + [[uninit]] U a[2]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + [[uninit]] U b[2][3]; // expected-error {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + (void)a; (void)b; +} + +// A union-typed data member of a non-union class cannot carry the marker +// either: delayed initialization by assigning one of its members would be +// just as erroneous there (paper section 5.6). +struct HasMarkedUnionMember { + U u [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a data member of union type under profile 'std::init'}} + [[uninit]] U arr[2]; // expected-error {{'[[uninit]]' cannot be applied to a data member of union type under profile 'std::init'}} +}; + // A marker on a union member of a non-enforced profile is silently accepted; // exercised by the no-profiles run above. @@ -71,6 +89,11 @@ void template_union_marker() { template void template_union_marker(); // expected-note {{in instantiation of function template specialization 'template_union_marker' requested here}} // expected-error@#template-union-marker {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} +// A dependent local that substitutes to an *array of* unions fires the same +// way (base element type) at instantiation. +template void template_union_marker(); // expected-note {{in instantiation of function template specialization 'template_union_marker' requested here}} +// expected-error@#template-union-marker {{'[[uninit]]' cannot be applied to a variable of union type under profile 'std::init'}} + // An uninstantiated pattern never reaches phase 7, so the marker is silent. template void template_union_never_instantiated() { From 07e6c0c589ee029ace6642d393cab308250c9216 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 18:34:31 -0400 Subject: [PATCH 201/289] Diagnose subobject reads of [[uninit]] objects in the read-through check --- clang/docs/ProfilesFramework.rst | 29 ++++--- .../clang/Basic/DiagnosticSemaKinds.td | 3 +- clang/lib/Sema/SemaProfiles.cpp | 40 ++++++++-- .../test/SemaCXX/safety-profile-init-read.cpp | 11 +++ .../safety-profile-init-ref-to-uninit.cpp | 76 +++++++++++++++++++ 5 files changed, 142 insertions(+), 17 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 8136febf3841e..89941d93e5eb8 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1074,18 +1074,25 @@ recognizers symmetric. (``Sema::DefaultLvalueConversion`` calling ``SemaProfiles::checkInitProfileReadThrough``), which by-value reads -- copy-initialization, by-value arguments, returns, and operator/condition operands -- all funnel through. It reuses the recognizer - in a read-only mode (a ``ForRead`` flag that drops the directly-named - ``[[uninit]]`` arm, leaving a direct read of such a named object to the - flow-based ``uninit_read`` pass while still recognizing indirection through a - ``[[ref_to_uninit]]`` pointer/reference), reports the shared rule + in a read-only mode (a ``ForRead`` flag), reports the shared rule ``uninit_read`` via ``err_init_uninit_read_through``, and exempts ``std::byte`` - (paper §4.5). Being Decl-less, it defers on a dependent context and fires - once, at instantiation. An address-of (``&*p``), a reference binding, a - discarded-value expression (``(void)*p``), and a write (``*p = 5``) apply no - lvalue-to-rvalue conversion and so are not reads. Out of scope, as remaining - limitations: class-type read-through (copy construction from ``*p``) and - compound-assignment reads (``*p += 1``, which build no lvalue-to-rvalue node), - consistent with the scalar slice and the deferred-writes stance. + (paper §4.5). Read-only mode drops the ``[[uninit]]`` marker only for the + *top-level* named entity -- a directly named ``[[uninit]]`` object and a + current-object member are flow-tracked (by the CFG ``uninit_read`` pass and + the ctor-body pass, which credit assignments), so their direct reads are left + to those passes. A *subobject* read of a named ``[[uninit]]`` object + (``s.x``, ``o.agg.f``) is recognized and diagnosed here: neither flow pass + tracks it, and member-wise delayed initialization of an ``[[uninit]]`` object + is itself banned (paper §5.4; only whole-object ``construct_at`` + re-initializes, which is uniformly unmodeled), so no assignment could have + given the member a value. Being Decl-less, it defers on a dependent context + and fires once, at instantiation. An address-of (``&*p``), a reference + binding, a discarded-value expression (``(void)*p``), and a write (``*p = 5`` + or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads. + Out of scope, as remaining limitations: class-type read-through (copy + construction from ``*p``) and compound-assignment reads (``*p += 1``, which + build no lvalue-to-rvalue node), consistent with the scalar slice and the + deferred-writes stance. - Known gaps: recognition is purely of the source's syntactic form, so a binding whose underlying operand is unrecognized -- pointer arithmetic, an integer-to-pointer cast, a call through a function pointer (no diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 56d46b314aeae..0712095d89d9c 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14188,6 +14188,7 @@ def err_init_uninit_requires_ref_to_uninit : ProfileRuleError< "%select{pointer|reference}1 to uninitialized memory must be marked " "'[[ref_to_uninit]]' under profile '%0'">; def err_init_uninit_read_through : ProfileRuleError< - "read through a '[[ref_to_uninit]]' pointer or reference accesses " + "read %select{through a '[[ref_to_uninit]]' pointer or reference|" + "of a member of an '[[uninit]]' object}1 accesses " "uninitialized memory under profile '%0'">; } // end of sema component. diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 3b10ad6faa204..9162c7366bb6d 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -800,12 +800,20 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, if (const auto *ME = dyn_cast(E)) { // a->m reaches m through the pointer a (object *a); a.m through the // glvalue a. When m does not itself denote uninit storage, the subobject is - // uninit exactly when its base is. + // uninit exactly when its base is. The base recursion leaves read mode: + // the ForRead drop exists because a *directly named* [[uninit]] object and + // a current-object member are flow-tracked (by the CFG uninit_read pass + // and the ctor-body pass, which credit assignments), but neither pass + // tracks a subobject reached through a member access -- and member-wise + // delayed initialization of an [[uninit]] object is itself banned (paper + // §5.4; only whole-object construct_at re-initializes, which is uniformly + // unmodeled) -- so below the top level the marker counts even for a read. if (DeclDenotesUninit(ME->getMemberDecl())) return UninitStorage::Uninitialized; - return ME->isArrow() - ? pointerRefersToUninitStorage(Ctx, ME->getBase(), ForRead) - : glvalueDenotesUninitStorage(Ctx, ME->getBase(), ForRead); + return ME->isArrow() ? pointerRefersToUninitStorage(Ctx, ME->getBase(), + /*ForRead=*/false) + : glvalueDenotesUninitStorage(Ctx, ME->getBase(), + /*ForRead=*/false); } // A call to a [[ref_to_uninit]]-returning reference function: the referent // it returns is uninitialized. @@ -892,6 +900,27 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, T->isReferenceType(), Src, D); } +// The read-through diagnostic distinguishes indirection through a +// [[ref_to_uninit]] pointer/reference from a subobject read of a named +// [[uninit]] object, which involves no [[ref_to_uninit]] entity. Approximate +// but sufficient for phrasing: walk up the dot member chain; the read is the +// latter form iff the chain reaches an [[uninit]]-marked member (e.g. the +// class-type member in this->agg.f) or bottoms out at a named [[uninit]] +// declaration. An arrow access reaches its object through a pointer, so the +// pointer wording applies from there on. +static bool isMemberChainOfUninitObject(const Expr *E) { + E = E->IgnoreParenImpCasts(); + while (const auto *ME = dyn_cast(E)) { + if (ME->getMemberDecl()->hasAttr()) + return true; + if (ME->isArrow()) + return false; + E = ME->getBase()->IgnoreParenImpCasts(); + } + const auto *DRE = dyn_cast(E); + return DRE && DRE->getDecl()->hasAttr(); +} + void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType) { @@ -909,7 +938,8 @@ void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, /*ForRead=*/true) != UninitStorage::Uninitialized) return; - Diag(Loc, diag::err_init_uninit_read_through) << "std::init"; + Diag(Loc, diag::err_init_uninit_read_through) + << "std::init" << (isMemberChainOfUninitObject(Glvalue) ? 1 : 0); } diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index f34c01dc8df92..86fb39193c6c6 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -160,6 +160,17 @@ void test_const_ptr_use_is_not_a_read() { int x [[uninit]]; take_const_ptr(&x); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } + +// A read of a member of an [[uninit]] aggregate local is the read-through +// check's (this CFG pass does not track member accesses of record locals, and +// member-wise delayed initialization is banned, paper section 5.4). Full +// coverage lives in safety-profile-init-ref-to-uninit.cpp. +struct Agg { int m; }; +void test_member_read_of_uninit_aggregate() { + Agg s [[uninit]]; + int y = s.m; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} #endif #ifdef DEMOTE diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index c514fb028a2fc..32804ebc0be79 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -724,6 +724,82 @@ void test_read_negatives(int *p [[ref_to_uninit]], int &r [[ref_to_uninit]], (void)ap; (void)q; (void)r2; } +// A subobject read of a named [[uninit]] object loads an uninitialized value, +// exactly like a read through a [[ref_to_uninit]] pointer: member-wise delayed +// initialization of an [[uninit]] object is banned (paper §5.4), so no +// assignment could have given the member a value. Only the *whole-object* +// direct read of a named [[uninit]] entity is left to the flow-based +// uninit_read pass (which credits assignments). +struct Pair { int x; int y; }; +struct PairHolder { Pair p; }; + +void test_member_read_of_uninit_object() { + Pair s [[uninit]]; + int y1 = s.x; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + take_value(s.y); // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + PairHolder o [[uninit]]; + int y2 = o.p.x; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + // The arrow spelling reaches the member through a pointer, so the + // diagnostic's phrasing approximation picks the pointer wording; the read is + // diagnosed all the same. + int y3 = (&s)->x; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y1; (void)y2; (void)y3; +} + +// Writes, discarded values, and address-taking apply no lvalue-to-rvalue +// conversion and are not reads; taking the member's address is the binding +// checks' territory (unchanged behavior, retested as a regression guard). +void test_member_read_negatives() { + Pair s [[uninit]]; + s.x = 1; // OK: write, not a read (writes are a deferred slice) + (void)s.x; // OK: discarded value + int *p = &s.x; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q [[ref_to_uninit]] = &s.x; // OK: binding checked by ref_to_uninit + (void)p; (void)q; +} + +void test_member_read_suppress() { + Pair s [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(s.x); } // OK +} + +// std::byte members stay exempt (paper §4.5). +struct WithByte { std::byte b; int i; }; +void test_member_read_byte_exempt() { + WithByte s [[uninit]]; + std::byte b = s.b; // OK + (void)b; +} + +// The §5.2 trust pattern is preserved: a scalar [[uninit]] *member* of the +// current object may be assigned in the constructor body (flow-checked by the +// ctor-body pass), so a member-function read of it is not flagged here. +struct BodyInit { + int m [[uninit]]; + BodyInit() { m = 1; } + int get() { return m; } // OK: trusted, assigned in the constructor body +}; + +// But a subobject of an [[uninit]] *class-type member* has no legal +// assignment path (member-wise delayed initialization is banned, and +// construct_at flow is uniformly unmodeled), so its read is flagged even on +// the current object. +struct HasAggMember { + Pair agg [[uninit]]; + int get() { return agg.x; } // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +}; + +// Like every Decl-less read check, the member read defers on a template +// pattern and fires once, at instantiation. +template +void template_member_read_bad() { + Pair s [[uninit]]; + int y = s.x; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_member_read_bad(); // expected-note {{in instantiation of function template specialization 'template_member_read_bad' requested here}} + // A read through a [[ref_to_uninit]] parameter inside a template body defers on // the pattern (a dependent context) and fires once, at instantiation -- whether // the read's operand is non-dependent (template_read_nondependent_bad) or From 6e2490ed7c9a2621dc1dbf25438113e460cd9b63 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 18:45:37 -0400 Subject: [PATCH 202/289] Check ref_to_uninit on by-reference lambda captures --- clang/docs/ProfilesFramework.rst | 13 +++++-- .../clang/Basic/DiagnosticSemaKinds.td | 3 ++ clang/include/clang/Sema/SemaProfiles.h | 11 ++++++ clang/lib/Sema/SemaLambda.cpp | 6 ++++ clang/lib/Sema/SemaProfiles.cpp | 19 +++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 34 +++++++++++++++++++ 6 files changed, 84 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 89941d93e5eb8..092d3109f72fa 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1050,8 +1050,17 @@ recognizers symmetric. overloaded operators, and calls to objects of class type such as functors and lambdas), arguments supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``, which reuses the pre-built expression - rather than re-running copy-initialization), and return statements - (``Sema::BuildReturnStmt``). Every site defers on a template pattern and + rather than re-running copy-initialization), return statements + (``Sema::BuildReturnStmt``), and lambda captures -- an init-capture binds + like a variable initialization when its variable is created + (``Sema::createLambdaInitCaptureVarDecl``), and a plain by-reference capture + of an entity denoting uninitialized storage (an ``[[uninit]]`` variable, or + a ``[[ref_to_uninit]]`` reference) is checked when the closure is built + (``Sema::BuildLambdaExpr``). A capture cannot carry the marker, so only the + unmarked-direction violation can fire there; a *copy* capture is not a + binding -- it reads the variable in the enclosing function's CFG, which is + the flow-based ``uninit_read`` pass's territory. Every site defers on a + template pattern and fires once, at instantiation. The variable, data-member, and constructor member-initializer sites pass the instantiated ``Decl`` (deferred by the ``D->isTemplated()`` check in ``shouldEmitProfileViolation``); the diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 0712095d89d9c..ec21d1dd3dd46 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14191,4 +14191,7 @@ def err_init_uninit_read_through : ProfileRuleError< "read %select{through a '[[ref_to_uninit]]' pointer or reference|" "of a member of an '[[uninit]]' object}1 accesses " "uninitialized memory under profile '%0'">; +def err_init_uninit_ref_capture : ProfileRuleError< + "capturing %1 by reference binds a reference to uninitialized memory " + "under profile '%0'">; } // end of sema component. diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 15e527e967b16..90562a6996ea6 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -226,6 +226,17 @@ class SemaProfiles : public SemaBase { void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType); + /// std::init / ref_to_uninit (paper §4.3): a by-reference lambda capture of + /// \p Var binds a reference to its storage, and a capture cannot carry + /// [[ref_to_uninit]], so capturing an entity that denotes uninitialized + /// storage -- an [[uninit]] variable, or a [[ref_to_uninit]] reference -- + /// is always the unmarked-direction violation. Called from + /// \c Sema::BuildLambdaExpr for each by-reference non-init variable capture + /// (init-captures are checked at \c createLambdaInitCaptureVarDecl); defers + /// on a template pattern, where TreeTransform rebuilds the lambda at + /// instantiation. + void checkInitProfileRefCapture(SourceLocation Loc, const ValueDecl *Var); + /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose /// [[uninit]] placed on a pointer, a union variable, or a union member. /// \p D must already carry the UninitAttr (the marker location is taken diff --git a/clang/lib/Sema/SemaLambda.cpp b/clang/lib/Sema/SemaLambda.cpp index 7cafc2ad9b02a..934496dcb6033 100644 --- a/clang/lib/Sema/SemaLambda.cpp +++ b/clang/lib/Sema/SemaLambda.cpp @@ -2281,6 +2281,12 @@ ExprResult Sema::BuildLambdaExpr(SourceLocation StartLoc, assert(From.isVariableCapture() && "unknown kind of capture"); ValueDecl *Var = From.getVariable(); LambdaCaptureKind Kind = From.isCopyCapture() ? LCK_ByCopy : LCK_ByRef; + // std::init / ref_to_uninit (paper §4.3): a by-reference capture binds + // an unmarked reference to the captured variable's storage. An + // init-capture was already checked when its variable was created + // (createLambdaInitCaptureVarDecl). + if (Kind == LCK_ByRef && !From.isInitCapture()) + Profiles().checkInitProfileRefCapture(From.getLocation(), Var); return LambdaCapture(From.getLocation(), IsImplicit, Kind, Var, From.getEllipsisLoc()); } diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 9162c7366bb6d..60d30b745b8a1 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -900,6 +900,25 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, T->isReferenceType(), Src, D); } +void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, + const ValueDecl *Var) { + if (!getLangOpts().Profiles) + return; + // Mirrors the glvalue recognizer's named-entity arm: the captured variable + // denotes uninitialized storage if it is [[uninit]], or if it is a + // [[ref_to_uninit]] reference (the capture binds to its referent). A copy + // capture is not this check's: it reads the variable in the enclosing + // function's CFG, which is the flow-based uninit_read pass's territory. + if (!Var->hasAttr() && + !(Var->getType()->isReferenceType() && Var->hasAttr())) + return; + if (deferUninitCheckOnTemplatePattern(SemaRef, /*D=*/nullptr)) + return; + if (!shouldEmitProfileViolation("std::init", "ref_to_uninit", Loc)) + return; + Diag(Loc, diag::err_init_uninit_ref_capture) << "std::init" << Var; +} + // The read-through diagnostic distinguishes indirection through a // [[ref_to_uninit]] pointer/reference from a subobject read of a named // [[uninit]] object, which involves no [[ref_to_uninit]] entity. Approximate diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 32804ebc0be79..deb03366baec9 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -323,6 +323,40 @@ void template_init_capture_bad() { } template void template_init_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_init_capture_bad' requested here}} +// A by-reference capture -- explicit or via a capture-default -- is the same +// binding as an init-capture: a capture cannot carry [[ref_to_uninit]], so +// capturing an [[uninit]] variable (or a [[ref_to_uninit]] reference) by +// reference is always the unmarked-direction violation. A copy capture is not +// a binding; it reads the variable in the enclosing function's CFG, which is +// the flow-based uninit_read pass's territory. +void test_ref_captures() { + int x [[uninit]]; + int ok = 0; + auto c1 = [&x] { x = 1; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c2 = [&] { x = 1; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c3 = [&ok] { ok = 1; }; // OK: initialized + int *rtu [[ref_to_uninit]] = &g_uninit; + auto c4 = [&rtu] { (void)rtu; }; // OK: the pointer object itself is initialized + int &ur [[ref_to_uninit]] = *rtu; + auto c5 = [&ur] { (void)ur; }; // expected-error {{capturing 'ur' by reference binds a reference to uninitialized memory under profile 'std::init'}} + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] { + auto c6 = [&x] { x = 2; }; // OK: suppressed + (void)c6; + } + (void)c1; (void)c2; (void)c3; (void)c4; (void)c5; +} + +// A by-reference capture inside a template body defers on the pattern and +// fires once, at instantiation (TreeTransform rebuilds the lambda). +template +void template_ref_capture_bad() { + int x [[uninit]]; + auto c = [&x] { x = 1; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + (void)c; +} +template void template_ref_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_ref_capture_bad' requested here}} + struct OpTag {}; OpTag operator+(OpTag, int *p [[ref_to_uninit]]); From 810d5150d2d1ab1aa7b8c5e5992997de60f5df65 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 19:10:27 -0400 Subject: [PATCH 203/289] Record enforce designators from the ungated enforcement list --- clang/lib/Sema/SemaProfiles.cpp | 6 ++++- clang/test/AST/ast-dump-profiles-enforce.cpp | 24 ++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 clang/test/AST/ast-dump-profiles-enforce.cpp diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 60d30b745b8a1..9e91ea26904c4 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -108,7 +108,11 @@ bool SemaProfiles::processProfilesEnforceAttr( StringRef Name = D.Name; StringRef Spelling = D.Spelling; - bool IsNew = !isProfileEnforced(Name); + // "Already recorded?" must use the ungated lookup: isProfileEnforced + // filters gated-off test:: names (and -fprofiles off), which would make + // every repetition of such a profile look new and re-append its + // designator to the attribute's argument arrays. + bool IsNew = !getProfileEnforcement(Name); if (!addProfileEnforcement(Name, Spelling, AL.getLoc())) continue; diff --git a/clang/test/AST/ast-dump-profiles-enforce.cpp b/clang/test/AST/ast-dump-profiles-enforce.cpp new file mode 100644 index 0000000000000..374bfcc662e90 --- /dev/null +++ b/clang/test/AST/ast-dump-profiles-enforce.cpp @@ -0,0 +1,24 @@ +// RUN: %clang_cc1 -std=c++23 -fprofiles -ast-dump %s | FileCheck %s + +// A valid repetition of an already-recorded designator has no effect +// (P3589R2 [decl.attr.enforce]p3), so it must not re-append the designator to +// the attribute's argument arrays. The "already recorded" test must use the +// ungated enforcement list: a gated-off test:: profile (recorded but reported +// not-enforced without -fprofiles-test-profiles, which this run deliberately +// omits) is still a recorded designator. + +[[profiles::enforce(test::type_cast)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}} test::type_cast test::type_cast 0{{$}} + +[[profiles::enforce(test::type_cast)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}}>{{$}} + +[[profiles::enforce(std::init, vendor(fortify: 3))]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}} std::init vendor std::init vendor(fortify : 3) 0 1 fortify 3 1{{$}} + +[[profiles::enforce(std::init)]]; +// CHECK: EmptyDecl +// CHECK-NEXT: ProfilesEnforceAttr {{.*}}>{{$}} From fd51377bd1548fc2c39e0d383d0d85110df17323 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 19:12:20 -0400 Subject: [PATCH 204/289] Test header-unit enforcement and require --- clang/docs/ProfilesFramework.rst | 7 +++ .../SemaCXX/safety-profile-header-unit.cpp | 63 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-header-unit.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 092d3109f72fa..eb4837491d8af 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -494,6 +494,13 @@ interface unit of ``M``. ``[[profiles::require(...)]]`` on an import-declaration validates that the imported module's ``EnforcedProfileDesignators`` contains a matching designator. +A *header unit* participates the same way: ``[[profiles::enforce(...)]]`` on +an empty-declaration in the header (the form P3589R2 §2.3 prescribes for +header units) is recorded on the header-unit module and serialized into its +BMI, so ``[[profiles::require]]`` on an ``import "header.h";`` validates +against it. As with named modules, importing an enforced header unit does not +enforce the profile in the importer. + ``[[profiles::enforce(...)]]`` on a *non-interface* module-declaration (a ``module M;`` implementation unit, or a ``module M:P;`` partition implementation unit) is accepted but recorded only translation-unit-locally; diff --git a/clang/test/SemaCXX/safety-profile-header-unit.cpp b/clang/test/SemaCXX/safety-profile-header-unit.cpp new file mode 100644 index 0000000000000..d5eea65e5cb93 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-header-unit.cpp @@ -0,0 +1,63 @@ +// Header units (P3589R2 s2.3): [[profiles::enforce]] on an empty-declaration +// in a header compiled as a header unit is recorded on the header-unit module +// and validated by [[profiles::require]] on its import. Importing an enforced +// header unit does not enforce the profile locally. + +// RUN: rm -rf %t +// RUN: mkdir -p %t +// RUN: split-file %s %t +// +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-header-unit -xc++-user-header %t/enforced.h -o %t/enforced.pcm +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-header-unit -xc++-user-header %t/plain.h -o %t/plain.pcm +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-header-unit -xc++-user-header %t/args.h -o %t/args.pcm +// +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_ok.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_fail.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_plain_fail.cpp -fmodule-file=%t/plain.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_no_leak.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_args_ok.cpp -fmodule-file=%t/args.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_args_fail.cpp -fmodule-file=%t/args.pcm -verify +// +// The -fprofiles-test-profiles gate only controls whether test:: rules fire; +// the designator is recorded and exported regardless (see the Driver Flag +// section of ProfilesFramework.rst), so require validates identically under +// plain -fprofiles. +// RUN: %clang_cc1 -std=c++20 -fprofiles -emit-header-unit -xc++-user-header %t/enforced.h -o %t/enforced-noflag.pcm +// RUN: %clang_cc1 -std=c++20 -fprofiles -Wno-experimental-header-units -fsyntax-only %t/import_ok.cpp -fmodule-file=%t/enforced-noflag.pcm -verify + +//--- enforced.h +[[profiles::enforce(test::type_cast)]]; +void hu_api(int); + +//--- plain.h +void plain_api(int); + +//--- args.h +[[profiles::enforce(vendor(fortify: 3))]]; +void args_api(int); + +//--- import_ok.cpp +// expected-no-diagnostics +import "enforced.h" [[profiles::require(test::type_cast)]]; + +//--- import_fail.cpp +import "enforced.h" [[profiles::require(test::other)]]; // expected-error {{required profile 'test::other' is not enforced by imported module}} + +//--- import_plain_fail.cpp +// A header unit built with no enforcement satisfies no requirement. +import "plain.h" [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} + +//--- import_no_leak.cpp +// Importing an enforced header unit does not enforce the profile in the +// importer: enforcement is always explicit and local. +// expected-no-diagnostics +import "enforced.h"; +long no_leak(void *p) { return reinterpret_cast(p); } + +//--- import_args_ok.cpp +// expected-no-diagnostics +import "args.h" [[profiles::require(vendor(fortify: 3))]]; + +//--- import_args_fail.cpp +// Require compares canonical designator spellings, arguments included. +import "args.h" [[profiles::require(vendor(fortify: 2))]]; // expected-error {{required profile 'vendor(fortify : 2)' is not enforced by imported module}} From 746771297a95e9acb29a1b56062975d876947a1c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 3 Jul 2026 19:28:31 -0400 Subject: [PATCH 205/289] Diagnose redeclarations outside a compatible profile dominion --- clang/docs/ProfilesFramework.rst | 51 +++++++- .../clang/Basic/DiagnosticSemaKinds.td | 6 + clang/include/clang/Sema/SemaProfiles.h | 11 ++ clang/lib/Sema/SemaDecl.cpp | 8 ++ clang/lib/Sema/SemaProfiles.cpp | 79 ++++++++++++ .../safety-profile-framework-modules.cppm | 119 ++++++++++++++++++ .../SemaCXX/safety-profile-header-unit.cpp | 20 +++ 7 files changed, 289 insertions(+), 5 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index eb4837491d8af..135734b62741f 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -527,6 +527,52 @@ and enforces a profile whose designator conflicts with a locally repeated Importing a module that enforces a profile does **not** enforce that profile in the importing translation unit. Enforcement is always explicit and local. +Redeclaration Compatibility +--------------------------- + +P3589R2 [decl.attr.enforce]p5: a declaration and its redeclarations must +appear in the dominions of mutually compatible profiles. The rule is +**symmetric** -- when a redeclaration is merged with a previous declaration +from another module unit, every profile whose dominion covered the previous +declaration must have a compatible counterpart covering the redeclaration, +and vice versa. In particular, a profile-enforcing translation unit that +redeclares an entity from a module (or header unit) compiled *without* a +compatible profile is ill-formed; the paper's escape hatch for such headers +is ``[[profiles::exempt]]`` (not yet implemented, see `Intentional +Omissions`_). The check +(``SemaProfiles::checkRedeclarationProfileCompatibility``) runs from +``Sema::CheckRedeclarationInModule``, the funnel for function, variable, tag, +alias, and class-template redeclarations; it is a framework rule -- a plain +error, not suppressible with ``[[profiles::suppress]]``, and diagnose-only +(the redeclaration still merges). + +Two profiles are *compatible* if they have the same name -- designator +arguments configure a profile without changing its identity -- or if both are +standard (``std::``-prefixed) profiles, which P3589R2 proclaims mutually +compatible. No further implementation-proclaimed compatibility is modeled. + +The previous declaration's dominion is approximated by its top-level module's +exported ``EnforcedProfileDesignators``, which is exact for declarations in +the module purview -- including purview ``extern "C"``/``extern "C++"`` +declarations (implicit global module), the common redeclarable case, since +module-attached entities cannot be redeclared in other translation units at +all. Two cases have an *unknown* dominion and are skipped rather than +guessed at (a missed diagnostic, never a wrong one): + +- A declaration in an **explicit global module fragment**: it precedes the + module-declaration, so the exported enforcements do not cover it, and its + TU's empty-declaration enforces are not serialized into the BMI. +- A previous declaration from the **same module family** (an implementation + or partition unit merging with its own interface): the exported set + under-approximates the interface TU's full dominion, and the interface's + enforcements are inherited into the current unit anyway, so checking would + false-positive on locally added profiles. + +A textual or PCH previous declaration is not checked at all: it shares the +current TU's dominion (the placement rule makes a TU's dominion uniform, and +a PCH's enforcements are restored into the including TU). Implicit template +instantiations are exempt, matching the module-ownership check. + Serialization ------------- @@ -548,11 +594,6 @@ The following parts of P3589R2 are deliberately not implemented: requires bookkeeping that connects the original spelling of an ``#include`` to the source locations of constructs in the included file, and the feature is not needed to exercise or validate the rest of the framework. -- The redeclaration consistency rule from P3589R2 section 2.2 paragraph 5 - (every redeclaration of a declaration in the dominion of a profile must - itself appear in the dominion of a compatible profile). Profile attributes - on redeclarations are parsed and recorded, but no cross-redeclaration - compatibility check is performed. Built-in Profiles diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index ec21d1dd3dd46..5a5ad09f77886 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14132,6 +14132,12 @@ def err_profiles_require_not_on_import : Error< "'profiles::require' attribute only allowed on module-import-declarations">; def err_profiles_require_not_enforced : Error< "required profile '%0' is not enforced by imported module">; +def err_profiles_redecl_incompatible : Error< + "%select{redeclaration of %1 is not in the dominion of a profile " + "compatible with '%2', which module '%3' enforces where %1 was " + "previously declared|%1 was previously declared in module '%3', outside " + "the dominion of a profile compatible with '%2', which this translation " + "unit enforces}0">; def err_profiles_suppress_justification_not_string : Error< "'justification' argument of 'profiles::suppress' must be a string literal">; def err_profile_type_cast_reinterpret : ProfileRuleError< diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 90562a6996ea6..a4826c53f9940 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -108,6 +108,17 @@ class SemaProfiles : public SemaBase { bool checkProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc, unsigned DiagID); + /// P3589R2 [decl.attr.enforce]p5: a declaration and its redeclarations must + /// appear in the dominions of mutually compatible profiles. Called from + /// \c Sema::CheckRedeclarationInModule when \p New redeclares \p Old. Only + /// a previous declaration from another module unit (a named module or a + /// header unit) can carry a different dominion; that TU's dominion is + /// approximated by the module's exported designator set. Profiles are + /// compatible by name, with all std:: profiles mutually compatible. + /// Diagnose-only: the redeclaration is not invalidated. + void checkRedeclarationProfileCompatibility(const NamedDecl *New, + const NamedDecl *Old); + /// Dispatch class-finalization profile callbacks for a completed class. /// Called from \c Sema::CheckCompletedCXXClass so parser, template /// instantiation, and lambda finalization paths all reach the same hook. diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp index 3b362e44131ee..0ecc2e8bb1eb7 100644 --- a/clang/lib/Sema/SemaDecl.cpp +++ b/clang/lib/Sema/SemaDecl.cpp @@ -1800,6 +1800,14 @@ bool Sema::CheckRedeclarationInModule(NamedDecl *New, NamedDecl *Old) { if (CheckRedeclarationExported(New, Old)) return true; + // P3589R2 [decl.attr.enforce]p5: a redeclaration must be covered by + // profiles compatible with those covering the previous declaration (and + // vice versa). Implicit instantiations are not written declarations, so + // they are exempt, matching the module-ownership check. Diagnose-only: + // merging proceeds either way. + if (!isImplicitInstantiation(New) && !isImplicitInstantiation(Old)) + Profiles().checkRedeclarationProfileCompatibility(New, Old); + return false; } diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 9e91ea26904c4..b972db954bb99 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -135,6 +135,85 @@ bool SemaProfiles::processProfilesEnforceAttr( return true; } +// P3589R2 [decl.attr.enforce]p5: profiles are compatible if they are the same +// -- by name; arguments configure a profile without changing its identity -- +// or proclaimed compatible by the implementation. "All standard profiles are +// compatible with each other" is the one proclamation modeled here. +static bool areProfilesCompatible(StringRef A, StringRef B) { + return A == B || (A.starts_with("std::") && B.starts_with("std::")); +} + +void SemaProfiles::checkRedeclarationProfileCompatibility( + const NamedDecl *New, const NamedDecl *Old) { + if (!getLangOpts().Profiles) + return; + // Only a previous declaration from another module unit (a named module or a + // header unit) can carry a different profile dominion. A textual or PCH + // previous declaration shares this TU's dominion: the placement rule makes + // a TU's dominion uniform over its declarations, and a PCH's enforcements + // are restored into this TU (ENFORCED_PROFILES). + if (!Old->isFromASTFile()) + return; + Module *M = Old->getOwningModule(); + if (!M) + return; + // A declaration in an explicit global-module-fragment precedes the module + // declaration, so the module's exported enforcements do not cover it, and + // its TU's empty-declaration enforcements are not serialized into the BMI. + // Its dominion is unknown: skip rather than guess (a missed diagnostic, + // never a wrong one). An *implicit* global-module-fragment declaration (a + // purview extern "C"/"C++" declaration, the common redeclarable case) sits + // inside the module declaration's dominion and is checked. + if (M->isExplicitGlobalModule()) + return; + Module *Top = M->getTopLevelModule(); + // Within the same module family (an implementation or partition unit seeing + // its interface) the exported set under-approximates the interface TU's + // full dominion, and the interface's enforcements are inherited into this + // unit anyway; skip rather than false-positive on locally added profiles. + if (Module *Current = SemaRef.getCurrentModule()) + if (Current->getTopLevelModule()->getPrimaryModuleInterfaceName() == + Top->getPrimaryModuleInterfaceName()) + return; + + // A gated-off test:: profile is inert in this compilation, on either side. + auto IsActive = [&](StringRef Name) { + return getLangOpts().ProfilesTestProfiles || !Name.starts_with("test::"); + }; + // First active profile in Enforced with no compatible counterpart in + // Covering; empty if fully covered. + auto FindUncovered = [&](const auto &Enforced, + const auto &Covering) -> StringRef { + for (const auto &EP : Enforced) { + StringRef Name = EP.ProfileName; + if (!IsActive(Name)) + continue; + if (llvm::none_of(Covering, [&](const auto &Other) { + return areProfilesCompatible(Name, Other.ProfileName); + })) + return Name; + } + return {}; + }; + + // The rule is symmetric: every profile whose dominion covers one + // declaration must have a compatible counterpart covering the other. + // Report the first violation in each direction. + StringRef MissingHere = + FindUncovered(Top->EnforcedProfileDesignators, EnforcedProfiles); + StringRef MissingThere = + FindUncovered(EnforcedProfiles, Top->EnforcedProfileDesignators); + if (MissingHere.empty() && MissingThere.empty()) + return; + if (!MissingHere.empty()) + Diag(New->getLocation(), diag::err_profiles_redecl_incompatible) + << /*PreviouslyEnforced=*/0 << New << MissingHere << Top->Name; + if (!MissingThere.empty()) + Diag(New->getLocation(), diag::err_profiles_redecl_incompatible) + << /*PreviouslyEnforced=*/1 << New << MissingThere << Top->Name; + Diag(Old->getLocation(), diag::note_previous_declaration); +} + ProfilesSuppressAttr * SemaProfiles::makeProfilesSuppressAttr(const ParsedAttr &AL) { const auto &Args = AL.getProfileSuppressArgs(); diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 3152d6956488c..9d0b263c15c4a 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -30,6 +30,19 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_require_no_args.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_mod.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_import.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_mod.cppm -o %t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_same.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_none.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_other.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_plain_mod.cppm -o %t/redecl_plain_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_reverse.cpp -fmodule-file=RedeclPlainMod=%t/redecl_plain_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_std_mod.cppm -o %t/redecl_std_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_std_compat.cpp -fmodule-file=RedeclStdMod=%t/redecl_std_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_vendor_mod.cppm -o %t/redecl_vendor_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_vendor_args.cpp -fmodule-file=RedeclVendorMod=%t/redecl_vendor_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -emit-module-interface %t/redecl_gmf_mod.cppm -o %t/redecl_gmf_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_gmf_skip.cpp -fmodule-file=RedeclGmfMod=%t/redecl_gmf_mod.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/redecl_impl_extra.cpp -fmodule-file=RedeclMod=%t/redecl_mod.pcm -verify // =================================================================== // Module with enforced profiles @@ -288,3 +301,109 @@ export void f(); //--- unknown_attr_import.cpp import TestMod [[vendor::unknown]]; // expected-warning {{unknown attribute 'vendor::unknown' ignored}} import TestMod [[deprecated]]; // expected-error {{'deprecated' attribute cannot be applied to a module import}} + +// =================================================================== +// Redeclaration profile compatibility (P3589R2 [decl.attr.enforce]p5): +// a declaration and its redeclarations must appear in the dominions of +// mutually compatible profiles. Compatibility is by profile name (the +// arguments configure a profile without changing its identity), and all +// std:: profiles are compatible with each other. The redeclarable +// fixtures are extern "C++" purview declarations -- entities attached to +// a named module cannot be redeclared in other TUs at all -- which sit +// inside the module declaration's dominion. +// =================================================================== +//--- redecl_mod.cppm +// expected-no-diagnostics +export module RedeclMod [[profiles::enforce(test::type_cast)]]; +extern "C++" { +void redecl_api(int); +struct RedeclTag; +} + +//--- redecl_plain_mod.cppm +// expected-no-diagnostics +export module RedeclPlainMod; +extern "C++" void plain_api(int); + +//--- redecl_std_mod.cppm +// expected-no-diagnostics +export module RedeclStdMod [[profiles::enforce(std::init)]]; +extern "C++" void std_api(int); + +//--- redecl_vendor_mod.cppm +// expected-no-diagnostics +export module RedeclVendorMod [[profiles::enforce(vendor(fortify: 3))]]; +extern "C++" void vendor_api(int); + +//--- redecl_same.cpp +// Redeclaring under the same profile satisfies both directions at once. +// expected-no-diagnostics +[[profiles::enforce(test::type_cast)]]; +import RedeclMod; +void redecl_api(int); +struct RedeclTag; + +//--- redecl_none.cpp +// Forward direction: the module's profile has no compatible counterpart here. +// Both function and tag redeclarations funnel through the check. +import RedeclMod; +void redecl_api(int); // expected-error {{redeclaration of 'redecl_api' is not in the dominion of a profile compatible with 'test::type_cast', which module 'RedeclMod' enforces where 'redecl_api' was previously declared}} +struct RedeclTag; // expected-error {{redeclaration of 'RedeclTag' is not in the dominion of a profile compatible with 'test::type_cast', which module 'RedeclMod' enforces where 'RedeclTag' was previously declared}} +// expected-note@redecl_mod.cppm:* {{previous declaration is here}} +// expected-note@redecl_mod.cppm:* {{previous declaration is here}} + +//--- redecl_other.cpp +// An incompatible (non-std) profile on each side violates both directions. +[[profiles::enforce(test::other)]]; +import RedeclMod; +void redecl_api(int); // expected-error {{redeclaration of 'redecl_api' is not in the dominion of a profile compatible with 'test::type_cast', which module 'RedeclMod' enforces where 'redecl_api' was previously declared}} \ + // expected-error {{'redecl_api' was previously declared in module 'RedeclMod', outside the dominion of a profile compatible with 'test::other', which this translation unit enforces}} +// expected-note@redecl_mod.cppm:* {{previous declaration is here}} + +//--- redecl_reverse.cpp +// Reverse direction: this TU enforces a profile; the previous declaration's +// TU enforced nothing compatible. +[[profiles::enforce(test::type_cast)]]; +import RedeclPlainMod; +void plain_api(int); // expected-error {{'plain_api' was previously declared in module 'RedeclPlainMod', outside the dominion of a profile compatible with 'test::type_cast', which this translation unit enforces}} +// expected-note@redecl_plain_mod.cppm:* {{previous declaration is here}} + +//--- redecl_std_compat.cpp +// All standard profiles are compatible with each other, in both directions. +// expected-no-diagnostics +[[profiles::enforce(std::other_profile)]]; +import RedeclStdMod; +void std_api(int); + +//--- redecl_vendor_args.cpp +// Compatibility is by profile name: different arguments configure the same +// profile. +// expected-no-diagnostics +[[profiles::enforce(vendor(fortify: 2))]]; +import RedeclVendorMod; +void vendor_api(int); + +//--- redecl_gmf_mod.cppm +// expected-no-diagnostics +module; +void redecl_gmf_api(int); +export module RedeclGmfMod [[profiles::enforce(test::type_cast)]]; +export inline void use_gmf() { redecl_gmf_api(1); } + +//--- redecl_gmf_skip.cpp +// A declaration in the module's *explicit* global module fragment precedes +// the module declaration, so the exported enforcement does not cover it, and +// its TU's empty-declaration enforces are not serialized: its dominion is +// unknown and the check skips it (a missed diagnostic, never a wrong one). +// expected-no-diagnostics +import RedeclGmfMod; +void redecl_gmf_api(int); + +//--- redecl_impl_extra.cpp +// An implementation unit may add TU-local enforcement; entities of its own +// module family are skipped (the exported set under-approximates the +// interface TU's dominion, and the interface's enforcement is inherited +// here anyway). +// expected-no-diagnostics +module RedeclMod [[profiles::enforce(test::other)]]; +extern "C++" void redecl_api(int); diff --git a/clang/test/SemaCXX/safety-profile-header-unit.cpp b/clang/test/SemaCXX/safety-profile-header-unit.cpp index d5eea65e5cb93..c5e2d1258fcbc 100644 --- a/clang/test/SemaCXX/safety-profile-header-unit.cpp +++ b/clang/test/SemaCXX/safety-profile-header-unit.cpp @@ -24,6 +24,11 @@ // plain -fprofiles. // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-header-unit -xc++-user-header %t/enforced.h -o %t/enforced-noflag.pcm // RUN: %clang_cc1 -std=c++20 -fprofiles -Wno-experimental-header-units -fsyntax-only %t/import_ok.cpp -fmodule-file=%t/enforced-noflag.pcm -verify +// +// Redeclaration profile compatibility (P3589R2 [decl.attr.enforce]p5) +// across a header unit, in both directions. +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/redecl_forward.cpp -fmodule-file=%t/enforced.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/redecl_reverse.cpp -fmodule-file=%t/plain.pcm -verify //--- enforced.h [[profiles::enforce(test::type_cast)]]; @@ -61,3 +66,18 @@ import "args.h" [[profiles::require(vendor(fortify: 3))]]; //--- import_args_fail.cpp // Require compares canonical designator spellings, arguments included. import "args.h" [[profiles::require(vendor(fortify: 2))]]; // expected-error {{required profile 'vendor(fortify : 2)' is not enforced by imported module}} + +//--- redecl_forward.cpp +// The header unit's TU enforced a profile; the redeclaring TU must enforce a +// compatible one. +import "enforced.h"; +void hu_api(int); // expected-error {{redeclaration of 'hu_api' is not in the dominion of a profile compatible with 'test::type_cast'}} +// expected-note@enforced.h:* {{previous declaration is here}} + +//--- redecl_reverse.cpp +// And symmetrically: this TU enforces a profile; the header unit's TU did not +// enforce a compatible one. +[[profiles::enforce(test::type_cast)]]; +import "plain.h"; +void plain_api(int); // expected-error {{'plain_api' was previously declared in module}} +// expected-note@plain.h:* {{previous declaration is here}} From f4a92ab0c818256307f3cd92f2c945522e7117b1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 11:49:48 -0400 Subject: [PATCH 206/289] Diagnose element reads of [[uninit]] arrays in the read-through check --- clang/docs/ProfilesFramework.rst | 11 ++-- .../clang/Basic/DiagnosticSemaKinds.td | 2 +- clang/lib/Sema/SemaProfiles.cpp | 52 +++++++++++---- .../test/SemaCXX/safety-profile-init-read.cpp | 17 +++-- .../safety-profile-init-ref-to-uninit.cpp | 64 +++++++++++++++++-- 5 files changed, 118 insertions(+), 28 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 135734b62741f..4c02052d41044 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1138,11 +1138,12 @@ recognizers symmetric. current-object member are flow-tracked (by the CFG ``uninit_read`` pass and the ctor-body pass, which credit assignments), so their direct reads are left to those passes. A *subobject* read of a named ``[[uninit]]`` object - (``s.x``, ``o.agg.f``) is recognized and diagnosed here: neither flow pass - tracks it, and member-wise delayed initialization of an ``[[uninit]]`` object - is itself banned (paper §5.4; only whole-object ``construct_at`` - re-initializes, which is uniformly unmodeled), so no assignment could have - given the member a value. Being Decl-less, it defers on a dependent context + (``s.x``, ``o.agg.f``) or array (``a[0]``, ``*a``, ``s.a[i]``) is recognized + and diagnosed here: neither flow pass tracks members or array elements, and + subobject-wise delayed initialization of an ``[[uninit]]`` object is itself + banned (paper §5.4/§5.5; only whole-object ``construct_at`` re-initializes, + which is uniformly unmodeled), so no assignment could have given the + subobject a value. Being Decl-less, it defers on a dependent context and fires once, at instantiation. An address-of (``&*p``), a reference binding, a discarded-value expression (``(void)*p``), and a write (``*p = 5`` or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads. diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 5a5ad09f77886..9a9d6fda471e5 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14195,7 +14195,7 @@ def err_init_uninit_requires_ref_to_uninit : ProfileRuleError< "'[[ref_to_uninit]]' under profile '%0'">; def err_init_uninit_read_through : ProfileRuleError< "read %select{through a '[[ref_to_uninit]]' pointer or reference|" - "of a member of an '[[uninit]]' object}1 accesses " + "of a subobject of an '[[uninit]]' object}1 accesses " "uninitialized memory under profile '%0'">; def err_init_uninit_ref_capture : ProfileRuleError< "capturing %1 by reference binds a reference to uninitialized memory " diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index b972db954bb99..f285f12d463cd 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -805,8 +805,13 @@ static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, return *PassThrough; // Array-to-pointer decay has been stripped above, leaving the array glvalue. + // Leave read mode here, like the member-access arm: neither the CFG + // uninit_read pass nor the ctor-body pass tracks array elements, and + // element-wise delayed initialization of an [[uninit]] array is itself + // banned (paper §5.5), so below an element access the marker counts even + // for a read. if (E->getType()->isArrayType()) - return glvalueDenotesUninitStorage(Ctx, E, ForRead); + return glvalueDenotesUninitStorage(Ctx, E, /*ForRead=*/false); // &G, where G denotes uninitialized storage. if (const auto *UO = dyn_cast(E)) @@ -1005,19 +1010,42 @@ void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, // The read-through diagnostic distinguishes indirection through a // [[ref_to_uninit]] pointer/reference from a subobject read of a named // [[uninit]] object, which involves no [[ref_to_uninit]] entity. Approximate -// but sufficient for phrasing: walk up the dot member chain; the read is the -// latter form iff the chain reaches an [[uninit]]-marked member (e.g. the -// class-type member in this->agg.f) or bottoms out at a named [[uninit]] -// declaration. An arrow access reaches its object through a pointer, so the -// pointer wording applies from there on. +// but sufficient for phrasing: walk up the dot member / array-element chain; +// the read is the latter form iff the chain reaches an [[uninit]]-marked +// member (e.g. the class-type member in this->agg.f) or bottoms out at a +// named [[uninit]] declaration. An arrow access or a subscript on a pointer +// reaches its object through a pointer, so the pointer wording applies from +// there on. static bool isMemberChainOfUninitObject(const Expr *E) { E = E->IgnoreParenImpCasts(); - while (const auto *ME = dyn_cast(E)) { - if (ME->getMemberDecl()->hasAttr()) - return true; - if (ME->isArrow()) - return false; - E = ME->getBase()->IgnoreParenImpCasts(); + while (true) { + if (const auto *ME = dyn_cast(E)) { + if (ME->getMemberDecl()->hasAttr()) + return true; + if (ME->isArrow()) + return false; + E = ME->getBase()->IgnoreParenImpCasts(); + continue; + } + // a[i] and *a on an array glvalue (decay stripped below) are subobject + // accesses like a dot member access; on a pointer base they reach the + // object through the pointer. + if (const auto *ASE = dyn_cast(E)) { + const Expr *Base = ASE->getBase()->IgnoreParenImpCasts(); + if (!Base->getType()->isArrayType()) + return false; + E = Base; + continue; + } + if (const auto *UO = dyn_cast(E); + UO && UO->getOpcode() == UO_Deref) { + const Expr *Sub = UO->getSubExpr()->IgnoreParenImpCasts(); + if (!Sub->getType()->isArrayType()) + return false; + E = Sub; + continue; + } + break; } const auto *DRE = dyn_cast(E); return DRE && DRE->getDecl()->hasAttr(); diff --git a/clang/test/SemaCXX/safety-profile-init-read.cpp b/clang/test/SemaCXX/safety-profile-init-read.cpp index 86fb39193c6c6..74808c588e76f 100644 --- a/clang/test/SemaCXX/safety-profile-init-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-read.cpp @@ -161,14 +161,21 @@ void test_const_ptr_use_is_not_a_read() { take_const_ptr(&x); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } -// A read of a member of an [[uninit]] aggregate local is the read-through -// check's (this CFG pass does not track member accesses of record locals, and -// member-wise delayed initialization is banned, paper section 5.4). Full -// coverage lives in safety-profile-init-ref-to-uninit.cpp. +// A read of a subobject of an [[uninit]] local -- an aggregate's member or an +// array's element -- is the read-through check's (this CFG pass does not track +// member accesses of record locals or arrays at all, and subobject-wise +// delayed initialization is banned, paper sections 5.4/5.5). Full coverage +// lives in safety-profile-init-ref-to-uninit.cpp. struct Agg { int m; }; void test_member_read_of_uninit_aggregate() { Agg s [[uninit]]; - int y = s.m; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y = s.m; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +void test_element_read_of_uninit_array() { + [[uninit]] int a[2]; + int y = a[0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} (void)y; } #endif diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index deb03366baec9..e48e399c0fa0d 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -769,10 +769,10 @@ struct PairHolder { Pair p; }; void test_member_read_of_uninit_object() { Pair s [[uninit]]; - int y1 = s.x; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} - take_value(s.y); // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y1 = s.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + take_value(s.y); // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} PairHolder o [[uninit]]; - int y2 = o.p.x; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y2 = o.p.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} // The arrow spelling reaches the member through a pointer, so the // diagnostic's phrasing approximation picks the pointer wording; the read is // diagnosed all the same. @@ -806,6 +806,60 @@ void test_member_read_byte_exempt() { (void)b; } +// An element read of a named [[uninit]] array is a subobject read exactly like +// s.x: neither flow pass tracks array elements (and element-wise delayed +// initialization is banned, paper §5.5), so the marker counts below the +// element access even for a read. *a denotes the same element as a[0]. +void test_element_read_of_uninit_array(int i) { + [[uninit]] int a[2]; + int y1 = a[0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + take_value(a[i]); // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y2 = *a; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + [[uninit]] int m[2][2]; + int y3 = m[1][0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y1; (void)y2; (void)y3; +} + +// An [[uninit]] array *member*'s element read is flagged the same way: like +// the class-type member in HasAggMember below, an array member has no legal +// element-wise assignment path, so the marker counts even on the current +// object. +struct WithArrMember { + [[uninit]] int a[2]; + int get() { return a[0]; } // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +}; + +// Writes, discarded values, and address-taking apply no lvalue-to-rvalue +// conversion and are not reads; bindings to the array or its elements stay the +// ref_to_uninit checks' territory (regression guards, unchanged behavior). +void test_element_read_negatives(int i) { + [[uninit]] int a[2]; + a[0] = 1; // OK: write, not a read (writes are a deferred slice) + (void)a[0]; // OK: discarded value + int *p [[ref_to_uninit]] = &a[0]; // OK: address-of is not a read; binding checked by ref_to_uninit + int *q [[ref_to_uninit]] = a; // OK: array decay is a binding, not a read + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { take_value(a[i]); } // OK: rule-targeted suppress + (void)p; (void)q; +} + +// std::byte arrays stay exempt (paper §4.5). +void test_element_read_byte_exempt() { + [[uninit]] std::byte b[2]; + std::byte v = b[0]; // OK + (void)v; +} + +// Like the member read, the element read defers on a template pattern and +// fires once, at instantiation. +template +void template_element_read_bad() { + [[uninit]] T a[2]; + int y = a[0]; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_element_read_bad(); // expected-note {{in instantiation of function template specialization 'template_element_read_bad' requested here}} + // The §5.2 trust pattern is preserved: a scalar [[uninit]] *member* of the // current object may be assigned in the constructor body (flow-checked by the // ctor-body pass), so a member-function read of it is not flagged here. @@ -821,7 +875,7 @@ struct BodyInit { // the current object. struct HasAggMember { Pair agg [[uninit]]; - int get() { return agg.x; } // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int get() { return agg.x; } // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} }; // Like every Decl-less read check, the member read defers on a template @@ -829,7 +883,7 @@ struct HasAggMember { template void template_member_read_bad() { Pair s [[uninit]]; - int y = s.x; // expected-error {{read of a member of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y = s.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} (void)y; } template void template_member_read_bad(); // expected-note {{in instantiation of function template specialization 'template_member_read_bad' requested here}} From 79e81eef1f6c02dc8df1c7fb3bf175b2b17f9c35 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 13:01:48 -0400 Subject: [PATCH 207/289] Thread the uninit recognizers' read mode through an access-options struct --- clang/docs/ProfilesFramework.rst | 4 +- clang/include/clang/Sema/SemaProfiles.h | 8 +-- clang/lib/Sema/SemaProfiles.cpp | 88 +++++++++++++++---------- 3 files changed, 59 insertions(+), 41 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 4c02052d41044..4692296eba19e 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1131,9 +1131,9 @@ recognizers symmetric. (``Sema::DefaultLvalueConversion`` calling ``SemaProfiles::checkInitProfileReadThrough``), which by-value reads -- copy-initialization, by-value arguments, returns, and operator/condition operands -- all funnel through. It reuses the recognizer - in a read-only mode (a ``ForRead`` flag), reports the shared rule + with its read access preset (``UninitAccessOpts``), reports the shared rule ``uninit_read`` via ``err_init_uninit_read_through``, and exempts ``std::byte`` - (paper §4.5). Read-only mode drops the ``[[uninit]]`` marker only for the + (paper §4.5). The read preset drops the ``[[uninit]]`` marker only for the *top-level* named entity -- a directly named ``[[uninit]]`` object and a current-object member are flow-tracked (by the CFG ``uninit_read`` pass and the ctor-body pass, which credit assignments), so their direct reads are left diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index a4826c53f9940..ad7153a23d4c5 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -230,10 +230,10 @@ class SemaProfiles : public SemaBase { /// [[ref_to_uninit]] pointer or reference, whose result is itself /// uninitialized. Called from Sema::DefaultLvalueConversion at the single /// lvalue-to-rvalue chokepoint, with \p Glvalue the operand being loaded - /// and \p ValueType its value type. Reuses the ref_to_uninit recognizer in - /// read-only mode, so a direct read of a named [[uninit]] object is left to - /// the flow-based uninit_read pass. A std::byte read is exempt (paper - /// §4.5). + /// and \p ValueType its value type. Reuses the ref_to_uninit recognizer + /// with its read access preset, so a direct read of a named [[uninit]] + /// object is left to the flow-based uninit_read pass. A std::byte read is + /// exempt (paper §4.5). void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index f285f12d463cd..9c8e389f1283c 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -721,14 +721,30 @@ bool SemaProfiles::diagnoseInvalidRefToUninitMarker(const Decl *D, // treat // Unknown as not uninitialized. // -// \p ForRead selects read-through mode -// (SemaProfiles::checkInitProfileReadThrough): a -// directly named [[uninit]] object is then not treated as uninitialized, -// because a read of such a named object is the flow-based uninit_read pass's -// responsibility; only indirection through a [[ref_to_uninit]] -// pointer/reference still counts. +// How the classified expression is being accessed is carried by +// UninitAccessOpts below. enum class UninitStorage { Initialized, Uninitialized, Unknown }; +// How an expression is being used, for the uninit recognizers. +// +// DropTopLevelUninit: a *directly named* [[uninit]] entity does not count as +// uninitialized. A value access of such an entity is the flow-based passes' +// responsibility (the CFG uninit_read pass and the ctor-body pass credit +// assignments), so the read-through check must not second-guess them. The +// flag is cleared at the first subobject step (member access, array element), +// where no flow pass tracks the storage; only indirection through a +// [[ref_to_uninit]] pointer/reference and below-top-level markers still count. +struct UninitAccessOpts { + bool DropTopLevelUninit = false; + + UninitAccessOpts withoutTopLevelDrop() const { return {}; } +}; + +// Presets: a binding source (markers count everywhere) and a value read (the +// top-level drop applies). +constexpr UninitAccessOpts UninitBindAccess{}; +constexpr UninitAccessOpts UninitReadAccess{/*DropTopLevelUninit=*/true}; + // Combine the arms of a conditional: Uninitialized dominates (either arm may be // taken), then Unknown, else Initialized. static UninitStorage combineArms(UninitStorage A, UninitStorage B) { @@ -739,8 +755,9 @@ static UninitStorage combineArms(UninitStorage A, UninitStorage B) { return UninitStorage::Initialized; } -static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, - bool ForRead = false); +static UninitStorage +glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + UninitAccessOpts Opts = UninitBindAccess); const ValueDecl *SemaProfiles::getDirectlyNamedDecl(const Expr *E) { E = E->IgnoreParenImpCasts(); @@ -790,9 +807,9 @@ static UninitStorage classifyRefToUninitCallee(const CallExpr *CE) { // \p E is a pointer prvalue. Classifies whether it points to uninitialized // storage. -static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, - const Expr *E, - bool ForRead = false) { +static UninitStorage +pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, + UninitAccessOpts Opts = UninitBindAccess) { if (!E) return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); @@ -800,23 +817,23 @@ static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, if (auto PassThrough = classifyUninitPassThrough( E, /*EmptyListState=*/UninitStorage::Initialized, [&](const Expr *Sub) { - return pointerRefersToUninitStorage(Ctx, Sub, ForRead); + return pointerRefersToUninitStorage(Ctx, Sub, Opts); })) return *PassThrough; // Array-to-pointer decay has been stripped above, leaving the array glvalue. - // Leave read mode here, like the member-access arm: neither the CFG + // Clear the top-level drop here, like the member-access arm: neither the CFG // uninit_read pass nor the ctor-body pass tracks array elements, and // element-wise delayed initialization of an [[uninit]] array is itself // banned (paper §5.5), so below an element access the marker counts even - // for a read. + // for a value access. if (E->getType()->isArrayType()) - return glvalueDenotesUninitStorage(Ctx, E, /*ForRead=*/false); + return glvalueDenotesUninitStorage(Ctx, E, Opts.withoutTopLevelDrop()); // &G, where G denotes uninitialized storage. if (const auto *UO = dyn_cast(E)) if (UO->getOpcode() == UO_AddrOf) - return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), ForRead); + return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), Opts); // A value of a [[ref_to_uninit]] pointer is Uninitialized; an unmarked // named pointer is a trusted Initialized pointer (paper §4.3). @@ -852,14 +869,14 @@ static UninitStorage pointerRefersToUninitStorage(ASTContext &Ctx, // manufactured from an integer (operand not a pointer) is Unknown. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->getType()->isPointerType()) - return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), ForRead); + return pointerRefersToUninitStorage(Ctx, CE->getSubExpr(), Opts); return UninitStorage::Unknown; } // \p E is a glvalue. Classifies whether it denotes uninitialized storage. -static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, - const Expr *E, bool ForRead) { +static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, + UninitAccessOpts Opts) { if (!E) return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); @@ -867,7 +884,7 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, if (auto PassThrough = classifyUninitPassThrough( E, /*EmptyListState=*/UninitStorage::Unknown, [&](const Expr *Sub) { - return glvalueDenotesUninitStorage(Ctx, Sub, ForRead); + return glvalueDenotesUninitStorage(Ctx, Sub, Opts); })) return *PassThrough; @@ -875,11 +892,12 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, // if it is a reference marked [[ref_to_uninit]] (the glvalue is its referent, // which is uninitialized). A [[ref_to_uninit]] *pointer* named here denotes // the pointer object itself -- which is initialized -- so it does not count. - // In read mode the [[uninit]] arm is dropped: a direct read of a named - // [[uninit]] object is left to the flow-based uninit_read pass, so only a - // [[ref_to_uninit]] reference (or indirection, handled below) still counts. + // Under the top-level drop the [[uninit]] arm is skipped: a value access of + // a directly named [[uninit]] object is the flow-based passes' territory, so + // only a [[ref_to_uninit]] reference (or indirection, handled below) still + // counts. auto DeclDenotesUninit = [&](const ValueDecl *VD) { - return (!ForRead && VD->hasAttr()) || + return (!Opts.DropTopLevelUninit && VD->hasAttr()) || (VD->getType()->isReferenceType() && VD->hasAttr()); }; if (const auto *DRE = dyn_cast(E)) @@ -888,38 +906,38 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, if (const auto *ME = dyn_cast(E)) { // a->m reaches m through the pointer a (object *a); a.m through the // glvalue a. When m does not itself denote uninit storage, the subobject is - // uninit exactly when its base is. The base recursion leaves read mode: - // the ForRead drop exists because a *directly named* [[uninit]] object and + // uninit exactly when its base is. The base recursion clears the top-level + // drop: the drop exists because a *directly named* [[uninit]] object and // a current-object member are flow-tracked (by the CFG uninit_read pass // and the ctor-body pass, which credit assignments), but neither pass // tracks a subobject reached through a member access -- and member-wise // delayed initialization of an [[uninit]] object is itself banned (paper // §5.4; only whole-object construct_at re-initializes, which is uniformly - // unmodeled) -- so below the top level the marker counts even for a read. + // unmodeled) -- so below the top level the marker counts for every access. if (DeclDenotesUninit(ME->getMemberDecl())) return UninitStorage::Uninitialized; - return ME->isArrow() ? pointerRefersToUninitStorage(Ctx, ME->getBase(), - /*ForRead=*/false) - : glvalueDenotesUninitStorage(Ctx, ME->getBase(), - /*ForRead=*/false); + return ME->isArrow() ? pointerRefersToUninitStorage( + Ctx, ME->getBase(), Opts.withoutTopLevelDrop()) + : glvalueDenotesUninitStorage( + Ctx, ME->getBase(), Opts.withoutTopLevelDrop()); } // A call to a [[ref_to_uninit]]-returning reference function: the referent // it returns is uninitialized. if (const auto *CE = dyn_cast(E)) return classifyRefToUninitCallee(CE); if (const auto *ASE = dyn_cast(E)) - return pointerRefersToUninitStorage(Ctx, ASE->getBase(), ForRead); + return pointerRefersToUninitStorage(Ctx, ASE->getBase(), Opts); // *p, where p points to uninitialized storage. if (const auto *UO = dyn_cast(E)) if (UO->getOpcode() == UO_Deref) - return pointerRefersToUninitStorage(Ctx, UO->getSubExpr(), ForRead); + return pointerRefersToUninitStorage(Ctx, UO->getSubExpr(), Opts); // A reference cast (an explicit cast yielding a glvalue) denotes the same // storage as its operand; propagate. Symmetric to the pointer-cast arm. if (const auto *CE = dyn_cast(E)) if (CE->getSubExpr()->isGLValue()) - return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), ForRead); + return glvalueDenotesUninitStorage(Ctx, CE->getSubExpr(), Opts); return UninitStorage::Unknown; } @@ -1065,7 +1083,7 @@ void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, return; if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) return; - if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, /*ForRead=*/true) != + if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, UninitReadAccess) != UninitStorage::Uninitialized) return; Diag(Loc, diag::err_init_uninit_read_through) From 80c6d11b2a6af5d504d86e85f3734e001ac14e61 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 13:04:38 -0400 Subject: [PATCH 208/289] Add a write mode to the uninit recognizers --- clang/lib/Sema/SemaProfiles.cpp | 69 ++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 9c8e389f1283c..6f74a1dd6c9fa 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -730,20 +730,35 @@ enum class UninitStorage { Initialized, Uninitialized, Unknown }; // DropTopLevelUninit: a *directly named* [[uninit]] entity does not count as // uninitialized. A value access of such an entity is the flow-based passes' // responsibility (the CFG uninit_read pass and the ctor-body pass credit -// assignments), so the read-through check must not second-guess them. The -// flag is cleared at the first subobject step (member access, array element), -// where no flow pass tracks the storage; only indirection through a -// [[ref_to_uninit]] pointer/reference and below-top-level markers still count. +// assignments), so the read-through check must not second-guess them -- and +// for a store, writing the whole named entity IS its initialization (paper +// §4.5: for a built-in type, a write is its initialization). The flag is +// cleared at the first subobject step (member access, array element), where +// no flow pass tracks the storage and only whole-object construct_at could +// re-initialize (paper §5.4). +// +// TrustRefToUninit: [[ref_to_uninit]] markers are ignored -- the storage +// reached through a marked pointer/reference (or returned by a marked +// function) classifies as Unknown rather than Uninitialized. Stores use this: +// a scalar write through the marker is the pointee's initialization (paper +// §4.5), and verifying class-type writes (construct_at) is a deferred slice, +// so a store through the marker must be neither banned nor endorsed. struct UninitAccessOpts { bool DropTopLevelUninit = false; + bool TrustRefToUninit = false; - UninitAccessOpts withoutTopLevelDrop() const { return {}; } + UninitAccessOpts withoutTopLevelDrop() const { + return {false, TrustRefToUninit}; + } }; -// Presets: a binding source (markers count everywhere) and a value read (the -// top-level drop applies). +// Presets: a binding source (markers count everywhere), a value read (the +// top-level drop applies), and a scalar store (additionally, storage reached +// through [[ref_to_uninit]] is trusted). constexpr UninitAccessOpts UninitBindAccess{}; constexpr UninitAccessOpts UninitReadAccess{/*DropTopLevelUninit=*/true}; +constexpr UninitAccessOpts UninitWriteAccess{/*DropTopLevelUninit=*/true, + /*TrustRefToUninit=*/true}; // Combine the arms of a conditional: Uninitialized dominates (either arm may be // taken), then Unknown, else Initialized. @@ -795,13 +810,18 @@ classifyUninitPassThrough(const Expr *E, UninitStorage EmptyListState, } // A call to a [[ref_to_uninit]]-returning function yields uninitialized -// storage (the pointed-to memory, or the returned referent). An unmarked -// direct callee is trusted Initialized (paper §4.3); a call with no direct -// callee (through a function pointer) is Unknown. Shared by both recognizers. -static UninitStorage classifyRefToUninitCallee(const CallExpr *CE) { - if (const FunctionDecl *FD = CE->getDirectCallee()) - return FD->hasAttr() ? UninitStorage::Uninitialized - : UninitStorage::Initialized; +// storage (the pointed-to memory, or the returned referent) -- deferred to +// Unknown when the marker is trusted (a store). An unmarked direct callee is +// trusted Initialized (paper §4.3); a call with no direct callee (through a +// function pointer) is Unknown. Shared by both recognizers. +static UninitStorage classifyRefToUninitCallee(const CallExpr *CE, + UninitAccessOpts Opts) { + if (const FunctionDecl *FD = CE->getDirectCallee()) { + if (!FD->hasAttr()) + return UninitStorage::Initialized; + return Opts.TrustRefToUninit ? UninitStorage::Unknown + : UninitStorage::Uninitialized; + } return UninitStorage::Unknown; } @@ -835,13 +855,17 @@ pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, if (UO->getOpcode() == UO_AddrOf) return glvalueDenotesUninitStorage(Ctx, UO->getSubExpr(), Opts); - // A value of a [[ref_to_uninit]] pointer is Uninitialized; an unmarked - // named pointer is a trusted Initialized pointer (paper §4.3). - if (const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(E)) - return VD->hasAttr() ? UninitStorage::Uninitialized - : UninitStorage::Initialized; + // A value of a [[ref_to_uninit]] pointer is Uninitialized (Unknown when the + // marker is trusted); an unmarked named pointer is a trusted Initialized + // pointer (paper §4.3). + if (const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(E)) { + if (!VD->hasAttr()) + return UninitStorage::Initialized; + return Opts.TrustRefToUninit ? UninitStorage::Unknown + : UninitStorage::Uninitialized; + } if (const auto *CE = dyn_cast(E)) - return classifyRefToUninitCallee(CE); + return classifyRefToUninitCallee(CE, Opts); // A default-initialized new-expression (none init style: no initializer // written) whose allocated type's default-initialization leaves a scalar @@ -898,7 +922,8 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, // counts. auto DeclDenotesUninit = [&](const ValueDecl *VD) { return (!Opts.DropTopLevelUninit && VD->hasAttr()) || - (VD->getType()->isReferenceType() && VD->hasAttr()); + (!Opts.TrustRefToUninit && VD->getType()->isReferenceType() && + VD->hasAttr()); }; if (const auto *DRE = dyn_cast(E)) return DeclDenotesUninit(DRE->getDecl()) ? UninitStorage::Uninitialized @@ -924,7 +949,7 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, // A call to a [[ref_to_uninit]]-returning reference function: the referent // it returns is uninitialized. if (const auto *CE = dyn_cast(E)) - return classifyRefToUninitCallee(CE); + return classifyRefToUninitCallee(CE, Opts); if (const auto *ASE = dyn_cast(E)) return pointerRefersToUninitStorage(Ctx, ASE->getBase(), Opts); From caa830469889daa056f1b7fa3aeb0c354473f79c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 13:12:09 -0400 Subject: [PATCH 209/289] Diagnose subobject writes of [[uninit]] objects under std::init --- .../clang/Basic/DiagnosticSemaKinds.td | 3 +++ clang/include/clang/Sema/SemaProfiles.h | 13 +++++++++++ clang/lib/Sema/SemaExpr.cpp | 15 +++++++++++++ clang/lib/Sema/SemaProfiles.cpp | 20 +++++++++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 22 ++++++++++++------- 5 files changed, 65 insertions(+), 8 deletions(-) diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 9a9d6fda471e5..402c0acee9f6f 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14197,6 +14197,9 @@ def err_init_uninit_read_through : ProfileRuleError< "read %select{through a '[[ref_to_uninit]]' pointer or reference|" "of a subobject of an '[[uninit]]' object}1 accesses " "uninitialized memory under profile '%0'">; +def err_init_uninit_subobject_write : ProfileRuleError< + "writing %select{a member|an element}1 of an '[[uninit]]' object does not " + "initialize it under profile '%0'; initialize the whole object">; def err_init_uninit_ref_capture : ProfileRuleError< "capturing %1 by reference binds a reference to uninitialized memory " "under profile '%0'">; diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index ad7153a23d4c5..da1c81a52147f 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -237,6 +237,19 @@ class SemaProfiles : public SemaBase { void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType); + /// std::init / uninit_write (paper §5.4-§5.6): diagnose a scalar store to a + /// proper subobject of a named [[uninit]] entity -- delayed piecemeal + /// initialization, which only whole-object construct_at could make good. + /// Called from Sema::CheckAssignmentOperands (the shared simple/compound + /// assignment funnel) and from the built-in increment/decrement arm of + /// Sema::CreateBuiltinUnaryOp, with \p LHS the store target. Reuses the + /// recognizer with its write access preset: a store to the whole named + /// entity is its initialization (paper §4.5), and storage reached through + /// [[ref_to_uninit]] is trusted (the deferred construct_at slice), so only + /// a below-top-level [[uninit]] marker fires. A std::byte store is exempt + /// (paper §4.5). + void checkInitProfileSubobjectWrite(SourceLocation Loc, const Expr *LHS); + /// std::init / ref_to_uninit (paper §4.3): a by-reference lambda capture of /// \p Var binds a reference to its storage, and a capture cannot carry /// [[ref_to_uninit]], so capturing an entity that denotes uninitialized diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index af03dd96e76fc..ab93e09a71ea5 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -14404,6 +14404,14 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS, if (CheckForModifiableLvalue(LHSExpr, Loc, *this)) return QualType(); + // std::init / uninit_write (paper §5.4-§5.6): a scalar store to a subobject + // of a named [[uninit]] object is banned delayed initialization. This is + // the shared funnel for simple and every compound assignment, so the check + // fires exactly once per built-in assignment; class-typed operator= never + // reaches it. + if (getLangOpts().Profiles) + Profiles().checkInitProfileSubobjectWrite(Loc, LHSExpr); + QualType LHSType = LHSExpr->getType(); QualType RHSType = CompoundType.isNull() ? RHS.get()->getType() : CompoundType; @@ -16184,6 +16192,13 @@ ExprResult Sema::CreateBuiltinUnaryOp(SourceLocation OpLoc, Opc == UO_PreInc || Opc == UO_PostInc, Opc == UO_PreInc || Opc == UO_PreDec); CanOverflow = isOverflowingIntegerType(Context, resultType); + // std::init / uninit_write (paper §5.4-§5.6): a built-in ++/-- stores to + // its operand like an assignment does to its LHS. Checked here rather + // than in CheckIncrementDecrementOperand, which self-recurses on + // placeholder operands and would fire twice; overloaded class ++/-- + // never reaches CreateBuiltinUnaryOp. + if (getLangOpts().Profiles && !resultType.isNull()) + Profiles().checkInitProfileSubobjectWrite(OpLoc, Input.get()); break; case UO_AddrOf: resultType = CheckAddressOfOperand(Input, OpLoc); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 6f74a1dd6c9fa..1c87ebba90c9d 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1115,6 +1115,26 @@ void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, << "std::init" << (isMemberChainOfUninitObject(Glvalue) ? 1 : 0); } +void SemaProfiles::checkInitProfileSubobjectWrite(SourceLocation Loc, + const Expr *LHS) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // a store the user wrote, so it must not drive this rule. + if (!LHS || isa(LHS->IgnoreParens())) + return; + if (deferUninitCheckOnTemplatePattern(SemaRef, /*D=*/nullptr)) + return; + // Paper §4.5: an uninitialized std::byte may be manipulated freely. + if (getASTContext().getBaseElementType(LHS->getType())->isStdByteType()) + return; + if (!shouldEmitProfileViolation("std::init", "uninit_write", Loc)) + return; + if (glvalueDenotesUninitStorage(getASTContext(), LHS, UninitWriteAccess) != + UninitStorage::Uninitialized) + return; + Diag(Loc, diag::err_init_uninit_subobject_write) + << "std::init" << !isa(LHS->IgnoreParenImpCasts()); +} + namespace { // Row for the unified finalization dispatch shared by class-finalization diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index e48e399c0fa0d..90184ad2600ef 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -780,12 +780,15 @@ void test_member_read_of_uninit_object() { (void)y1; (void)y2; (void)y3; } -// Writes, discarded values, and address-taking apply no lvalue-to-rvalue -// conversion and are not reads; taking the member's address is the binding -// checks' territory (unchanged behavior, retested as a regression guard). +// Discarded values and address-taking apply no lvalue-to-rvalue conversion +// and are not reads; taking the member's address is the binding checks' +// territory (unchanged behavior, retested as a regression guard). A write is +// not a read either, but a subobject store of an [[uninit]] object is itself +// banned as delayed initialization (uninit_write; full coverage in +// safety-profile-init-write.cpp). void test_member_read_negatives() { Pair s [[uninit]]; - s.x = 1; // OK: write, not a read (writes are a deferred slice) + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} (void)s.x; // OK: discarded value int *p = &s.x; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} int *q [[ref_to_uninit]] = &s.x; // OK: binding checked by ref_to_uninit @@ -829,12 +832,15 @@ struct WithArrMember { int get() { return a[0]; } // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} }; -// Writes, discarded values, and address-taking apply no lvalue-to-rvalue -// conversion and are not reads; bindings to the array or its elements stay the -// ref_to_uninit checks' territory (regression guards, unchanged behavior). +// Discarded values and address-taking apply no lvalue-to-rvalue conversion +// and are not reads; bindings to the array or its elements stay the +// ref_to_uninit checks' territory (regression guards, unchanged behavior). An +// element store is not a read either, but is itself banned as delayed +// initialization (uninit_write; full coverage in +// safety-profile-init-write.cpp). void test_element_read_negatives(int i) { [[uninit]] int a[2]; - a[0] = 1; // OK: write, not a read (writes are a deferred slice) + a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} (void)a[0]; // OK: discarded value int *p [[ref_to_uninit]] = &a[0]; // OK: address-of is not a read; binding checked by ref_to_uninit int *q [[ref_to_uninit]] = a; // OK: array decay is a binding, not a read From 969293486874dd7cdd50ae9d6ed02de13ab48951 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 13:17:34 -0400 Subject: [PATCH 210/289] Test std::init subobject writes of [[uninit]] objects --- .../SemaCXX/safety-profile-init-write.cpp | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-init-write.cpp diff --git a/clang/test/SemaCXX/safety-profile-init-write.cpp b/clang/test/SemaCXX/safety-profile-init-write.cpp new file mode 100644 index 0000000000000..123be5a3976bf --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-write.cpp @@ -0,0 +1,202 @@ +// RUN: %clang_cc1 -fsyntax-only -verify -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// std::init / uninit_write (paper §5.4-§5.6): a scalar store to a proper +// subobject of a named [[uninit]] entity is banned delayed initialization -- +// only writing the whole named entity initializes it (paper §4.5), and only +// whole-object construct_at could make a piecemeal-initialized object good. + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +namespace std { enum class byte : unsigned char {}; } + +struct Pair { int x; int y; }; + +bool cond(); + +// A store to the whole named entity is its initialization (paper §4.5), for +// the marked local itself and for a directly-targeted marked member. +void test_whole_entity_store_is_initialization() { + int x [[uninit]]; + x = 7; // OK: the write initializes x + (void)x; +} + +struct WithMarkedMember { int m [[uninit]]; }; +void test_direct_marked_member_store(WithMarkedMember &o) { + o.m = 1; // OK: the store targets the marked member itself +} + +// The §5.2 constructor-body pattern is untouched: a current-object member +// store reaches the member through `this` (a pointer), in every spelling. +struct CtorBody { + int m [[uninit]]; + CtorBody() { + m = 1; // OK + this->m = 2; // OK + (*this).m = 3; // OK + } +}; + +// Member stores below a named [[uninit]] object (paper §5.4). +void test_member_store(bool c) { + Pair s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + (&s)->x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + Pair t [[uninit]]; + (c ? s : t).x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + + WithMarkedMember b [[uninit]]; + b.m = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + + Pair u; // expected-error {{variable 'u' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + u.x = 1; // OK: 'u' is not marked; its diagnostic is uninit_decl's, at the declaration +} + +// A marked *member* below the top level bans the store the same way, on a +// named object or on the current object -- a class-type member has no legal +// piecemeal path (only whole-object construct_at, which is unmodeled). +struct HasAgg { Pair agg [[uninit]]; }; +void test_nested_marked_member_store(HasAgg &h) { + h.agg.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +struct SelfAgg { + Pair agg [[uninit]]; + void set() { + agg.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +}; + +// Element stores of [[uninit]] arrays are the §5.5 random-access-init ban. +void test_element_store(int i) { + [[uninit]] int a[2]; + a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + a[i] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + *a = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + [[uninit]] int m2[2][2]; + m2[1][0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// A marked array *member*'s elements are proper subobjects of the marked +// array, so element stores are banned even on the current object. +struct WithArrMember { + [[uninit]] int a[2]; + void set() { + a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +}; +void test_member_array_element_store(WithArrMember &w) { + w.a[0] = 1; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Member assignment to a marker-retaining union is the §5.6 ban (the marker +// itself is union_marker's; here it is suppressed and retained). +union U { int x; float y; }; +void test_union_member_store() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] U u [[uninit]]; + u.x = 9; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Compound assignments and built-in increments store like plain assignment. +// A compound assignment also reads the old value; where its operand +// conversion performs the load (the shift forms promote their LHS), the +// read-through diagnostic fires alongside. +void test_compound_and_incdec_stores() { + Pair s [[uninit]]; + s.x += 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + s.x <<= 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + ++s.x; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + s.x--; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + [[uninit]] int a[2]; + --a[0]; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Writes through a [[ref_to_uninit]] pointer or reference are the deferred +// construct_at slice: for a built-in type the write is the pointee's +// initialization (paper §4.5), so they are neither banned nor endorsed. +[[ref_to_uninit]] int &get_uninit_ref(); +void test_write_through_ref_to_uninit(int *p [[ref_to_uninit]], + int &r [[ref_to_uninit]], + Pair *ptr [[ref_to_uninit]]) { + *p = 5; // OK + p[3] = 0; // OK + *p += 1; // OK + r = 5; // OK + ptr->x = 5; // OK + get_uninit_ref() = 5; // OK +} + +// std::byte may be left uninitialized and manipulated freely (paper §4.5). +void test_byte_exempt() { + [[uninit]] std::byte b[2]; + b[0] = std::byte{1}; // OK +} + +// The §5.4 sanctioned route for whole-object (re)initialization: a +// [[ref_to_uninit]]-taking construct_at, whose parameter binding accepts &s. +template +T *construct_at(T *p [[ref_to_uninit]], Args &&...args); + +void test_construct_at_pattern() { + Pair s [[uninit]]; + construct_at(&s, 1, 2); // OK: the marked parameter accepts &s + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// Suppression: whole-profile and rule-targeted, on statements and on the +// enclosing declaration; a different rule does not cover the store. +void test_suppress_stmt() { + Pair s [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { s.x = 1; } // OK + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_write")]] { s.y = 2; } // OK + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { + s.x = 3; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "uninit_write")]] +void test_suppress_decl() { + Pair s [[uninit]]; + s.x = 1; // OK: the function-level suppression covers the body +} + +// Like every Decl-less expression check, the store check defers on a template +// pattern and fires once, at instantiation; a never-instantiated template and +// a discarded if-constexpr branch stay silent. +template +void template_write_bad() { + Pair s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +template void template_write_bad(); // expected-note {{in instantiation of function template specialization 'template_write_bad' requested here}} + +template +void template_write_dependent_bad() { + T s [[uninit]]; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +template void template_write_dependent_bad(); // expected-note {{in instantiation of function template specialization 'template_write_dependent_bad' requested here}} + +template +void template_write_never_instantiated() { + Pair s [[uninit]]; + s.x = 1; +} + +template +void template_write_discarded_branch() { + Pair s [[uninit]]; + if constexpr (false) { + s.x = 1; + } else { + s.x = 2; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + } +} +template void template_write_discarded_branch(); // expected-note {{in instantiation of function template specialization 'template_write_discarded_branch' requested here}} From ff699223101fa97a212c6361df4560f9bdbe8149 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 13:19:55 -0400 Subject: [PATCH 211/289] Document the std::init uninit_write rule --- clang/docs/ProfilesFramework.rst | 82 ++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 4692296eba19e..e3474e8e6ecb2 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -742,7 +742,9 @@ for the user): the obligation is keyed on a read before assignment, not on the constructor's end. A scalar *read through* a ``[[ref_to_uninit]]`` pointer/reference is diagnosed at the lvalue-to-rvalue conversion (R7, ``uninit_read``); *writes* through such a pointer/reference are not yet verified -(the paper relegates them to ``construct_at`` or suppression). +(the paper relegates them to ``construct_at`` or suppression). A scalar store +to a *subobject* of a named ``[[uninit]]`` entity, by contrast, is banned as +delayed piecemeal initialization (R10, ``uninit_write``). Dynamically-created objects are covered when bound: a ``new`` expression that default-initializes its allocated object and leaves a scalar subobject @@ -1037,7 +1039,9 @@ assignment when compiled without the profile. in addition to the members *of* a union shown above. - The banned marker is retained on the declaration after it is diagnosed, so the ``uninit_decl`` / ``ctor_uninit_member`` rules treat the entity as - acknowledged and do not emit a second, contradictory diagnostic. + acknowledged and do not emit a second, contradictory diagnostic. A member + assignment to such a marker-retaining union (the §5.6 delayed-initialization + ban) is caught by ``uninit_write`` (R10). An *unmarked* union left uninitialized is itself the error (paper §5.6): ``SemaProfiles::defaultInitLeavesScalarIndeterminate`` reports a union as indeterminate @@ -1133,7 +1137,12 @@ recognizers symmetric. operator/condition operands -- all funnel through. It reuses the recognizer with its read access preset (``UninitAccessOpts``), reports the shared rule ``uninit_read`` via ``err_init_uninit_read_through``, and exempts ``std::byte`` - (paper §4.5). The read preset drops the ``[[uninit]]`` marker only for the + (paper §4.5). ``UninitAccessOpts`` carries two axes -- the top-level + ``[[uninit]]`` drop and a ``[[ref_to_uninit]]`` trust flag -- whose presets + distinguish a *binding* source (markers count everywhere), a value *read* + (this rule), and a scalar *store* (``uninit_write``, R10, which shares the + drop and additionally trusts the ``[[ref_to_uninit]]`` arms). + The read preset drops the ``[[uninit]]`` marker only for the *top-level* named entity -- a directly named ``[[uninit]]`` object and a current-object member are flow-tracked (by the CFG ``uninit_read`` pass and the ctor-body pass, which credit assignments), so their direct reads are left @@ -1146,8 +1155,9 @@ recognizers symmetric. subobject a value. Being Decl-less, it defers on a dependent context and fires once, at instantiation. An address-of (``&*p``), a reference binding, a discarded-value expression (``(void)*p``), and a write (``*p = 5`` - or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads. - Out of scope, as remaining limitations: class-type read-through (copy + or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads + (``s.x = 1`` is instead banned as a subobject store by ``uninit_write``, + R10). Out of scope, as remaining limitations: class-type read-through (copy construction from ``*p``) and compound-assignment reads (``*p += 1``, which build no lvalue-to-rvalue node), consistent with the scalar slice and the deferred-writes stance. @@ -1248,6 +1258,68 @@ language rule (paper §3), so it is an initialized object; marking it ``HonorUninitMarkers=false``, R4's factual choice). Exactly one diagnostic fires in every case. +R10. ``uninit_write`` -- pattern 1 +.................................. + +A scalar store to a *proper subobject* of a named ``[[uninit]]`` entity is +banned delayed initialization (paper §1 "reading or writing uninitialized +memory is an error"; §5.4 member-wise, §5.5 random-access element, §5.6 +union-member). Writing the whole named entity is that entity's +initialization (paper §4.5: for a built-in type, a write is its +initialization) and stays legal -- the flow passes (R1) credit it -- as does +the §5.2 constructor-body pattern, whose member stores reach the current +object through ``this``. Only whole-object ``construct_at`` could make a +piecemeal-initialized object good, and construct_at flow is uniformly +unmodeled, so no store below a marked entity can be part of a valid +initialization sequence. + +.. code-block:: c++ + + struct S { int x; int y; }; + + void f() { + S s [[uninit]]; + s.x = 1; // error: writing a member of an [[uninit]] object (uninit_write) + [[uninit]] int a[2]; + a[0] = 1; // error: writing an element of an [[uninit]] object + int v [[uninit]]; + v = 7; // OK: the write initializes the whole entity + } + + void g(int *p [[ref_to_uninit]]) { + *p = 5; // OK: a write through the marker is the pointee's + // initialization (the deferred construct_at slice) + } + +- Diagnostic: ``err_init_uninit_subobject_write`` (a ``%select`` + distinguishes a member store from an element store). +- Recognizer: the shared classifier run with its *write* access preset + (``UninitAccessOpts``, see R7): the top-level drop makes a whole-entity + store legal, and ``TrustRefToUninit`` classifies storage reached through a + ``[[ref_to_uninit]]`` pointer/reference (or returned by a marked function) + as unknown, so a store through the marker is neither banned nor endorsed. + The shared arms cover member chains, array elements (``a[i]``, ``*a``, + member arrays -- on a named object or the current one), ``(&s)->x``, and + the conditional/comma/braced pass-through bases. +- Check sites: ``Sema::CheckAssignmentOperands`` -- the funnel both simple + and compound assignment converge on, so ``=`` and every ``op=`` are checked + exactly once, and class-typed ``operator=`` (which diverts to overload + resolution) never reaches it -- and the built-in increment/decrement arm of + ``Sema::CreateBuiltinUnaryOp`` (overloaded class ``++``/``--`` never + reaches it). Both are Decl-less sites: they defer on a template pattern + via the dependent-context guard and fire once, at instantiation. +- A compound assignment also *reads* the old value; where its operand + conversion performs the load (the shift forms promote their LHS), the R7 + read-through diagnostic fires alongside this rule's. +- ``std::byte`` stores are exempt (paper §4.5), matching every read-side + rule. +- Known gaps: whole-object assignment to a marked class object + (``s = S{...}``) goes through the overloaded ``operator=`` path and is not + checked -- class-type writes are uniformly deferred with construct_at -- + and writes through ``[[ref_to_uninit]]`` are deliberately out of scope (for + a scalar the write is the initialization; verifying class-type writes needs + construct_at modeling). + Diagnostic suppression ~~~~~~~~~~~~~~~~~~~~~~ From 37f8b2b676b28717b38a16f7e9ab4c01d3595293 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 13:41:10 -0400 Subject: [PATCH 212/289] Check read-through on compound-assignment and increment operands --- clang/docs/ProfilesFramework.rst | 21 +++++++---- clang/include/clang/Sema/SemaProfiles.h | 12 ++++--- clang/lib/Sema/SemaExpr.cpp | 19 ++++++++-- .../safety-profile-init-ref-to-uninit.cpp | 35 +++++++++++++++++++ .../SemaCXX/safety-profile-init-write.cpp | 28 +++++++++------ 5 files changed, 92 insertions(+), 23 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index e3474e8e6ecb2..48818bcb89e7a 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1157,10 +1157,16 @@ recognizers symmetric. binding, a discarded-value expression (``(void)*p``), and a write (``*p = 5`` or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads (``s.x = 1`` is instead banned as a subobject store by ``uninit_write``, - R10). Out of scope, as remaining limitations: class-type read-through (copy - construction from ``*p``) and compound-assignment reads (``*p += 1``, which - build no lvalue-to-rvalue node), consistent with the scalar slice and the - deferred-writes stance. + R10). A compound assignment (``*p += 1``) and a built-in ``++``/``--`` + also read the old value while building no lvalue-to-rvalue node; their + loads are checked at the operator sites instead + (``Sema::CheckAssignmentOperands`` for the non-shift compounds, the + increment/decrement arm of ``Sema::CreateBuiltinUnaryOp`` for + ``++``/``--``), while the shift compounds keep loading through the + chokepoint via their LHS promotion and are excluded from the operator hook, + so exactly one diagnostic fires. Out of scope, as the remaining + limitation: class-type read-through (copy construction from ``*p``), + consistent with the scalar slice and the deferred-writes stance. - Known gaps: recognition is purely of the source's syntactic form, so a binding whose underlying operand is unrecognized -- pointer arithmetic, an integer-to-pointer cast, a call through a function pointer (no @@ -1308,9 +1314,10 @@ initialization sequence. ``Sema::CreateBuiltinUnaryOp`` (overloaded class ``++``/``--`` never reaches it). Both are Decl-less sites: they defer on a template pattern via the dependent-context guard and fire once, at instantiation. -- A compound assignment also *reads* the old value; where its operand - conversion performs the load (the shift forms promote their LHS), the R7 - read-through diagnostic fires alongside this rule's. +- A compound assignment or a built-in ``++``/``--`` also *reads* the old + value, so on a subobject of a marked object the R7 read-through diagnostic + fires alongside this rule's (the shift forms load through their LHS + promotion, the rest through R7's operator-site hooks). - ``std::byte`` stores are exempt (paper §4.5), matching every read-side rule. - Known gaps: whole-object assignment to a marked class object diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index da1c81a52147f..bc5ce8abcd1df 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -230,10 +230,14 @@ class SemaProfiles : public SemaBase { /// [[ref_to_uninit]] pointer or reference, whose result is itself /// uninitialized. Called from Sema::DefaultLvalueConversion at the single /// lvalue-to-rvalue chokepoint, with \p Glvalue the operand being loaded - /// and \p ValueType its value type. Reuses the ref_to_uninit recognizer - /// with its read access preset, so a direct read of a named [[uninit]] - /// object is left to the flow-based uninit_read pass. A std::byte read is - /// exempt (paper §4.5). + /// and \p ValueType its value type; and from the compound-assignment + /// (Sema::CheckAssignmentOperands) and increment/decrement + /// (Sema::CreateBuiltinUnaryOp) operator sites, whose reads build no + /// lvalue-to-rvalue node (the shift-compounds are excluded there because + /// their LHS promotion already funnels through the chokepoint). Reuses the + /// ref_to_uninit recognizer with its read access preset, so a direct read + /// of a named [[uninit]] object is left to the flow-based uninit_read + /// pass. A std::byte read is exempt (paper §4.5). void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType); diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index ab93e09a71ea5..aa289e5ae96d4 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -14409,8 +14409,17 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS, // the shared funnel for simple and every compound assignment, so the check // fires exactly once per built-in assignment; class-typed operator= never // reaches it. - if (getLangOpts().Profiles) + if (getLangOpts().Profiles) { + // A compound assignment reads the old value but builds no + // lvalue-to-rvalue node for it, so the DefaultLvalueConversion + // read-through chokepoint never sees the load; check it here. The shift + // forms are the exception: CheckShiftOperands promotes their LHS through + // DefaultLvalueConversion, which has already fired for them. + if (!CompoundType.isNull() && Opc != BO_ShlAssign && Opc != BO_ShrAssign) + Profiles().checkInitProfileReadThrough(LHSExpr->getExprLoc(), LHSExpr, + LHSExpr->getType()); Profiles().checkInitProfileSubobjectWrite(Loc, LHSExpr); + } QualType LHSType = LHSExpr->getType(); QualType RHSType = CompoundType.isNull() ? RHS.get()->getType() : @@ -16197,8 +16206,14 @@ ExprResult Sema::CreateBuiltinUnaryOp(SourceLocation OpLoc, // than in CheckIncrementDecrementOperand, which self-recurses on // placeholder operands and would fire twice; overloaded class ++/-- // never reaches CreateBuiltinUnaryOp. - if (getLangOpts().Profiles && !resultType.isNull()) + if (getLangOpts().Profiles && !resultType.isNull()) { + // ++/-- reads the old value with no lvalue-to-rvalue node (unlike -x + // or !x); check the load here. + Profiles().checkInitProfileReadThrough(Input.get()->getExprLoc(), + Input.get(), + Input.get()->getType()); Profiles().checkInitProfileSubobjectWrite(OpLoc, Input.get()); + } break; case UO_AddrOf: resultType = CheckAddressOfOperand(Input, OpLoc); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 90184ad2600ef..915129b95ce13 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -918,6 +918,41 @@ void template_read_never_instantiated(int *p [[ref_to_uninit]]) { (void)y; } +// A compound assignment and a built-in ++/-- read the old value before +// storing, but build no lvalue-to-rvalue node for the operand, so the +// operator sites check the load directly. Every compound form through a +// [[ref_to_uninit]] pointer or reference is diagnosed; the shift forms load +// through their LHS promotion instead and must fire exactly once. Reading an +// unmarked pointer's pointee is trusted, and ++ on the marked pointer itself +// reads the (initialized) pointer object, not through it. +void test_compound_read_through(int *p [[ref_to_uninit]], int *q, + int &r [[ref_to_uninit]], + Inner *ptr [[ref_to_uninit]], int i) { + *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + p[i] -= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + r *= 2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ptr->m |= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ++*p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (*p)--; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ++r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p <<= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *q += 1; // OK: an unmarked pointer is trusted initialized + ++p; // OK: reads the pointer object itself, not through it +} + +void test_compound_read_suppress(int *p [[ref_to_uninit]]) { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { *p += 1; } // OK: rule-targeted suppress +} + +// Like every Decl-less read check, the compound read defers on a template +// pattern and fires once, at instantiation. +template +void template_compound_read_bad(int *p [[ref_to_uninit]]) { + *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} +template void template_compound_read_bad(int *); // expected-note {{in instantiation of function template specialization 'template_compound_read_bad' requested here}} + // std::init / ref_to_uninit (paper §5): a pointer/reference member given a // *written* constructor member-initializer is checked with the enclosing // constructor as the Decl, so a class-template pattern defers and fires once at diff --git a/clang/test/SemaCXX/safety-profile-init-write.cpp b/clang/test/SemaCXX/safety-profile-init-write.cpp index 123be5a3976bf..9a85d15579f6d 100644 --- a/clang/test/SemaCXX/safety-profile-init-write.cpp +++ b/clang/test/SemaCXX/safety-profile-init-write.cpp @@ -99,31 +99,39 @@ void test_union_member_store() { u.x = 9; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} } -// Compound assignments and built-in increments store like plain assignment. -// A compound assignment also reads the old value; where its operand -// conversion performs the load (the shift forms promote their LHS), the -// read-through diagnostic fires alongside. +// Compound assignments and built-in increments store like plain assignment, +// and every one of them also reads the old value: the shift forms load +// through their LHS promotion (DefaultLvalueConversion), the rest are checked +// at the operator sites, so each fires the read-through diagnostic exactly +// once alongside the write. void test_compound_and_incdec_stores() { Pair s [[uninit]]; - s.x += 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + s.x += 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} s.x <<= 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} - ++s.x; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} - s.x--; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + ++s.x; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + s.x--; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} [[uninit]] int a[2]; - --a[0]; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + --a[0]; // expected-error {{writing an element of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} } // Writes through a [[ref_to_uninit]] pointer or reference are the deferred // construct_at slice: for a built-in type the write is the pointee's -// initialization (paper §4.5), so they are neither banned nor endorsed. +// initialization (paper §4.5), so they are neither banned nor endorsed. A +// compound assignment through the marker still *reads* the old value, which +// is a genuine uninit_read violation (full coverage in +// safety-profile-init-ref-to-uninit.cpp). [[ref_to_uninit]] int &get_uninit_ref(); void test_write_through_ref_to_uninit(int *p [[ref_to_uninit]], int &r [[ref_to_uninit]], Pair *ptr [[ref_to_uninit]]) { *p = 5; // OK p[3] = 0; // OK - *p += 1; // OK + *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} r = 5; // OK ptr->x = 5; // OK get_uninit_ref() = 5; // OK From f0b0d9b0539e13466125cfd0d4247fb451f1ec6c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 15:09:00 -0400 Subject: [PATCH 213/289] Pin whole-record copies through [[ref_to_uninit]] in tests and docs --- clang/docs/ProfilesFramework.rst | 14 +++++-- .../safety-profile-init-ref-to-uninit.cpp | 42 +++++++++++++++++++ 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 48818bcb89e7a..b483b63731b18 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1164,9 +1164,17 @@ recognizers symmetric. increment/decrement arm of ``Sema::CreateBuiltinUnaryOp`` for ``++``/``--``), while the shift compounds keep loading through the chokepoint via their LHS promotion and are excluded from the operator hook, - so exactly one diagnostic fires. Out of scope, as the remaining - limitation: class-type read-through (copy construction from ``*p``), - consistent with the scalar slice and the deferred-writes stance. + so exactly one diagnostic fires. A *class-type* copy from ``*p`` (copy-, + direct-, braced-, argument-, or return-copy) never reaches the chokepoint -- + record glvalues undergo no lvalue-to-rvalue conversion -- but is caught all + the same by the *binding* rule at the copy constructor's reference + parameter, so the diagnostic is + ``err_init_uninit_requires_ref_to_uninit`` rather than + ``err_init_uninit_read_through``. The escape is the paper's own (§7.2): a + copy constructor declared with a ``[[ref_to_uninit]]`` reference parameter + accepts the uninitialized source. Because this is a binding, not a read, + the ``std::byte`` exemption does not apply to a record that merely + *contains* ``std::byte`` members. - Known gaps: recognition is purely of the source's syntactic form, so a binding whose underlying operand is unrecognized -- pointer arithmetic, an integer-to-pointer cast, a call through a function pointer (no diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 915129b95ce13..317f27c77a80c 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -809,6 +809,48 @@ void test_member_read_byte_exempt() { (void)b; } +// A whole-record copy from *pp reads the uninitialized pointee (paper +// abstract: an object marked [[ref_to_uninit]] cannot be read through), but +// class types never reach the lvalue-to-rvalue chokepoint. The copy is caught +// all the same, by the binding rule at the copy constructor's reference +// parameter -- so the diagnostic is the binding one, not the read-through +// one. This holds for copy-, direct-, braced-, argument-, and return-copies +// alike. +void take_pair(Pair v); + +Pair test_record_copy_through(Pair *pp [[ref_to_uninit]]) { + Pair v = *pp; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + Pair w(*pp); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + Pair b = {*pp}; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_pair(*pp); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)v; (void)w; (void)b; + return *pp; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// The escape is the paper's own (§7.2): a copy constructor declared with a +// [[ref_to_uninit]] parameter accepts the uninitialized source -- and then, +// symmetrically, rejects an initialized one. +struct MarkedCopy { + int x; + MarkedCopy(); + MarkedCopy(const MarkedCopy &q [[ref_to_uninit]]); +}; + +void test_record_copy_marked_ctor(MarkedCopy *mp [[ref_to_uninit]], + const MarkedCopy &init) { + MarkedCopy v = *mp; // OK: the marked parameter accepts the uninit pointee + MarkedCopy w = init; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)v; (void)w; +} + +// A record *containing* std::byte members is not std::byte, so the §4.5 read +// exemption does not extend to the whole-record binding. +struct ByteBox { std::byte b; }; +void test_record_copy_byte_member(ByteBox *bp [[ref_to_uninit]]) { + ByteBox v = *bp; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)v; +} + // An element read of a named [[uninit]] array is a subobject read exactly like // s.x: neither flow pass tracks array elements (and element-wise delayed // initialization is banned, paper §5.5), so the marker counts below the From 2bdc32bbb234b118431a8193c52fa004e7b4212b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 15:14:26 -0400 Subject: [PATCH 214/289] Check ref_to_uninit on arguments of calls without a declared callee --- clang/docs/ProfilesFramework.rst | 17 +++++++--- clang/include/clang/Sema/SemaProfiles.h | 4 ++- clang/lib/Sema/SemaInit.cpp | 16 ++++++---- clang/lib/Sema/SemaProfiles.cpp | 7 +++-- .../safety-profile-init-ref-to-uninit.cpp | 31 +++++++++++++++++++ 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index b483b63731b18..9ea128f49e846 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1098,9 +1098,15 @@ recognizers symmetric. initialization (``InitListChecker``), pointer assignment (``Sema::CreateBuiltinBinOp``), call arguments at parameter copy-initialization (``Sema::PerformCopyInitialization``, the funnel for - every call form with a declared callee -- plain calls, constructor calls, - overloaded operators, and calls to objects of class type such as functors - and lambdas), arguments supplied by a parameter's default argument + every call form -- plain calls, constructor calls, overloaded operators, + and calls to objects of class type such as functors and lambdas; a call + with *no declared callee*, through a function pointer, is checked there + too, its parameters treated as unmarked targets since no declaration could + carry ``[[ref_to_uninit]]`` (paper §7.2) -- so passing uninitialized + memory through a function pointer diagnoses even when the pointed-to + function's own parameter is marked, the marker being a declaration + property invisible through the pointer; suppress at the call if the flow + is intended), arguments supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``, which reuses the pre-built expression rather than re-running copy-initialization), return statements (``Sema::BuildReturnStmt``), and lambda captures -- an init-capture binds @@ -1177,8 +1183,9 @@ recognizers symmetric. *contains* ``std::byte`` members. - Known gaps: recognition is purely of the source's syntactic form, so a binding whose underlying operand is unrecognized -- pointer arithmetic, an - integer-to-pointer cast, a call through a function pointer (no - ``FunctionDecl``), or a variadic (``...``) argument -- is classified as + integer-to-pointer cast, the *result* of a call through a function pointer + (no ``FunctionDecl`` to read a return marker from), or a variadic (``...``) + argument -- is classified as *unknown* and diagnosed for neither direction. A ``[[ref_to_uninit]]`` target therefore accepts it (rather than the earlier *false positive*), while an unmarked target also accepts it (a remaining missed diagnostic). The diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index bc5ce8abcd1df..7362038ec5964 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -215,7 +215,9 @@ class SemaProfiles : public SemaBase { /// std::init / ref_to_uninit (paper §5): check that binding \p Src to /// \p Target (a variable, data member, parameter, or function) is - /// consistent with the target's [[ref_to_uninit]] marking. \p T is the + /// consistent with the target's [[ref_to_uninit]] marking. A null \p Target + /// is a binding with no declaration to carry the marker (a parameter of a + /// call through a function pointer) and is checked as unmarked. \p T is the /// bound type -- the target's type, or the return type when \p Target is a /// function. No-op unless \p T is a non-dependent pointer or reference (a /// dependent type defers to instantiation, where the check site re-runs diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 8084c795c816f..353dfc372722b 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -10148,13 +10148,17 @@ Sema::PerformCopyInitialization(const InitializedEntity &Entity, // funnel for call arguments from every call form -- GatherArgumentsForCall, // overloaded operators, and calls to objects of class type -- so the binding // check runs here, exactly once per argument. A type-only parameter entity - // (variadic promotion, a call with no declared callee) has no declaration to - // read the marking from and is skipped. A default argument does not re-run + // (a call with no declared callee, e.g. through a function pointer) has no + // declaration that could carry [[ref_to_uninit]], so it is checked as an + // unmarked target (paper §7.2: passing uninitialized memory needs an + // appropriately declared callee). A default argument does not re-run // copy-initialization at the call site; GatherArgumentsForCall checks those. - if (!Result.isInvalid() && Entity.isParameterKind()) - if (const auto *Parm = dyn_cast_or_null(Entity.getDecl())) - Profiles().checkInitProfileRefToUninitBinding(InitE->getExprLoc(), Parm, - Parm->getType(), InitE); + if (!Result.isInvalid() && Entity.isParameterKind()) { + const auto *Parm = dyn_cast_or_null(Entity.getDecl()); + Profiles().checkInitProfileRefToUninitBinding( + InitE->getExprLoc(), Parm, Parm ? Parm->getType() : Entity.getType(), + InitE); + } return Result; } diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 1c87ebba90c9d..7eff8241b73c4 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1027,8 +1027,11 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, if (!getLangOpts().Profiles || T.isNull() || T->isDependentType() || (!T->isPointerType() && !T->isReferenceType())) return; - checkInitProfileRefToUninit(Loc, Target->hasAttr(), - T->isReferenceType(), Src, D); + // A null Target is a binding site with no declaration to carry the marker + // (a parameter of a call through a function pointer): always unmarked. + checkInitProfileRefToUninit(Loc, + Target && Target->hasAttr(), + T->isReferenceType(), Src, D); } void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 317f27c77a80c..e5ce3c61ec9ff 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -296,6 +296,37 @@ void test_lambda_arguments() { l(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} } +// A call with no declared callee (through a function pointer) has no +// parameter declaration that could carry [[ref_to_uninit]], so its arguments +// are checked as unmarked targets (paper §7.2: passing uninitialized memory +// needs an appropriately declared callee). That holds even when the pointer +// happens to point at a function whose parameter is marked -- the marker is a +// declaration property, invisible through the pointer (paper §1.3, local +// analysis); suppress at the call if the flow is intended. +void test_fnptr_call_arguments(void (*fp)(int *), void (*fr)(int &)) { + fp(&g_init); // OK + fp(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + fp(rtu); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + fr(g_init); // OK + fr(g_uninit); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + void (*marked)(int *) = take_uninit_ptr; + marked(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { fp(&g_uninit); } // OK: suppressed + (void)rtu; +} + +// Decl-less like the declared-callee argument site: defers on the pattern and +// fires once, at instantiation. +template +void template_fnptr_call_arg(void (*fp)(T *)) { + fp(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_fnptr_call_arg(void (*)(int *)); // expected-note {{in instantiation of function template specialization 'template_fnptr_call_arg' requested here}} + // An init-capture is a binding: a capture cannot carry [[ref_to_uninit]], so // capturing a pointer or reference to uninitialized memory is always the // unmarked-direction violation. From 08d0378e090de135611bf1ff6bfca94f366b68cb Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 15:17:27 -0400 Subject: [PATCH 215/289] Check ref_to_uninit on variadic call arguments --- clang/docs/ProfilesFramework.rst | 13 +++++--- clang/lib/Sema/SemaExpr.cpp | 14 ++++++++ .../safety-profile-init-ref-to-uninit.cpp | 33 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 9ea128f49e846..97bef319aad5c 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1108,7 +1108,11 @@ recognizers symmetric. property invisible through the pointer; suppress at the call if the flow is intended), arguments supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``, which reuses the pre-built expression - rather than re-running copy-initialization), return statements + rather than re-running copy-initialization), variadic (``...``) arguments + (also ``GatherArgumentsForCall``, at the promotion loop -- a ``...`` + parameter cannot carry the marker, so a pointer argument is checked as an + unmarked target, paper §7.2, while a promoted *value* read stays the + read-through chokepoint's), return statements (``Sema::BuildReturnStmt``), and lambda captures -- an init-capture binds like a variable initialization when its variable is created (``Sema::createLambdaInitCaptureVarDecl``), and a plain by-reference capture @@ -1183,10 +1187,9 @@ recognizers symmetric. *contains* ``std::byte`` members. - Known gaps: recognition is purely of the source's syntactic form, so a binding whose underlying operand is unrecognized -- pointer arithmetic, an - integer-to-pointer cast, the *result* of a call through a function pointer - (no ``FunctionDecl`` to read a return marker from), or a variadic (``...``) - argument -- is classified as - *unknown* and diagnosed for neither direction. A ``[[ref_to_uninit]]`` target + integer-to-pointer cast, or the *result* of a call through a function + pointer (no ``FunctionDecl`` to read a return marker from) -- is classified + as *unknown* and diagnosed for neither direction. A ``[[ref_to_uninit]]`` target therefore accepts it (rather than the earlier *false positive*), while an unmarked target also accepts it (a remaining missed diagnostic). The pass-through forms above forward to such an operand without laundering it, so diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index aa289e5ae96d4..fd824129c9470 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6321,6 +6321,20 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, for (Expr *A : Args.slice(ArgIx)) { ExprResult Arg = DefaultVariadicArgumentPromotion(A, CallType, FDecl); Invalid |= Arg.isInvalid(); + // std::init / ref_to_uninit (paper §5): a variadic argument never + // reaches parameter copy-initialization, and a `...` parameter cannot + // carry [[ref_to_uninit]], so a pointer passed through it is checked + // here as an unmarked target (paper §7.2: passing uninitialized + // memory needs an appropriately declared callee). Value reads of the + // promoted argument already funnel through the lvalue-to-rvalue + // chokepoint; the pointer binding is the only direction added here. + if (!Arg.isInvalid() && getLangOpts().Profiles) { + Expr *E = Arg.get(); + if (E->getType()->isPointerType()) + Profiles().checkInitProfileRefToUninit( + E->getExprLoc(), /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, E); + } AllArgs.push_back(Arg.get()); } } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index e5ce3c61ec9ff..dc2c8946a0d81 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -327,6 +327,39 @@ void template_fnptr_call_arg(void (*fp)(T *)) { } template void template_fnptr_call_arg(void (*)(int *)); // expected-note {{in instantiation of function template specialization 'template_fnptr_call_arg' requested here}} +// A variadic (...) argument never reaches parameter copy-initialization, and +// a ... parameter cannot carry [[ref_to_uninit]], so a pointer passed through +// it is checked as an unmarked target (paper §7.2). A *value* passed through +// ... is promoted with an ordinary lvalue-to-rvalue load, so its read is the +// read-through chokepoint's, not this site's. +void vf(int, ...); + +void test_variadic_arguments() { + vf(0, &g_init); // OK + vf(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *rtu [[ref_to_uninit]] = &g_uninit; + vf(0, rtu); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + vf(0, g_uninit_arr); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + vf(0, *rtu); // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + + // Through a variadic function pointer: the named arguments are the + // no-declared-callee site's, the ... arguments this one's -- exactly one + // diagnostic either way. + void (*vfp)(int, ...) = vf; + vfp(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { vf(0, &g_uninit); } // OK: suppressed + (void)rtu; +} + +// Decl-less: defers on the pattern, fires once at instantiation. +template +void template_variadic_arg() { + vf(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_variadic_arg(); // expected-note {{in instantiation of function template specialization 'template_variadic_arg' requested here}} + // An init-capture is a binding: a capture cannot carry [[ref_to_uninit]], so // capturing a pointer or reference to uninitialized memory is always the // unmarked-direction violation. From 0e461e2b455049ea64bb3cb69e672b4455a04dbe Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 15:24:50 -0400 Subject: [PATCH 216/289] Parse profiles attributes leniently when -fprofiles is off Also drop the doubled quotes in err_profiles_expected_rparen. --- clang/docs/ProfilesFramework.rst | 6 ++ .../clang/Basic/DiagnosticParseKinds.td | 2 +- clang/lib/Parse/ParseDeclCXX.cpp | 34 +++++++++++ .../cxx-profiles-framework-disabled.cpp | 58 +++++++++++++++++++ 4 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 clang/test/Parser/cxx-profiles-framework-disabled.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 97bef319aad5c..949ad0b1af31a 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -52,6 +52,12 @@ Without ``-fprofiles``: - ``[[profiles::enforce]]``, ``[[profiles::suppress]]``, and ``[[profiles::require]]`` are diagnosed as ``warn_attribute_ignored`` and have no semantic effect. +- Their argument clauses are **not** checked against the P3589R2 profile + grammar: like any standard attribute the implementation does not act on, + an arbitrary balanced-token argument clause -- or none at all -- is + accepted, so code annotated for a profiles-enabled build compiles cleanly + (modulo the warning) with the feature off. P3589R2's grammar is enforced + only under ``-fprofiles``. - No profile rule check ever fires, even at sites that call ``checkProfileViolation``. diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index 879ebcf6da564..0915bb5f2bb6f 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1896,7 +1896,7 @@ def err_profiles_expected_profile_name : Error< def err_profiles_expected_lparen : Error< "%0 attribute requires an argument clause">; def err_profiles_expected_rparen : Error< - "expected ')' in '%0' attribute">; + "expected ')' in %0 attribute">; def err_profiles_invalid_argument_token : Error< "invalid token in profile argument">; diff --git a/clang/lib/Parse/ParseDeclCXX.cpp b/clang/lib/Parse/ParseDeclCXX.cpp index bf218f5cd37ab..43ccc0132f0d9 100644 --- a/clang/lib/Parse/ParseDeclCXX.cpp +++ b/clang/lib/Parse/ParseDeclCXX.cpp @@ -5293,6 +5293,40 @@ bool Parser::TryParseProfilesAttribute(IdentifierInfo *AttrName, !AttrName->isStr("require")) return false; + // Without -fprofiles the attributes are ignored (their LangOpts gate makes + // Sema emit warn_attribute_ignored), so behave like any ignored standard + // attribute: accept an arbitrary balanced-token argument clause -- or none + // -- without profile grammar checking. The attribute is still created, with + // default-constructed (empty) custom data, so the Sema warning path sees + // it; no consumer reads the custom data while the feature is off. + if (!getLangOpts().Profiles) { + SourceLocation End = AttrNameLoc; + if (Tok.is(tok::l_paren)) { + BalancedDelimiterTracker T(*this, tok::l_paren); + T.consumeOpen(); + T.skipToEnd(); + End = T.getCloseLocation(); + } + if (EndLoc) + *EndLoc = End; + + AttributePool &Pool = Attrs.getPool(); + void *CustomData; + if (AttrName->isStr("enforce")) + CustomData = Pool.make(); + else if (AttrName->isStr("suppress")) + CustomData = Pool.make(); + else + CustomData = Pool.make(); + + ParsedAttr *PA = + Attrs.addNew(AttrName, SourceRange(AttrNameLoc, End), + AttributeScopeInfo(ScopeName, ScopeLoc), nullptr, 0, + ParsedAttr::Form::CXX11()); + PA->setCustomData(CustomData); + return true; + } + if (Tok.isNot(tok::l_paren)) { Diag(AttrNameLoc, diag::err_profiles_expected_lparen) << AttrName; return true; diff --git a/clang/test/Parser/cxx-profiles-framework-disabled.cpp b/clang/test/Parser/cxx-profiles-framework-disabled.cpp new file mode 100644 index 0000000000000..3f12aa90b1d34 --- /dev/null +++ b/clang/test/Parser/cxx-profiles-framework-disabled.cpp @@ -0,0 +1,58 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=disabled -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=enabled -fprofiles -DPROFILES_ENABLED -std=c++23 %s + +// With -fprofiles off, clang does not act on the profiles attributes: they +// are ignored with a warning like any standard attribute the implementation +// does not implement, and their argument clauses are ordinary balanced-token +// sequences, not checked against the P3589R2 profile grammar (which governs +// an implementation that enforces profiles). With -fprofiles on, the grammar +// errors fire as usual; this file asserts both behaviors side by side. + +// Well-formed spellings: ignored when off, accepted when on. +// disabled-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::type)]]; +// disabled-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(vendor::checks(fortify: 3))]]; + +// Malformed argument clauses: accepted as balanced-token soup when off, +// grammar errors when on. +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{expected profile name}} +[[profiles::enforce(+)]]; +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{expected profile name}} +[[profiles::enforce()]]; +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{expected ')' in 'enforce' attribute}} +[[profiles::enforce(a b)]]; + +// A missing argument clause is likewise fine when off. +// disabled-warning@+2 {{'profiles::enforce' attribute ignored}} +// enabled-error@+1 {{'enforce' attribute requires an argument clause}} +[[profiles::enforce]]; +// disabled-warning@+2 {{'profiles::require' attribute ignored}} +// enabled-error@+1 {{'require' attribute requires an argument clause}} +[[profiles::require]]; + +#ifndef PROFILES_ENABLED +// Semicolons, nested parens, and braces are all balanced tokens, fine in an +// ignored attribute's argument clause. +// disabled-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(a; (b), {c})]]; +#endif + +// disabled-warning@+2 {{'profiles::suppress' attribute ignored}} +// enabled-error@+1 {{'justification' argument of 'profiles::suppress' must be a string literal}} +[[profiles::suppress(p, justification: 42)]] int x = 0; +// disabled-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::type)]] int y = 0; + +#ifndef PROFILES_ENABLED +// A genuinely unbalanced argument clause is not a valid attribute in any +// mode: the balanced-token skip still diagnoses the missing ')'. +// disabled-error@+4 {{expected ')'}} +// disabled-note@+3 {{to match this '('}} +// disabled-error@+2 {{expected ']'}} +// disabled-error@+2 {{expected external declaration}} +[[profiles::enforce(]]; +#endif From 327aba623a8a8d2e627f8da02ff153366a4d2030 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Mon, 6 Jul 2026 15:37:35 -0400 Subject: [PATCH 217/289] Check ref_to_uninit on variadic functor and lambda arguments BuildCallToObjectOfClassType promotes variadic arguments in its own loop, bypassing the GatherArgumentsForCall hook. --- clang/docs/ProfilesFramework.rst | 10 ++++--- clang/include/clang/Sema/SemaProfiles.h | 8 +++++ clang/lib/Sema/SemaExpr.cpp | 19 ++++-------- clang/lib/Sema/SemaOverload.cpp | 5 ++++ clang/lib/Sema/SemaProfiles.cpp | 19 ++++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 30 +++++++++++++++++++ 6 files changed, 73 insertions(+), 18 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 949ad0b1af31a..34e19cdcc4753 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1115,10 +1115,12 @@ recognizers symmetric. is intended), arguments supplied by a parameter's default argument (``Sema::GatherArgumentsForCall``, which reuses the pre-built expression rather than re-running copy-initialization), variadic (``...``) arguments - (also ``GatherArgumentsForCall``, at the promotion loop -- a ``...`` - parameter cannot carry the marker, so a pointer argument is checked as an - unmarked target, paper §7.2, while a promoted *value* read stays the - read-through chokepoint's), return statements + (the promotion loops in ``GatherArgumentsForCall`` and + ``Sema::BuildCallToObjectOfClassType``, so variadic functors and variadic + lambdas are covered too -- a ``...`` parameter cannot carry the marker, so + a pointer argument is checked as an unmarked target, paper §7.2, while a + promoted *value* read stays the read-through chokepoint's), return + statements (``Sema::BuildReturnStmt``), and lambda captures -- an init-capture binds like a variable initialization when its variable is created (``Sema::createLambdaInitCaptureVarDecl``), and a plain by-reference capture diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 7362038ec5964..5498983f71380 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -256,6 +256,14 @@ class SemaProfiles : public SemaBase { /// (paper §4.5). void checkInitProfileSubobjectWrite(SourceLocation Loc, const Expr *LHS); + /// std::init / ref_to_uninit (paper §5): a pointer argument passed through + /// a variadic `...` parameter, which cannot carry [[ref_to_uninit]], is + /// checked as an unmarked target. Called with the promoted argument from + /// the C++ variadic promotion loops (Sema::GatherArgumentsForCall and + /// Sema::BuildCallToObjectOfClassType); a non-pointer argument is a no-op + /// (its value read is the lvalue-to-rvalue chokepoint's). + void checkInitProfileVariadicArgument(const Expr *Arg); + /// std::init / ref_to_uninit (paper §4.3): a by-reference lambda capture of /// \p Var binds a reference to its storage, and a capture cannot carry /// [[ref_to_uninit]], so capturing an entity that denotes uninitialized diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index fd824129c9470..86bd43e7f0987 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -6321,20 +6321,11 @@ bool Sema::GatherArgumentsForCall(SourceLocation CallLoc, FunctionDecl *FDecl, for (Expr *A : Args.slice(ArgIx)) { ExprResult Arg = DefaultVariadicArgumentPromotion(A, CallType, FDecl); Invalid |= Arg.isInvalid(); - // std::init / ref_to_uninit (paper §5): a variadic argument never - // reaches parameter copy-initialization, and a `...` parameter cannot - // carry [[ref_to_uninit]], so a pointer passed through it is checked - // here as an unmarked target (paper §7.2: passing uninitialized - // memory needs an appropriately declared callee). Value reads of the - // promoted argument already funnel through the lvalue-to-rvalue - // chokepoint; the pointer binding is the only direction added here. - if (!Arg.isInvalid() && getLangOpts().Profiles) { - Expr *E = Arg.get(); - if (E->getType()->isPointerType()) - Profiles().checkInitProfileRefToUninit( - E->getExprLoc(), /*TargetIsRefToUninit=*/false, - /*IsReference=*/false, E); - } + // std::init / ref_to_uninit (paper §5): a `...` parameter cannot + // carry the marker, so a pointer argument is checked as an unmarked + // target. + if (!Arg.isInvalid()) + Profiles().checkInitProfileVariadicArgument(Arg.get()); AllArgs.push_back(Arg.get()); } } diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp index 97018dbe81057..f37e26e88268e 100644 --- a/clang/lib/Sema/SemaOverload.cpp +++ b/clang/lib/Sema/SemaOverload.cpp @@ -33,6 +33,7 @@ #include "clang/Sema/SemaARM.h" #include "clang/Sema/SemaCUDA.h" #include "clang/Sema/SemaObjC.h" +#include "clang/Sema/SemaProfiles.h" #include "clang/Sema/Template.h" #include "clang/Sema/TemplateDeduction.h" #include "llvm/ADT/DenseSet.h" @@ -16777,6 +16778,10 @@ Sema::BuildCallToObjectOfClassType(Scope *S, Expr *Obj, ExprResult Arg = DefaultVariadicArgumentPromotion( Args[i], VariadicCallType::Method, nullptr); IsError |= Arg.isInvalid(); + // std::init / ref_to_uninit (paper §5): a `...` parameter cannot carry + // the marker, so a pointer argument is checked as an unmarked target. + if (!Arg.isInvalid()) + Profiles().checkInitProfileVariadicArgument(Arg.get()); MethodArgs.push_back(Arg.get()); } } diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 7eff8241b73c4..3bd6900b4f2fc 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1034,6 +1034,25 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, T->isReferenceType(), Src, D); } +void SemaProfiles::checkInitProfileVariadicArgument(const Expr *Arg) { + // std::init / ref_to_uninit (paper §5): a variadic argument never reaches + // parameter copy-initialization, and a `...` parameter cannot carry + // [[ref_to_uninit]], so a pointer passed through it is checked as an + // unmarked target (paper §7.2: passing uninitialized memory needs an + // appropriately declared callee). Value reads of the promoted argument + // already funnel through the lvalue-to-rvalue chokepoint; the pointer + // binding is the only direction added here. Called from the two C++ + // variadic promotion loops -- Sema::GatherArgumentsForCall and + // Sema::BuildCallToObjectOfClassType (functors, variadic lambdas) -- not + // from Sema::DefaultVariadicArgumentPromotion itself, whose other callers + // re-promote already-promoted arguments (the os_log builtin check) or + // promote during ObjC method matching, where a check would double-fire. + if (!getLangOpts().Profiles || !Arg || !Arg->getType()->isPointerType()) + return; + checkInitProfileRefToUninit(Arg->getExprLoc(), /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Arg); +} + void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, const ValueDecl *Var) { if (!getLangOpts().Profiles) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index dc2c8946a0d81..3f66b54efe2a8 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -360,6 +360,36 @@ void template_variadic_arg() { } template void template_variadic_arg(); // expected-note {{in instantiation of function template specialization 'template_variadic_arg' requested here}} +// A call to an object of class type promotes its variadic arguments in its +// own loop (Sema::BuildCallToObjectOfClassType), distinct from +// GatherArgumentsForCall's; both are hooked, so variadic functors and +// variadic lambdas are covered too. +struct VariadicFunctor { + void operator()(int, ...); +}; + +void test_variadic_functor_arguments() { + VariadicFunctor f; + f(0, &g_init); // OK + f(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + auto l = [](int, ...) {}; + l(0, &g_init); // OK + l(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A surrogate call converts to a function pointer and calls through it, so +// its named arguments are the no-declared-callee site's. +struct Surrogate { + using FP = void (*)(int *); + operator FP(); +}; + +void test_surrogate_call_arguments() { + Surrogate s; + s(&g_init); // OK + s(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + // An init-capture is a binding: a capture cannot carry [[ref_to_uninit]], so // capturing a pointer or reference to uninitialized memory is always the // unmarked-direction violation. From 2be3a1ce5a1a69e772e821c117dd8351c8b548d0 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 11:30:34 -0400 Subject: [PATCH 218/289] Record the suppressed construct's begin location on the suppress stack --- clang/include/clang/Sema/SemaProfiles.h | 10 ++++++++-- clang/lib/Sema/SemaProfiles.cpp | 19 +++++++++++++------ clang/lib/Sema/TreeTransform.h | 4 ++-- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 5498983f71380..2f47098fdd30d 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -50,6 +50,11 @@ class SemaProfiles : public SemaBase { struct ProfileSuppressEntry { StringRef ProfileName; StringRef RuleName; + /// Begin location of the construct the suppression appertains to (the + /// declaration or statement, not the attribute). The entry's dominion + /// starts here; its end is bounded by the ProfileSuppressScope's + /// lifetime (P3589R2 s2.4p3). + SourceLocation Begin; }; SmallVector ProfileSuppressStack; @@ -311,14 +316,15 @@ class SemaProfiles : public SemaBase { Sema &S; unsigned Count = 0; - void push(StringRef ProfileName, StringRef RuleName); + void push(StringRef ProfileName, StringRef RuleName, SourceLocation Begin); void addFromDecl(const Decl *D); public: ProfileSuppressScope(Sema &S, const ParsedAttributesView &Attrs); ProfileSuppressScope(Sema &S, const Decl *D, bool WalkLexicalParents = false); - ProfileSuppressScope(Sema &S, ArrayRef Attrs); + ProfileSuppressScope(Sema &S, ArrayRef Attrs, + SourceLocation Begin); ~ProfileSuppressScope(); }; }; diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 3bd6900b4f2fc..a497ffaa26c3e 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -377,8 +377,9 @@ bool SemaProfiles::checkProfileViolation(StringRef ProfileName, } void SemaProfiles::ProfileSuppressScope::push(StringRef ProfileName, - StringRef RuleName) { - S.Profiles().ProfileSuppressStack.push_back({ProfileName, RuleName}); + StringRef RuleName, + SourceLocation Begin) { + S.Profiles().ProfileSuppressStack.push_back({ProfileName, RuleName, Begin}); ++Count; } @@ -391,14 +392,19 @@ SemaProfiles::ProfileSuppressScope::ProfileSuppressScope( if (AL.getKind() != ParsedAttr::AT_ProfilesSuppress) continue; const auto &Args = AL.getProfileSuppressArgs(); + // These are the prefix attributes of a statement or declaration about to + // be parsed, so the attribute's own location is the construct's begin. if (!Args.Name.empty()) - push(Args.Name, Args.Rule); + push(Args.Name, Args.Rule, AL.getLoc()); } } void SemaProfiles::ProfileSuppressScope::addFromDecl(const Decl *D) { + SourceLocation Begin = D->getBeginLoc(); + if (Begin.isInvalid()) + Begin = D->getLocation(); for (const auto *A : D->specific_attrs()) - push(A->getProfileName(), A->getRule()); + push(A->getProfileName(), A->getRule(), Begin); } SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, @@ -416,13 +422,14 @@ SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, } SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, - ArrayRef Attrs) + ArrayRef Attrs, + SourceLocation Begin) : S(S) { if (!S.getLangOpts().Profiles) return; for (const auto *A : Attrs) if (const auto *PSA = dyn_cast(A)) - push(PSA->getProfileName(), PSA->getRule()); + push(PSA->getProfileName(), PSA->getRule(), Begin); } SemaProfiles::ProfileSuppressScope::~ProfileSuppressScope() { diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h index bb49f93714de4..552d15cc6d830 100644 --- a/clang/lib/Sema/TreeTransform.h +++ b/clang/lib/Sema/TreeTransform.h @@ -8312,8 +8312,8 @@ template StmtResult TreeTransform::TransformAttributedStmt(AttributedStmt *S, StmtDiscardKind SDK) { - SemaProfiles::ProfileSuppressScope ProfileSuppressGuard(getSema(), - S->getAttrs()); + SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( + getSema(), S->getAttrs(), S->getBeginLoc()); StmtResult SubStmt = getDerived().TransformStmt(S->getSubStmt(), SDK); From 07b5b84aee1474f545375975bccd8d7159f27e36 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 11:35:37 -0400 Subject: [PATCH 219/289] Match parse-time suppressions by dominion begin location --- clang/include/clang/Sema/SemaProfiles.h | 10 ++++++++-- clang/lib/Sema/SemaProfiles.cpp | 25 ++++++++++++++++++++----- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 2f47098fdd30d..6dff1f96442b6 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -97,8 +97,14 @@ class SemaProfiles : public SemaBase { ProfilesSuppressAttr *makeImplicitProfilesSuppressAttr(StringRef ProfileName, StringRef RuleName); - bool isProfileSuppressed(StringRef ProfileName, - StringRef RuleName = "") const; + /// True if a live parse-time suppress entry for \p ProfileName / + /// \p RuleName covers \p Loc. An entry matches only tokens at or after its + /// construct's begin location (its dominion, P3589R2 s2.4p3); the owning + /// ProfileSuppressScope's lifetime bounds the dominion's end. Tokens from + /// outside the construct -- e.g. a template pattern instantiated + /// synchronously while the scope is live -- are not suppressed. + bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, + SourceLocation Loc) const; bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, const Decl *D) const; bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index a497ffaa26c3e..579e1efa17d25 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -21,6 +21,7 @@ #include "clang/AST/ParentMap.h" #include "clang/Analysis/AnalysisDeclContext.h" #include "clang/Basic/Module.h" +#include "clang/Basic/SourceManager.h" #include "clang/Sema/Attr.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/Sema.h" @@ -255,11 +256,25 @@ static bool profileSuppressMatches(StringRef EntryProfile, StringRef EntryRule, } bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, - StringRef RuleName) const { - for (const auto &E : ProfileSuppressStack) - if (profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, - RuleName)) + StringRef RuleName, + SourceLocation Loc) const { + const SourceManager &SM = getASTContext().getSourceManager(); + for (const auto &E : ProfileSuppressStack) { + if (!profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, + RuleName)) + continue; + // The entry's dominion starts at its construct's begin location, so a + // violation at an earlier token -- e.g. in a template pattern + // instantiated synchronously while the scope is live -- is outside it + // (P3589R2 s2.4p3). Fail open on an invalid location on either side + // (isBeforeInTranslationUnit rejects invalid locations), preserving + // plain-liveness behavior for synthesized code. Locations are compared + // in raw TU token order: expansion-loc normalization would collapse all + // tokens of one macro expansion onto the invocation and over-suppress. + if (Loc.isInvalid() || E.Begin.isInvalid() || + !SM.isBeforeInTranslationUnit(Loc, E.Begin)) return true; + } return false; } @@ -324,7 +339,7 @@ bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, // based dominion, P3589R2 s2.4p3). Their decl-aware walk already covers a // suppression on the declaration or a lexical parent. if ((!InProfileFinalizationCheck && - isProfileSuppressed(ProfileName, RuleName)) || + isProfileSuppressed(ProfileName, RuleName, Loc)) || isProfileSuppressed(ProfileName, RuleName, D)) return false; // P3589R2 Section 1.1: "its static semantic effects are as-if applied only From 1a25ea097dfac79cc925cb1bef4537e1f3c442b1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 11:40:55 -0400 Subject: [PATCH 220/289] Add regression tests for suppress leaking into instantiations --- .../test/SemaCXX/safety-profile-init-decl.cpp | 23 +++++ .../safety-profile-init-field-marker.cpp | 11 +++ .../test/SemaCXX/safety-profile-type-cast.cpp | 90 +++++++++++++++++++ 3 files changed, 124 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-decl.cpp b/clang/test/SemaCXX/safety-profile-init-decl.cpp index 440daf15f8356..b56e1b22515f4 100644 --- a/clang/test/SemaCXX/safety-profile-init-decl.cpp +++ b/clang/test/SemaCXX/safety-profile-init-decl.cpp @@ -165,3 +165,26 @@ void template_ptr_marker() { } template void template_ptr_marker(); // expected-note {{in instantiation of function template specialization 'template_ptr_marker' requested here}} // expected-error@#template-ptr-marker {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// A [[profiles::suppress(std::init)]] live at the point of instantiation +// covers the trigger's tokens, not the pattern's (P3589R2 s2.4p3): rules +// inside a synchronously instantiated pattern must still fire. +template +auto instantiation_leak_use() { + T t; // expected-error {{variable 't' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)t; + return 0; +} +void instantiation_leak_trigger() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] int leaked = instantiation_leak_use(); // expected-note {{in instantiation of function template specialization 'instantiation_leak_use' requested here}} + (void)leaked; +} + +// A declarator-position suppress covers a diagnostic located at the declared +// name: entries anchor to the construct's begin, not the attribute's. +void declarator_position_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + int c [[profiles::suppress(std::init, rule: "uninit_decl")]]; + (void)c; +} diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 8ac28808d210d..4a82b5e3e3207 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -128,3 +128,14 @@ void test_invalid_subjects(int p [[uninit]]) { // expected-error {{'uninit' attr // no-profiles-error {{'uninit' attribute cannot be applied to a structured binding}} (void)p; (void)lr; (void)a; (void)b; } + +// The marker re-check on the instantiated field is not suppressed by a +// [[profiles::suppress(std::init)]] live at the point of instantiation -- +// the pattern's tokens are outside that dominion (P3589R2 s2.4p3). +template +struct LeakMarker { + T p [[uninit]]; // #leak-marker-field +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] LeakMarker leak_marker_use{nullptr}; // expected-note {{in instantiation of template class 'LeakMarker' requested here}} +// expected-error@#leak-marker-field {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 6e4ab740c78b4..830b4ddccc780 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -712,3 +712,93 @@ struct DeepOuter { }; }; }; + +// A [[profiles::suppress]] live at the point of instantiation covers the +// trigger's tokens, not the pattern's (P3589R2 s2.4p3, token-based dominion): +// parse-time checks in synchronously instantiated code must not be +// suppressed by the caller's scope. + +int instantiation_leak_target = 0; + +// Function-template body instantiated from a suppressed initializer. +template +auto instantiation_leak_fn() { + return reinterpret_cast(&instantiation_leak_target); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] auto *leak_fn_use = instantiation_leak_fn(); // expected-note {{in instantiation of function template specialization 'instantiation_leak_fn' requested here}} + +// NSDMI of an unrelated class template instantiated from a suppressed +// declaration. +template +struct LeakNSDMI { // #LeakNSDMI + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#LeakNSDMI {{in instantiation of default member initializer 'LeakNSDMI::p' requested here}} +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] LeakNSDMI leak_nsdmi_use; // expected-note {{in evaluation of exception specification for 'LeakNSDMI::LeakNSDMI' needed here}} + +// Variable-template initializer. +template +auto *leak_vt = reinterpret_cast(&instantiation_leak_target); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *leak_vt_use = leak_vt; // expected-note {{in instantiation of variable template specialization 'leak_vt' requested here}} + +// Default argument instantiated at a suppressed call site. +template +int *leak_def(T *q = reinterpret_cast(&instantiation_leak_target)) { return q; } // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *leak_def_use = leak_def(); // expected-note {{in instantiation of default function argument expression for 'leak_def' required here}} + +// A closure created during the instantiation must not absorb the caller's +// suppress either. +template +auto leak_lambda() { + auto l = [](auto x) { return reinterpret_cast(x); }; // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} + return l(0L); // expected-note {{in instantiation of function template specialization 'leak_lambda()::(lambda)::operator()' requested here}} +} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] auto *leak_lambda_use = leak_lambda(); // expected-note {{in instantiation of function template specialization 'leak_lambda' requested here}} + +// A suppressed *statement* inside a pattern does not reach a lexically +// unrelated pattern instantiated under it: GapLeaked's tokens precede the +// suppressed block, so its NSDMI check still fires. +template +struct GapLeaked { // #GapLeaked + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#GapLeaked {{in instantiation of default member initializer 'GapLeaked::p' requested here}} +}; +template +void gap_user() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + GapLeaked v; // expected-note {{in evaluation of exception specification for 'GapLeaked::GapLeaked' needed here}} + (void)v; + } +} +template void gap_user(); // expected-note {{in instantiation of function template specialization 'gap_user' requested here}} + +// Contrast: a local class *defined inside* the suppressed block is within the +// dominion, so its NSDMI stays suppressed when instantiated under it. +template +void local_class_in_suppressed_block() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + struct Local { T *p = reinterpret_cast(0); }; + Local v; + (void)v; + } +} +template void local_class_in_suppressed_block(); + +// Dominion matching compares raw TU token order, so macro-emitted code +// behaves by its expansion position: a violation spelled in a macro argument +// of the suppressed declaration is within the dominion; a pattern's +// macro-emitted violation stays outside it. +#define EMIT_CAST(ty, x) reinterpret_cast(x) +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *macro_arg_suppressed = EMIT_CAST(int, &instantiation_leak_target); +template +auto leak_macro_fn() { return EMIT_CAST(T, &instantiation_leak_target); } // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] int *leak_macro_use = leak_macro_fn(); // expected-note {{in instantiation of function template specialization 'leak_macro_fn' requested here}} From a476a15101e755821814314590aebb82a92a0848 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 11:44:44 -0400 Subject: [PATCH 221/289] Fold the finalization suppress guard into dominion matching --- clang/include/clang/Sema/SemaProfiles.h | 8 ------ clang/lib/Sema/SemaProfiles.cpp | 27 +++++++++---------- .../SemaCXX/safety-profile-class-final.cpp | 20 ++++++++++++-- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 6dff1f96442b6..305e8e3abb37c 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -58,14 +58,6 @@ class SemaProfiles : public SemaBase { }; SmallVector ProfileSuppressStack; - /// True while a class/constructor finalization profile callback runs. - /// Finalization can fire as a side effect of instantiating an unrelated - /// entity whose ProfileSuppressScope is still on ProfileSuppressStack, so - /// during finalization that transient stack is ignored and suppression is - /// resolved only from the finalized declaration and its lexical parents - /// (token-based dominion, P3589R2 s2.4p3). - bool InProfileFinalizationCheck = false; - bool isProfileEnforced(StringRef ProfileName) const; /// True if any entry of \p Entries names an enforced profile. \p Entries is diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 579e1efa17d25..57ea8d8886f02 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -25,7 +25,6 @@ #include "clang/Sema/Attr.h" #include "clang/Sema/ParsedAttr.h" #include "clang/Sema/Sema.h" -#include "llvm/Support/SaveAndRestore.h" using namespace clang; @@ -333,13 +332,13 @@ bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, // not depend on a parse-time scope still being active, so finalization checks // that run after the parse scope is torn down still respect suppression. // - // Finalization callbacks skip the parse-time stack: they can fire while an - // unrelated entity's instantiation ProfileSuppressScope is still active, and - // that scope does not lexically enclose the finalized declaration (token- - // based dominion, P3589R2 s2.4p3). Their decl-aware walk already covers a - // suppression on the declaration or a lexical parent. - if ((!InProfileFinalizationCheck && - isProfileSuppressed(ProfileName, RuleName, Loc)) || + // The stack consult is dominion-checked against Loc: a check that fires + // while an unrelated construct's ProfileSuppressScope is live -- a + // synchronously instantiated pattern, or a class finalized as a side effect + // of one -- matches only entries whose construct's tokens cover Loc + // (P3589R2 s2.4p3), so no explicit finalization or instantiation guard is + // needed here. + if (isProfileSuppressed(ProfileName, RuleName, Loc) || isProfileSuppressed(ProfileName, RuleName, D)) return false; // P3589R2 Section 1.1: "its static semantic effects are as-if applied only @@ -1318,12 +1317,12 @@ void dispatchFinalizationProfiles(Sema &S, Node *D, if (!S.Profiles().anyProfileEnforced(Table)) return; // Finalization can run nested in an unrelated instantiation whose - // [[profiles::suppress]] scope is still on the parse-time stack; the - // callbacks - // must resolve suppression only from D and its lexical parents, not that - // transient stack (P3589R2 s2.4p3). - llvm::SaveAndRestore InFinalization( - S.Profiles().InProfileFinalizationCheck, true); + // [[profiles::suppress]] scope is still on the parse-time stack. No guard + // is needed: the stack consult is dominion-checked, so such an entry + // matches only if its construct's tokens cover the finalized declaration. + // FIXME: a pattern forward-declared before but *defined after* the + // suppressed construct is wrongly matched while the scope is live (scope + // liveness over-approximates the dominion's end). for (const auto &E : Table) if (S.Profiles().isProfileEnforced(E.Name)) E.Callback(S, D); diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp index 5b1c633fefe8a..5c05be97ebd2b 100644 --- a/clang/test/SemaCXX/safety-profile-class-final.cpp +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -154,8 +154,9 @@ void test_local_class() { } // Function-level suppress silences a local class defined in its body via -// the parse-time ProfileSuppressStack (not via the lexical-parent walk; -// the walk goes through the *Decl* chain, not statement context). +// the decl-aware walk (the local class's lexical DeclContext is the +// attributed function) and, equivalently, via the dominion-checked +// parse-time stack -- the class's tokens are within the function's. // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::class_final)]] void test_local_class_suppressed_via_fn() { @@ -164,6 +165,21 @@ void test_local_class_suppressed_via_fn() { (void)x; } +// A *statement-level* suppress reaches a local class defined inside its +// dominion too: that suppression exists only on the parse-time stack (no +// Decl carries the attribute), and the class's tokens are covered. +void test_local_class_suppressed_via_stmt() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::class_final)]] { + struct LocalInSuppressedStmt { int m; }; + LocalInSuppressedStmt x; + (void)x; + } + struct LocalAfterStmt { int m; }; // expected-error {{test profile fired on completion of class 'LocalAfterStmt' under profile 'test::class_final'}} + LocalAfterStmt y; + (void)y; +} + // Local classes inside a lambda *body* still fire. Only the closure type // itself is filtered by isLambda(); user-defined classes nested inside the // closure's call operator are not lambdas. From 2c114029ea1b0aa7265895b73e6cd03ef46564c5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 11:46:26 -0400 Subject: [PATCH 222/289] Document dominion matching of the parse-time suppress stack --- clang/docs/ProfilesFramework.rst | 78 +++++++++++++++++++------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 34e19cdcc4753..b84e14cd099e4 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -325,14 +325,13 @@ This pattern needs two pieces, both colocated with the dispatcher. overload, which walks the declaration and its lexical parents for a matching ``[[profiles::suppress]]``, so suppression on the class or any enclosing lexical ``Decl`` works without the dispatcher establishing a - suppress scope. For the duration of the callbacks the dispatcher sets - ``Sema::InProfileFinalizationCheck``, which makes - ``shouldEmitProfileViolation`` ignore the transient parse-time - ``ProfileSuppressStack``: finalization can run as a side effect of an - *unrelated* template instantiation whose ``[[profiles::suppress]]`` scope is - still on that stack, and that scope does not lexically enclose the finalized - class (see :ref:`profiles-token-dominion`). Suppression is therefore - resolved only from the declaration and its lexical parents. + suppress scope. Finalization can run as a side effect of an *unrelated* + template instantiation whose ``[[profiles::suppress]]`` scope is still on + the transient parse-time ``ProfileSuppressStack``; because stack entries + are matched against the violation's location (see + :ref:`profiles-token-dominion`), such a scope -- whose construct's tokens + do not cover the finalized class -- does not suppress the callback's + diagnostics. 2. **Emit diagnostics from the callback via** ``SemaProfiles::shouldEmitProfileViolation``. Each callback decides where on @@ -401,16 +400,15 @@ table ``ConstructorFinalizationProfiles`` of the same passes the ``CXXConstructorDecl`` to the decl-aware ``shouldEmitProfileViolation`` overload; that overload walks the declaration and its lexical parents, so ``[[profiles::suppress]]`` on the constructor, -the class, or an enclosing lexical ``Decl`` works. As for pattern 3, the -shared ``dispatchFinalizationProfiles`` dispatcher runs these callbacks under -the ``Sema::InProfileFinalizationCheck`` guard, so they resolve suppression -only from that decl-aware walk and ignore the transient parse-time -``ProfileSuppressStack`` (see :ref:`profiles-token-dominion`). A constructor -body is normally instantiated lazily -- outside any unrelated suppress scope -- -so here the guard is defensive; the scenario it actually prevents arises in -pattern 3, where a class completes synchronously inside an enclosing -instantiation. A callback that should only apply to user-written constructors -checks ``Ctor->isUserProvided()``. +the class, or an enclosing lexical ``Decl`` works. As for pattern 3, a +transient parse-time suppress scope belonging to an unrelated construct does +not reach these callbacks, because stack entries only match violations whose +tokens their construct covers (see :ref:`profiles-token-dominion`). A +constructor body is normally instantiated lazily -- outside any unrelated +suppress scope -- so for pattern 4 this matters rarely; the scenario it +actually handles arises in pattern 3, where a class completes synchronously +inside an enclosing instantiation. A callback that should only apply to +user-written constructors checks ``Ctor->isUserProvided()``. .. _profiles-token-dominion: @@ -433,17 +431,23 @@ marker. This applies identically to the parse-time suppression stack and the post-parse Stmt-tree walker described in pattern 2. -Token-based dominion is also why the class- and constructor-finalization -dispatch (patterns 3 and 4) deliberately ignores the parse-time -``ProfileSuppressStack``. A finalization callback can run while an -*unrelated* entity is being instantiated -- for example, completing a class -template used inside a ``[[profiles::suppress(P)]]``-annotated function -template that is itself being instantiated. That instantiation's suppress -scope is on the stack, but its tokens do not enclose the finalized class, so -honoring it would suppress a violation outside its dominion. Finalization -therefore resolves suppression only from the finalized declaration and its -lexical parents (via the ``Sema::InProfileFinalizationCheck`` guard on -``dispatchFinalizationProfiles``). +The parse-time stack enforces the dominion positionally: each entry records +the begin location of the construct its attribute appertains to, a violation +matches an entry only if its location is at or after that begin (in +translation-unit token order), and the entry's ``ProfileSuppressScope`` +lifetime bounds the dominion's end. This is what keeps a live suppress scope +from leaking into code whose tokens it does not cover. A check can fire +under an *unrelated* construct's scope in two ways: a template pattern +instantiated synchronously while the scope is live (the pattern's tokens -- +which instantiated code retains as its source locations -- precede the +suppressed construct), and a class or constructor finalized as a side effect +of such an instantiation (patterns 3 and 4). In both cases the entry's +begin location is after the violation's, so the suppression correctly does +not apply; conversely, a local class or lambda *defined inside* the +suppressed construct is covered, whichever path re-enters it. One known +over-approximation: scope liveness bounds the dominion's end, so a pattern +forward-declared before but *defined after* the suppressed construct is +wrongly treated as covered while the scope is live. .. _profiles-internals: @@ -460,9 +464,11 @@ benefit from understanding, even though they do not interact with them directly. An RAII guard that pushes suppression entries onto ``Sema::ProfileSuppressStack`` and pops them on destruction. It is used by the parser and template instantiation machinery to make ``[[profiles::suppress]]`` attributes active -during the appropriate region. ``checkProfileViolation`` consults -``ProfileSuppressStack`` directly, so profile implementers never need to create -``ProfileSuppressScope`` objects. +during the appropriate region. Each entry records the begin location of the +construct its attribute appertains to, and matches only violations located at +or after it (see :ref:`profiles-token-dominion`). ``checkProfileViolation`` +consults ``ProfileSuppressStack`` directly, so profile implementers never need +to create ``ProfileSuppressScope`` objects. Stmt-Tree Suppression Walker ---------------------------- @@ -489,6 +495,14 @@ applies to instantiated code. This is done via ``ProfileSuppressScope`` with - ``TreeTransform.h`` -- ``TransformAttributedStmt`` (suppress on statements) and ``TransformDeclStmt`` (suppress on declarations within a ``DeclStmt``). +The reverse direction is *not* propagated: a suppress scope live at the +*point of instantiation* (for example on the declaration whose initializer +triggers it) covers the trigger's tokens, not the pattern's. Instantiated +code retains the pattern's source locations, so the dominion check on stack +entries (see :ref:`profiles-token-dominion`) keeps such a scope from +suppressing checks that fire inside a synchronously instantiated body, NSDMI, +default argument, or instantiated marker re-check. + Module Enforcement ------------------ From bb1b14e6d41d13409764e7aa74752266d811b5c8 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 12:43:16 -0400 Subject: [PATCH 223/289] Record the suppressed construct's end location on the suppress stack --- clang/include/clang/Sema/SemaProfiles.h | 17 ++++++-- clang/lib/Sema/SemaProfiles.cpp | 54 +++++++++++++++++++++---- clang/lib/Sema/TreeTransform.h | 2 +- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 305e8e3abb37c..fdcc00b9ad8c5 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -52,9 +52,17 @@ class SemaProfiles : public SemaBase { StringRef RuleName; /// Begin location of the construct the suppression appertains to (the /// declaration or statement, not the attribute). The entry's dominion - /// starts here; its end is bounded by the ProfileSuppressScope's - /// lifetime (P3589R2 s2.4p3). + /// starts here (P3589R2 s2.4p3). SourceLocation Begin; + /// End location of the construct, recorded only when the construct was + /// fully parsed at push time; invalid otherwise, leaving the dominion's + /// end bounded by the ProfileSuppressScope's lifetime. That fallback is + /// exact for a construct still being parsed -- its later tokens do not + /// exist yet, and instantiation of a not-yet-defined template is + /// deferred past the scope's death -- while a completed construct's + /// recorded end keeps a live scope from covering a pattern first + /// declared after it. + SourceLocation End; }; SmallVector ProfileSuppressStack; @@ -314,7 +322,8 @@ class SemaProfiles : public SemaBase { Sema &S; unsigned Count = 0; - void push(StringRef ProfileName, StringRef RuleName, SourceLocation Begin); + void push(StringRef ProfileName, StringRef RuleName, SourceLocation Begin, + SourceLocation End); void addFromDecl(const Decl *D); public: @@ -322,7 +331,7 @@ class SemaProfiles : public SemaBase { ProfileSuppressScope(Sema &S, const Decl *D, bool WalkLexicalParents = false); ProfileSuppressScope(Sema &S, ArrayRef Attrs, - SourceLocation Begin); + SourceLocation Begin, SourceLocation End); ~ProfileSuppressScope(); }; }; diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 57ea8d8886f02..4e42f4edbf6d7 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -392,8 +392,10 @@ bool SemaProfiles::checkProfileViolation(StringRef ProfileName, void SemaProfiles::ProfileSuppressScope::push(StringRef ProfileName, StringRef RuleName, - SourceLocation Begin) { - S.Profiles().ProfileSuppressStack.push_back({ProfileName, RuleName, Begin}); + SourceLocation Begin, + SourceLocation End) { + S.Profiles().ProfileSuppressStack.push_back( + {ProfileName, RuleName, Begin, End}); ++Count; } @@ -407,18 +409,55 @@ SemaProfiles::ProfileSuppressScope::ProfileSuppressScope( continue; const auto &Args = AL.getProfileSuppressArgs(); // These are the prefix attributes of a statement or declaration about to - // be parsed, so the attribute's own location is the construct's begin. + // be parsed, so the attribute's own location is the construct's begin and + // no end is known yet (the scope's lifetime bounds it). if (!Args.Name.empty()) - push(Args.Name, Args.Rule, AL.getLoc()); + push(Args.Name, Args.Rule, AL.getLoc(), SourceLocation()); } } +/// The end location of \p D's construct if it is fully parsed, invalid +/// otherwise. A partially parsed construct's end location is usually *valid +/// but early* -- a mid-parse class collapses to its name token (the brace +/// range is set only by ActOnTagFinishDefinition, after even the late-parsed +/// members), a body-pending function ends at its declarator, an +/// uninitialized variable at its declarator -- so each arm gates on the +/// marker that the construct's real end has been seen. Returning invalid +/// falls back to scope-lifetime bounding, which is exact mid-parse. +static SourceLocation getCompletedConstructEnd(const Decl *D) { + if (const auto *TD = dyn_cast(D)) + return TD->getBraceRange().getEnd(); + if (const auto *FD = dyn_cast(D)) { + // isLateTemplateParsed makes doesThisDeclarationHaveABody true while the + // body is merely token-cached and the range still ends at the declarator. + if (FD->doesThisDeclarationHaveABody() && !FD->isLateTemplateParsed()) + return FD->getSourceRange().getEnd(); + return SourceLocation(); + } + if (const auto *VD = dyn_cast(D)) { + if (VD->hasInit()) + return VD->getSourceRange().getEnd(); + return SourceLocation(); + } + if (const auto *FD = dyn_cast(D)) { + // The in-class initializer expression is null while its late parse is + // still pending. + if (FD->hasNonNullInClassInitializer()) + return FD->getInClassInitializer()->getEndLoc(); + return SourceLocation(); + } + if (const auto *ND = dyn_cast(D)) + return ND->getRBraceLoc(); + return SourceLocation(); +} + void SemaProfiles::ProfileSuppressScope::addFromDecl(const Decl *D) { SourceLocation Begin = D->getBeginLoc(); if (Begin.isInvalid()) Begin = D->getLocation(); + SourceLocation End = getCompletedConstructEnd(D); for (const auto *A : D->specific_attrs()) - push(A->getProfileName(), A->getRule(), Begin); + push(A->getProfileName(), A->getRule(), Begin, End); } SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, @@ -437,13 +476,14 @@ SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, const Decl *D, SemaProfiles::ProfileSuppressScope::ProfileSuppressScope(Sema &S, ArrayRef Attrs, - SourceLocation Begin) + SourceLocation Begin, + SourceLocation End) : S(S) { if (!S.getLangOpts().Profiles) return; for (const auto *A : Attrs) if (const auto *PSA = dyn_cast(A)) - push(PSA->getProfileName(), PSA->getRule(), Begin); + push(PSA->getProfileName(), PSA->getRule(), Begin, End); } SemaProfiles::ProfileSuppressScope::~ProfileSuppressScope() { diff --git a/clang/lib/Sema/TreeTransform.h b/clang/lib/Sema/TreeTransform.h index 552d15cc6d830..daec22e2a3b80 100644 --- a/clang/lib/Sema/TreeTransform.h +++ b/clang/lib/Sema/TreeTransform.h @@ -8313,7 +8313,7 @@ StmtResult TreeTransform::TransformAttributedStmt(AttributedStmt *S, StmtDiscardKind SDK) { SemaProfiles::ProfileSuppressScope ProfileSuppressGuard( - getSema(), S->getAttrs(), S->getBeginLoc()); + getSema(), S->getAttrs(), S->getBeginLoc(), S->getEndLoc()); StmtResult SubStmt = getDerived().TransformStmt(S->getSubStmt(), SDK); From 0ef203f3e479686b487615f621369a0244074893 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 12:47:27 -0400 Subject: [PATCH 224/289] Bound dominion matching by the suppressed construct's end location --- clang/include/clang/Sema/SemaProfiles.h | 12 +++++++----- clang/lib/Sema/SemaProfiles.cpp | 25 +++++++++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index fdcc00b9ad8c5..6685daf883a2b 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -98,11 +98,13 @@ class SemaProfiles : public SemaBase { StringRef RuleName); /// True if a live parse-time suppress entry for \p ProfileName / - /// \p RuleName covers \p Loc. An entry matches only tokens at or after its - /// construct's begin location (its dominion, P3589R2 s2.4p3); the owning - /// ProfileSuppressScope's lifetime bounds the dominion's end. Tokens from - /// outside the construct -- e.g. a template pattern instantiated - /// synchronously while the scope is live -- are not suppressed. + /// \p RuleName covers \p Loc. An entry matches only tokens within its + /// construct's recorded range (its dominion, P3589R2 s2.4p3); when the + /// construct was still being parsed at push time no end is recorded and + /// the owning ProfileSuppressScope's lifetime bounds the dominion's end. + /// Tokens from outside the construct -- e.g. a template pattern + /// instantiated synchronously while the scope is live, wherever it is + /// declared -- are not suppressed. bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, SourceLocation Loc) const; bool isProfileSuppressed(StringRef ProfileName, StringRef RuleName, diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 4e42f4edbf6d7..c69c78d65015a 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -262,16 +262,22 @@ bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, if (!profileSuppressMatches(E.ProfileName, E.RuleName, ProfileName, RuleName)) continue; - // The entry's dominion starts at its construct's begin location, so a - // violation at an earlier token -- e.g. in a template pattern - // instantiated synchronously while the scope is live -- is outside it - // (P3589R2 s2.4p3). Fail open on an invalid location on either side + // The entry's dominion is its construct's token range (P3589R2 s2.4p3): + // a violation before the recorded begin -- e.g. in a template pattern + // instantiated synchronously while the scope is live -- is outside it, + // as is one past the recorded end -- e.g. in a pattern first declared + // *after* the suppressed construct. The end is recorded only for a + // construct fully parsed at push time; when it is invalid the scope's + // lifetime bounds the dominion, which is exact mid-parse (later tokens + // are unparsed and instantiation of undefined templates is deferred). + // Fail open on an invalid location on either side // (isBeforeInTranslationUnit rejects invalid locations), preserving // plain-liveness behavior for synthesized code. Locations are compared // in raw TU token order: expansion-loc normalization would collapse all // tokens of one macro expansion onto the invocation and over-suppress. if (Loc.isInvalid() || E.Begin.isInvalid() || - !SM.isBeforeInTranslationUnit(Loc, E.Begin)) + (!SM.isBeforeInTranslationUnit(Loc, E.Begin) && + (E.End.isInvalid() || !SM.isBeforeInTranslationUnit(E.End, Loc)))) return true; } return false; @@ -1358,11 +1364,10 @@ void dispatchFinalizationProfiles(Sema &S, Node *D, return; // Finalization can run nested in an unrelated instantiation whose // [[profiles::suppress]] scope is still on the parse-time stack. No guard - // is needed: the stack consult is dominion-checked, so such an entry - // matches only if its construct's tokens cover the finalized declaration. - // FIXME: a pattern forward-declared before but *defined after* the - // suppressed construct is wrongly matched while the scope is live (scope - // liveness over-approximates the dominion's end). + // is needed: the stack consult is dominion-checked against the entry's + // recorded construct range, so such an entry matches only if its + // construct's tokens cover the finalized declaration -- including when the + // finalized pattern is first declared after the suppressed construct. for (const auto &E : Table) if (S.Profiles().isProfileEnforced(E.Name)) E.Callback(S, D); From b03b892073361feedc5b5e38e481413ad1da1252 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 12:50:13 -0400 Subject: [PATCH 225/289] Add regression tests for suppress leaking past a construct's end --- .../SemaCXX/safety-profile-class-final.cpp | 27 +++++++ .../test/SemaCXX/safety-profile-type-cast.cpp | 70 +++++++++++++++++++ 2 files changed, 97 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-class-final.cpp b/clang/test/SemaCXX/safety-profile-class-final.cpp index 5c05be97ebd2b..18c02b88b989e 100644 --- a/clang/test/SemaCXX/safety-profile-class-final.cpp +++ b/clang/test/SemaCXX/safety-profile-class-final.cpp @@ -266,6 +266,33 @@ template } template void nested_leak_user(); // expected-note {{in instantiation of function template specialization 'nested_leak_user' requested here}} +// The dominion's end is bounded by the construct's recorded end: a pattern +// *first declared after* the suppressed construct (reached here via a +// dependent name) is outside its dominion even while the scope is live. +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void fwd_final_user() { + typename T::type v; // expected-note {{in instantiation of template class 'FwdFinalVictim' requested here}} + (void)v; +} +template struct FwdFinalVictim { U m; }; // expected-error {{test profile fired on completion of class 'FwdFinalVictim' under profile 'test::class_final'}} +struct FwdFinalHolder { using type = FwdFinalVictim; }; // expected-error {{test profile fired on completion of class 'FwdFinalHolder' under profile 'test::class_final'}} +template void fwd_final_user(); // expected-note {{in instantiation of function template specialization 'fwd_final_user' requested here}} + +// Contrast: with a plain forward declaration *before* the suppressed +// pattern, the specialization's location is the forward declaration's name +// loc -- before the dominion begins -- so this fired even without +// end-bounding. +template struct FwdVictim; // expected-error {{test profile fired on completion of class 'FwdVictim' under profile 'test::class_final'}} +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::class_final)]] void fwd_victim_user() { + FwdVictim v; // expected-note {{in instantiation of template class 'FwdVictim' requested here}} + (void)v; +} +template struct FwdVictim { T m; }; +template void fwd_victim_user(); // expected-note {{in instantiation of function template specialization 'fwd_victim_user' requested here}} + // Without `-fprofiles`, the enforce attribute is `warn_attribute_ignored` // and the diagnostic never fires. This is exercised by the no-profiles RUN // line, which expects only the two attribute-ignored warnings above. diff --git a/clang/test/SemaCXX/safety-profile-type-cast.cpp b/clang/test/SemaCXX/safety-profile-type-cast.cpp index 830b4ddccc780..c7b3a685eddd2 100644 --- a/clang/test/SemaCXX/safety-profile-type-cast.cpp +++ b/clang/test/SemaCXX/safety-profile-type-cast.cpp @@ -802,3 +802,73 @@ template auto leak_macro_fn() { return EMIT_CAST(T, &instantiation_leak_target); } // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(test::type_cast)]] int *leak_macro_use = leak_macro_fn(); // expected-note {{in instantiation of function template specialization 'leak_macro_fn' requested here}} + +// The dominion's end is bounded by the construct's recorded end location: a +// live suppress scope does not cover a pattern first declared or defined +// *after* the suppressed construct, even though those tokens compare after +// the dominion's begin. + +// Fwd-declared class template defined after the suppressed pattern. +template struct FwdLeak; +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(test::type_cast)]] void fwd_leak_user() { + FwdLeak v; // expected-note {{in evaluation of exception specification for 'FwdLeak::FwdLeak' needed here}} + (void)v; +} +template +struct FwdLeak { // #FwdLeak + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FwdLeak {{in instantiation of default member initializer 'FwdLeak::p' requested here}} +}; +template void fwd_leak_user(); // expected-note {{in instantiation of function template specialization 'fwd_leak_user' requested here}} + +// Statement-entry variant: the suppressed block's recorded end excludes the +// later-defined pattern. +template struct FwdLeak2; +template void fwd_gap_user() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(test::type_cast)]] { + FwdLeak2 v; // expected-note {{in evaluation of exception specification for 'FwdLeak2::FwdLeak2' needed here}} + (void)v; + } +} +template +struct FwdLeak2 { // #FwdLeak2 + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FwdLeak2 {{in instantiation of default member initializer 'FwdLeak2::p' requested here}} +}; +template void fwd_gap_user(); // expected-note {{in instantiation of function template specialization 'fwd_gap_user' requested here}} + +// TagDecl arm: the suppressed class template's brace range bounds its parent +// entry, excluding the later-defined pattern its member instantiates. +template struct FwdLeak3; +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] FwdSupClass { + static void m() { + FwdLeak3 v; // expected-note {{in evaluation of exception specification for 'FwdLeak3::FwdLeak3' needed here}} + (void)v; + } +}; +template +struct FwdLeak3 { // #FwdLeak3 + T *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} \ + // expected-note@#FwdLeak3 {{in instantiation of default member initializer 'FwdLeak3::p' requested here}} +}; +void fwd_sup_use() { FwdSupClass::m(); } // expected-note {{in instantiation of member function 'FwdSupClass::m' requested here}} + +// A construct still being parsed records no end -- the scope's lifetime +// bounds the dominion, which is exact mid-parse. A class-level suppress +// therefore still covers members instantiated from a static member's +// in-class initializer, both synchronously mid-parse (the generic lambda's +// call operator) and deferred (the member function template). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] MidParseSync { + static inline int *x = [](auto t) { return reinterpret_cast(t); }(0); +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(test::type_cast)]] MidParseDeferred { + template static T *f() { return reinterpret_cast(0); } + static inline int *x = f(); +}; From 3df616eb0c8acc9c2d2f02802e3dfb7b40958e89 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 12:50:56 -0400 Subject: [PATCH 226/289] Document end-bounding of the parse-time suppress dominion --- clang/docs/ProfilesFramework.rst | 47 +++++++++++++++++++------------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index b84e14cd099e4..e608b7e9857a3 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -432,22 +432,30 @@ This applies identically to the parse-time suppression stack and the post-parse Stmt-tree walker described in pattern 2. The parse-time stack enforces the dominion positionally: each entry records -the begin location of the construct its attribute appertains to, a violation -matches an entry only if its location is at or after that begin (in -translation-unit token order), and the entry's ``ProfileSuppressScope`` -lifetime bounds the dominion's end. This is what keeps a live suppress scope +the token range of the construct its attribute appertains to, and a +violation matches an entry only if its location falls within that range (in +translation-unit token order). This is what keeps a live suppress scope from leaking into code whose tokens it does not cover. A check can fire under an *unrelated* construct's scope in two ways: a template pattern -instantiated synchronously while the scope is live (the pattern's tokens -- -which instantiated code retains as its source locations -- precede the -suppressed construct), and a class or constructor finalized as a side effect -of such an instantiation (patterns 3 and 4). In both cases the entry's -begin location is after the violation's, so the suppression correctly does -not apply; conversely, a local class or lambda *defined inside* the -suppressed construct is covered, whichever path re-enters it. One known -over-approximation: scope liveness bounds the dominion's end, so a pattern -forward-declared before but *defined after* the suppressed construct is -wrongly treated as covered while the scope is live. +instantiated synchronously while the scope is live (instantiated code +retains the pattern's source locations, which lie outside the suppressed +construct wherever the pattern is declared -- before it, or first declared +after it), and a class or constructor finalized as a side effect of such an +instantiation (patterns 3 and 4). In both cases the violation's location is +outside the entry's range, so the suppression correctly does not apply; +conversely, a local class or lambda *defined inside* the suppressed +construct is covered, whichever path re-enters it. + +The range's end is recorded only when the construct was already fully +parsed when the entry was pushed -- a completed pattern or lexical parent at +an instantiation site, or a transformed ``AttributedStmt``. For a construct +still being parsed no end is recorded (its end location would be +misleadingly early: a mid-parse class collapses to its name token, a +body-pending function to its declarator) and the entry's +``ProfileSuppressScope`` lifetime bounds the dominion instead. That +fallback is exact mid-parse: the construct's later tokens do not exist yet, +and instantiation of a template that has no definition yet is deferred past +the scope's death. .. _profiles-internals: @@ -464,11 +472,12 @@ benefit from understanding, even though they do not interact with them directly. An RAII guard that pushes suppression entries onto ``Sema::ProfileSuppressStack`` and pops them on destruction. It is used by the parser and template instantiation machinery to make ``[[profiles::suppress]]`` attributes active -during the appropriate region. Each entry records the begin location of the -construct its attribute appertains to, and matches only violations located at -or after it (see :ref:`profiles-token-dominion`). ``checkProfileViolation`` -consults ``ProfileSuppressStack`` directly, so profile implementers never need -to create ``ProfileSuppressScope`` objects. +during the appropriate region. Each entry records the token range of the +construct its attribute appertains to (the end only once the construct is +fully parsed) and matches only violations located within it (see +:ref:`profiles-token-dominion`). ``checkProfileViolation`` consults +``ProfileSuppressStack`` directly, so profile implementers never need to +create ``ProfileSuppressScope`` objects. Stmt-Tree Suppression Walker ---------------------------- From a9ed99e1a84700755d173229853fb6d6ec427d82 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 13:53:07 -0400 Subject: [PATCH 227/289] Funnel the operator, throw, and new std::init checks through shared helpers --- clang/include/clang/Sema/SemaProfiles.h | 39 +++++++++++++++ clang/lib/Sema/SemaExpr.cpp | 40 ++++----------- clang/lib/Sema/SemaExprCXX.cpp | 32 +++++------- clang/lib/Sema/SemaProfiles.cpp | 65 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 52 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 6685daf883a2b..e05d591d73607 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -288,6 +288,45 @@ class SemaProfiles : public SemaBase { /// instantiation. void checkInitProfileRefCapture(SourceLocation Loc, const ValueDecl *Var); + /// std::init / ref_to_uninit (paper §4.3): assigning to a pointer must + /// respect the assigned-to pointer's [[ref_to_uninit]] marking; a no-op for + /// a non-pointer LHS. Hosts the cluster from Sema::CreateBuiltinBinOp's + /// BO_Assign arm; also run on a reused assignment by recheckReusedExpr. + void checkInitProfilePointerAssignment(Expr *LHS, Expr *RHS, + SourceLocation OpLoc); + + /// std::init: the check pair every built-in assignment hosts (paper + /// §5.4-§5.6): the compound-assignment old-value load (read-through -- + /// excluding the shifts, whose LHS promotion already loads through the + /// lvalue-to-rvalue chokepoint) and the subobject-write check. Hosts the + /// cluster from Sema::CheckAssignmentOperands; also run on a reused + /// assignment by recheckReusedExpr. \p IsCompound distinguishes `op=` + /// from `=` (host site: !CompoundType.isNull(); reused node: + /// isa). + void checkInitProfileAssignmentOperands(BinaryOperatorKind Opc, + Expr *LHSExpr, bool IsCompound, + SourceLocation OpLoc); + + /// std::init: the check pair a built-in ++/-- hosts -- the old-value load + /// (read-through) and the store (subobject-write). Hosts the cluster from + /// Sema::CreateBuiltinUnaryOp's increment/decrement arm; also run on a + /// reused operator by recheckReusedExpr. + void checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc); + + /// std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes + /// the exception object, which cannot carry [[ref_to_uninit]]; a no-op for + /// a non-pointer exception object. Hosts the cluster from + /// Sema::BuildCXXThrow; also run on a reused throw by recheckReusedExpr. + void checkInitProfileThrowOperand(const Expr *Operand); + + /// std::init / ref_to_uninit (paper §5): a written initializer for an + /// allocated pointer binds it like a variable initialization, and a heap + /// pointer object cannot carry [[ref_to_uninit]]. \p Init is the single + /// written initializer expression, or null when there is none (a no-op). + /// Hosts the cluster from Sema::BuildCXXNew; also run on a reused + /// new-expression by recheckReusedExpr. + void checkInitProfileNewInitializer(QualType AllocType, Expr *Init); + /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose /// [[uninit]] placed on a pointer, a union variable, or a union member. /// \p D must already carry the UninitAttr (the marker location is taken diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index 86bd43e7f0987..a1882aa655c7a 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -14414,17 +14414,9 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS, // the shared funnel for simple and every compound assignment, so the check // fires exactly once per built-in assignment; class-typed operator= never // reaches it. - if (getLangOpts().Profiles) { - // A compound assignment reads the old value but builds no - // lvalue-to-rvalue node for it, so the DefaultLvalueConversion - // read-through chokepoint never sees the load; check it here. The shift - // forms are the exception: CheckShiftOperands promotes their LHS through - // DefaultLvalueConversion, which has already fired for them. - if (!CompoundType.isNull() && Opc != BO_ShlAssign && Opc != BO_ShrAssign) - Profiles().checkInitProfileReadThrough(LHSExpr->getExprLoc(), LHSExpr, - LHSExpr->getType()); - Profiles().checkInitProfileSubobjectWrite(Loc, LHSExpr); - } + if (getLangOpts().Profiles) + Profiles().checkInitProfileAssignmentOperands( + Opc, LHSExpr, /*IsCompound=*/!CompoundType.isNull(), Loc); QualType LHSType = LHSExpr->getType(); QualType RHSType = CompoundType.isNull() ? RHS.get()->getType() : @@ -15525,18 +15517,10 @@ ExprResult Sema::CreateBuiltinBinOp(SourceLocation OpLoc, DiagnoseSelfMove(LHS.get(), RHS.get(), OpLoc); // std::init / ref_to_uninit (paper §5): assigning a pointer must respect - // the [[ref_to_uninit]] marking of the assigned-to pointer. References - // cannot be reseated, so only pointer assignment applies. The marker is - // read when the LHS directly names a pointer entity; any other lvalue - // (e.g. *pp, arr[i]) cannot carry a local marker, so it is the default - // unmarked pointer (paper §4.3) and must not be bound to uninitialized - // memory. - if (getLangOpts().Profiles && LHS.get()->getType()->isPointerType()) { - const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(LHS.get()); - Profiles().checkInitProfileRefToUninit( - OpLoc, VD && VD->hasAttr(), - /*IsReference=*/false, RHS.get()); - } + // the [[ref_to_uninit]] marking of the assigned-to pointer. + if (getLangOpts().Profiles) + Profiles().checkInitProfilePointerAssignment(LHS.get(), RHS.get(), + OpLoc); // Avoid copying a block to the heap if the block is assigned to a local // auto variable that is declared in the same scope as the block. This @@ -16211,14 +16195,8 @@ ExprResult Sema::CreateBuiltinUnaryOp(SourceLocation OpLoc, // than in CheckIncrementDecrementOperand, which self-recurses on // placeholder operands and would fire twice; overloaded class ++/-- // never reaches CreateBuiltinUnaryOp. - if (getLangOpts().Profiles && !resultType.isNull()) { - // ++/-- reads the old value with no lvalue-to-rvalue node (unlike -x - // or !x); check the load here. - Profiles().checkInitProfileReadThrough(Input.get()->getExprLoc(), - Input.get(), - Input.get()->getType()); - Profiles().checkInitProfileSubobjectWrite(OpLoc, Input.get()); - } + if (getLangOpts().Profiles && !resultType.isNull()) + Profiles().checkInitProfileIncDec(Input.get(), OpLoc); break; case UO_AddrOf: resultType = CheckAddressOfOperand(Input, OpLoc); diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index ef9793bf20fa3..6aa23b8a07a25 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -906,16 +906,11 @@ ExprResult Sema::BuildCXXThrow(SourceLocation OpLoc, Expr *Ex, if (Res.isInvalid()) return ExprError(); - // std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes - // the exception object, which cannot carry [[ref_to_uninit]], so throwing - // a pointer to uninitialized memory is always the unmarked-direction - // violation. (Reads like `throw *p` funnel through the read-through check - // instead.) Dependent operands are outside this block; the Decl-less - // wrapper defers on a template pattern. - if (ExceptionObjectTy->isPointerType()) - Profiles().checkInitProfileRefToUninit(Ex->getExprLoc(), - /*TargetIsRefToUninit=*/false, - /*IsReference=*/false, Ex); + // std::init / ref_to_uninit (paper §5): throwing a pointer to + // uninitialized memory. Dependent operands are outside this block; the + // Decl-less wrapper defers on a template pattern. + if (getLangOpts().Profiles) + Profiles().checkInitProfileThrowOperand(Ex); Ex = Res.get(); } @@ -2620,17 +2615,12 @@ ExprResult Sema::BuildCXXNew(SourceRange Range, bool UseGlobal, Initializer = FullInit.get(); // std::init / ref_to_uninit (paper §5): a written initializer for an - // allocated pointer binds it like a variable initialization -- but a heap - // pointer object cannot carry [[ref_to_uninit]], so binding it to - // uninitialized memory is always the unmarked-direction violation. A - // braced `new T*{&x}` presents the InitListExpr, which the recognizer's - // single-element pass-through looks through. Dependent operands are - // outside this block; BuildCXXNew re-runs at instantiation, and the - // Decl-less wrapper defers on a template pattern. - if (AllocType->isPointerType() && Exprs.size() == 1) - Profiles().checkInitProfileRefToUninit(Exprs[0]->getExprLoc(), - /*TargetIsRefToUninit=*/false, - /*IsReference=*/false, Exprs[0]); + // allocated pointer binds it like a variable initialization. Dependent + // operands are outside this block; the Decl-less wrapper defers on a + // template pattern. + if (getLangOpts().Profiles) + Profiles().checkInitProfileNewInitializer( + AllocType, Exprs.size() == 1 ? Exprs[0] : nullptr); // FIXME: If we have a KnownArraySize, check that the array bound of the // initializer is no greater than that constant value. diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index c69c78d65015a..122191151b890 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1224,6 +1224,71 @@ void SemaProfiles::checkInitProfileSubobjectWrite(SourceLocation Loc, << "std::init" << !isa(LHS->IgnoreParenImpCasts()); } +void SemaProfiles::checkInitProfilePointerAssignment(Expr *LHS, Expr *RHS, + SourceLocation OpLoc) { + // References cannot be reseated, so only pointer assignment applies. The + // marker is read when the LHS directly names a pointer entity; any other + // lvalue (e.g. *pp, arr[i]) cannot carry a local marker, so it is the + // default unmarked pointer (paper §4.3) and must not be bound to + // uninitialized memory. + if (!LHS->getType()->isPointerType()) + return; + const ValueDecl *VD = getDirectlyNamedDecl(LHS); + checkInitProfileRefToUninit(OpLoc, VD && VD->hasAttr(), + /*IsReference=*/false, RHS); +} + +void SemaProfiles::checkInitProfileAssignmentOperands(BinaryOperatorKind Opc, + Expr *LHSExpr, + bool IsCompound, + SourceLocation OpLoc) { + // A compound assignment reads the old value but builds no lvalue-to-rvalue + // node for it, so the DefaultLvalueConversion read-through chokepoint never + // sees the load; check it here. The shift forms are the exception: + // CheckShiftOperands promotes their LHS through DefaultLvalueConversion, + // which has already fired for them. + if (IsCompound && Opc != BO_ShlAssign && Opc != BO_ShrAssign) + checkInitProfileReadThrough(LHSExpr->getExprLoc(), LHSExpr, + LHSExpr->getType()); + checkInitProfileSubobjectWrite(OpLoc, LHSExpr); +} + +void SemaProfiles::checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc) { + // ++/-- reads the old value with no lvalue-to-rvalue node (unlike -x or + // !x), then stores to its operand like an assignment does to its LHS. + checkInitProfileReadThrough(Operand->getExprLoc(), Operand, + Operand->getType()); + checkInitProfileSubobjectWrite(OpLoc, Operand); +} + +void SemaProfiles::checkInitProfileThrowOperand(const Expr *Operand) { + // A thrown pointer copy-initializes the exception object, which cannot + // carry [[ref_to_uninit]], so throwing a pointer to uninitialized memory is + // always the unmarked-direction violation. (Reads like `throw *p` funnel + // through the read-through check instead.) + QualType ExceptionObjectTy = + getASTContext().getExceptionObjectType(Operand->getType()); + if (!ExceptionObjectTy->isPointerType()) + return; + checkInitProfileRefToUninit(Operand->getExprLoc(), + /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Operand); +} + +void SemaProfiles::checkInitProfileNewInitializer(QualType AllocType, + Expr *Init) { + // A written initializer for an allocated pointer binds it like a variable + // initialization -- but a heap pointer object cannot carry + // [[ref_to_uninit]], so binding it to uninitialized memory is always the + // unmarked-direction violation. A braced `new T*{&x}` presents the + // InitListExpr, which the recognizer's single-element pass-through looks + // through. + if (!AllocType->isPointerType() || !Init) + return; + checkInitProfileRefToUninit(Init->getExprLoc(), + /*TargetIsRefToUninit=*/false, + /*IsReference=*/false, Init); +} namespace { // Row for the unified finalization dispatch shared by class-finalization From dd78650895086c0ed25e8a61a91968193e891131 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 15:13:26 -0400 Subject: [PATCH 228/289] Diagnose non-dependent std::init violations at template definition time Defer Decl-less checks on operand instantiation-dependence instead of the dependent context; instantiation rebuilds may repeat a definition-time diag. --- clang/docs/ProfilesFramework.rst | 78 ++++-- clang/include/clang/Sema/SemaProfiles.h | 43 ++-- clang/lib/Sema/SemaExprCXX.cpp | 12 +- clang/lib/Sema/SemaProfiles.cpp | 89 ++++--- .../safety-profile-init-ref-to-uninit.cpp | 233 ++++++++++++++---- .../SemaCXX/safety-profile-init-write.cpp | 61 ++++- 6 files changed, 384 insertions(+), 132 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index e608b7e9857a3..ba3327db76622 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -180,16 +180,29 @@ or template instantiation; selected specializations replay suppressed diagnostics when used. Unevaluated and discarded-statement contexts are skipped. The profile name is passed as ``%0``. -``checkProfileViolation`` fires at parse time. For a *non-dependent* -expression inside a template, the check therefore runs on the template -*pattern*, and -- because for some node kinds (such as casts) a non-dependent -``Build*`` result is reused unchanged at instantiation -- it is not re-run on -the specialization. As a result it can fire for a never-instantiated template -or in an ``if constexpr`` branch discarded only at instantiation. -``test::type_cast`` accepts this (it is a test-only profile). A profile whose -``Build*`` routine *is* re-run at instantiation can instead defer on a template -pattern with a ``CurContext->isDependentContext()`` guard, as the ``std::init`` -ref_to_uninit binding checks do (see ``checkInitProfileRefToUninit``). +``checkProfileViolation`` fires at parse time. Inside a template, parse-time +checks follow one unified model. A *non-dependent* construct is checked on +the template *pattern*, at definition time: TreeTransform may return such a +node unchanged at instantiation (for some node kinds, such as casts, a +non-dependent ``Build*`` result is reused), so deferring would silently lose +the diagnostic. This deliberately trades strict "as-if after phase 7" purity +(P3589R2 §1.1) for reuse-proof diagnostics: a non-dependent violation +diagnoses even in a never-instantiated template or in an ``if constexpr`` +branch whose discarding is not yet known at the pattern (a branch already +known discarded -- a non-value-dependent false condition -- stays silent). +An *instantiation-dependent* construct cannot be checked on the pattern; it +is always rebuilt at instantiation, where the re-run ``Build*`` checks the +substituted form, once per specialization. A construct with non-dependent +check operands can still be rebuilt at instantiation (a local variable, a +call argument, or a return statement forces a rebuild, for example); the +re-run ``Build*`` then repeats the definition-time diagnostic at the same +location, with an ``in instantiation of ...`` note. This repetition is +accepted for now; ``test::type_cast``'s cast nodes are never rebuilt when +non-dependent, so it never repeats. + +Under ``-fdelayed-template-parsing`` the body of a never-instantiated +template is never parsed at all, so definition-time diagnosis of +non-dependent violations does not occur in that mode. Suppression for parse-time check sites is consulted via the ``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` @@ -1152,19 +1165,28 @@ recognizers symmetric. (``Sema::BuildLambdaExpr``). A capture cannot carry the marker, so only the unmarked-direction violation can fire there; a *copy* capture is not a binding -- it reads the variable in the enclosing function's CFG, which is - the flow-based ``uninit_read`` pass's territory. Every site defers on a - template pattern and - fires once, at instantiation. The variable, data-member, and constructor - member-initializer sites pass the instantiated ``Decl`` (deferred by the - ``D->isTemplated()`` check in ``shouldEmitProfileViolation``); the - constructor site passes the enclosing constructor and is re-run by + the flow-based ``uninit_read`` pass's territory. Inside a template the + sites split by timing. The Decl-carrying variable, data-member, and + constructor member-initializer sites defer on the pattern (the + ``D->isTemplated()`` check in ``shouldEmitProfileViolation``) and fire + once, at instantiation, on the instantiated ``Decl``; the constructor site + passes the enclosing constructor and is re-run by ``BuildMemberInitializer`` at instantiation, exactly like - ``ctor_uninit_member``. The Decl-less call-argument, assignment, return, and - aggregate-field sites -- whose ``Build*`` / ``InitListChecker`` routine is - re-run at instantiation -- instead defer via a - ``CurContext->isDependentContext()`` guard in ``checkInitProfileRefToUninit``, so - they neither double-fire nor fire in a discarded ``if constexpr`` branch or a - never-instantiated template. The aggregate-field hooks + ``ctor_uninit_member``. The Decl-less call-argument, assignment, return, + aggregate-field, and capture sites instead defer only when the source (for + the capture, the captured variable's type; for pointer assignment, also + the LHS) is *instantiation-dependent* -- such constructs are always rebuilt + at instantiation, where the re-run ``Build*`` / ``InitListChecker`` routine + checks the substituted form. A non-dependent construct fires at + *definition time* (TreeTransform can reuse it unchanged, so deferring + would lose the diagnostic) and repeats if the construct is rebuilt at + instantiation anyway; see Pattern 1 above for the unified model, its + accepted phase-7 trade, and the accepted repetition. The two timings are + visible side by side: + ``int *p = &g_uninit;`` in a template fires at instantiation (Decl-carrying + variable site), while ``p = &g_uninit;`` fires at definition time + (Decl-less assignment site) -- a deliberate asymmetry. The aggregate-field + hooks (``CheckSubElementType`` for a pointer field, ``CheckReferenceType`` for a reference field) are scoped to a member subobject (``EK_Member`` with a non-null parent), so the enclosing variable/argument/return is left to its own @@ -1193,8 +1215,10 @@ recognizers symmetric. subobject-wise delayed initialization of an ``[[uninit]]`` object is itself banned (paper §5.4/§5.5; only whole-object ``construct_at`` re-initializes, which is uniformly unmodeled), so no assignment could have given the - subobject a value. Being Decl-less, it defers on a dependent context - and fires once, at instantiation. An address-of (``&*p``), a reference + subobject a value. Being Decl-less, it defers only on an + instantiation-dependent glvalue (rebuilt and re-checked at instantiation) + and otherwise fires at definition time, repeating if the read is rebuilt + at instantiation anyway (accepted). An address-of (``&*p``), a reference binding, a discarded-value expression (``(void)*p``), and a write (``*p = 5`` or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads (``s.x = 1`` is instead banned as a subobject store by ``uninit_write``, @@ -1361,8 +1385,10 @@ initialization sequence. exactly once, and class-typed ``operator=`` (which diverts to overload resolution) never reaches it -- and the built-in increment/decrement arm of ``Sema::CreateBuiltinUnaryOp`` (overloaded class ``++``/``--`` never - reaches it). Both are Decl-less sites: they defer on a template pattern - via the dependent-context guard and fire once, at instantiation. + reaches it). Both are Decl-less sites: they defer only on an + instantiation-dependent store target -- always rebuilt and re-checked at + instantiation -- and otherwise fire at definition time, repeating if the + operator is rebuilt at instantiation anyway (see Pattern 1 and R7). - A compound assignment or a built-in ``++``/``--`` also *reads* the old value, so on a subobject of a marked object the R7 read-through diagnostic fires alongside this rule's (the shift forms load through their LHS diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index e05d591d73607..b3da925b95c72 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -222,6 +222,14 @@ class SemaProfiles : public SemaBase { /// a marked target must refer to uninitialized memory, and an unmarked /// target must not. Shared by the variable, data-member, assignment, /// argument, and return check sites; gated by shouldEmitProfileViolation. + /// A Decl-less call defers only on an instantiation-dependent \p Src -- + /// such a construct is always rebuilt at instantiation, re-running this + /// funnel with the substituted source -- and otherwise fires at definition + /// time; if the construct is rebuilt at instantiation anyway (a local + /// operand, a call argument, a return), the same diagnostic repeats there + /// (accepted for now). A Decl-carrying call instead defers via the + /// D->isTemplated() check in shouldEmitProfileViolation and fires on the + /// instantiated declaration. void checkInitProfileRefToUninit(SourceLocation Loc, bool TargetIsRefToUninit, bool IsReference, const Expr *Src, const Decl *D = nullptr); @@ -252,7 +260,10 @@ class SemaProfiles : public SemaBase { /// their LHS promotion already funnels through the chokepoint). Reuses the /// ref_to_uninit recognizer with its read access preset, so a direct read /// of a named [[uninit]] object is left to the flow-based uninit_read - /// pass. A std::byte read is exempt (paper §4.5). + /// pass. A std::byte read is exempt (paper §4.5). Defers only on an + /// instantiation-dependent \p Glvalue (rebuilt at instantiation, where the + /// check re-runs); a non-dependent read fires at definition time and may + /// repeat if the read is rebuilt at instantiation anyway (accepted). void checkInitProfileReadThrough(SourceLocation Loc, const Expr *Glvalue, QualType ValueType); @@ -266,7 +277,10 @@ class SemaProfiles : public SemaBase { /// entity is its initialization (paper §4.5), and storage reached through /// [[ref_to_uninit]] is trusted (the deferred construct_at slice), so only /// a below-top-level [[uninit]] marker fires. A std::byte store is exempt - /// (paper §4.5). + /// (paper §4.5). Defers only on an instantiation-dependent \p LHS (rebuilt + /// at instantiation, where the check re-runs); a non-dependent store fires + /// at definition time and may repeat if the assignment is rebuilt at + /// instantiation anyway (accepted). void checkInitProfileSubobjectWrite(SourceLocation Loc, const Expr *LHS); /// std::init / ref_to_uninit (paper §5): a pointer argument passed through @@ -284,14 +298,18 @@ class SemaProfiles : public SemaBase { /// is always the unmarked-direction violation. Called from /// \c Sema::BuildLambdaExpr for each by-reference non-init variable capture /// (init-captures are checked at \c createLambdaInitCaptureVarDecl); defers - /// on a template pattern, where TreeTransform rebuilds the lambda at - /// instantiation. + /// only when the captured variable's type is instantiation-dependent. + /// TreeTransform always rebuilds a lambda at instantiation, so a deferred + /// capture re-processes there -- and a definition-time fire repeats there + /// (accepted). void checkInitProfileRefCapture(SourceLocation Loc, const ValueDecl *Var); /// std::init / ref_to_uninit (paper §4.3): assigning to a pointer must /// respect the assigned-to pointer's [[ref_to_uninit]] marking; a no-op for /// a non-pointer LHS. Hosts the cluster from Sema::CreateBuiltinBinOp's - /// BO_Assign arm; also run on a reused assignment by recheckReusedExpr. + /// BO_Assign arm. An instantiation-dependent LHS defers to the + /// instantiation rebuild (its marker cannot be read yet); the source's + /// dependence is the shared funnel's to defer on. void checkInitProfilePointerAssignment(Expr *LHS, Expr *RHS, SourceLocation OpLoc); @@ -299,32 +317,29 @@ class SemaProfiles : public SemaBase { /// §5.4-§5.6): the compound-assignment old-value load (read-through -- /// excluding the shifts, whose LHS promotion already loads through the /// lvalue-to-rvalue chokepoint) and the subobject-write check. Hosts the - /// cluster from Sema::CheckAssignmentOperands; also run on a reused - /// assignment by recheckReusedExpr. \p IsCompound distinguishes `op=` - /// from `=` (host site: !CompoundType.isNull(); reused node: - /// isa). + /// cluster from Sema::CheckAssignmentOperands. \p IsCompound distinguishes + /// `op=` from `=` (!CompoundType.isNull() at the host site). void checkInitProfileAssignmentOperands(BinaryOperatorKind Opc, Expr *LHSExpr, bool IsCompound, SourceLocation OpLoc); /// std::init: the check pair a built-in ++/-- hosts -- the old-value load /// (read-through) and the store (subobject-write). Hosts the cluster from - /// Sema::CreateBuiltinUnaryOp's increment/decrement arm; also run on a - /// reused operator by recheckReusedExpr. + /// Sema::CreateBuiltinUnaryOp's increment/decrement arm. void checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc); /// std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes /// the exception object, which cannot carry [[ref_to_uninit]]; a no-op for /// a non-pointer exception object. Hosts the cluster from - /// Sema::BuildCXXThrow; also run on a reused throw by recheckReusedExpr. + /// Sema::BuildCXXThrow. void checkInitProfileThrowOperand(const Expr *Operand); /// std::init / ref_to_uninit (paper §5): a written initializer for an /// allocated pointer binds it like a variable initialization, and a heap /// pointer object cannot carry [[ref_to_uninit]]. \p Init is the single /// written initializer expression, or null when there is none (a no-op). - /// Hosts the cluster from Sema::BuildCXXNew; also run on a reused - /// new-expression by recheckReusedExpr. + /// Hosts the cluster from Sema::BuildCXXNew. An instantiation-dependent + /// allocated type defers to the instantiation rebuild. void checkInitProfileNewInitializer(QualType AllocType, Expr *Init); /// std::init / pointer_marker + union_marker (paper §4.1, §5.6): diagnose diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index 6aa23b8a07a25..433c6200c8e4a 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -907,8 +907,9 @@ ExprResult Sema::BuildCXXThrow(SourceLocation OpLoc, Expr *Ex, return ExprError(); // std::init / ref_to_uninit (paper §5): throwing a pointer to - // uninitialized memory. Dependent operands are outside this block; the - // Decl-less wrapper defers on a template pattern. + // uninitialized memory. The Decl-less wrapper defers only on an + // instantiation-dependent operand, rebuilt at instantiation; a + // non-dependent throw is checked at definition time. if (getLangOpts().Profiles) Profiles().checkInitProfileThrowOperand(Ex); @@ -2615,9 +2616,10 @@ ExprResult Sema::BuildCXXNew(SourceRange Range, bool UseGlobal, Initializer = FullInit.get(); // std::init / ref_to_uninit (paper §5): a written initializer for an - // allocated pointer binds it like a variable initialization. Dependent - // operands are outside this block; the Decl-less wrapper defers on a - // template pattern. + // allocated pointer binds it like a variable initialization. The + // Decl-less wrapper defers only on an instantiation-dependent allocated + // type or initializer, rebuilt at instantiation; a non-dependent + // new-expression is checked at definition time. if (getLangOpts().Profiles) Profiles().checkInitProfileNewInitializer( AllocType, Exprs.size() == 1 ? Exprs[0] : nullptr); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 122191151b890..7d1183fdbe410 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -353,20 +353,27 @@ bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, // possible to 'SFINAE out' failure of a program to satisfy a profile // requirement." // - // A templated entity is not yet a phase-7 entity, so a profile rule must fire - // only on its instantiation -- where D is the instantiated, non-templated - // declaration -- not on the template pattern. Checking the pattern too would - // diagnose never-instantiated templates and double-fire (once when the - // pattern is parsed and again at each instantiation). + // A templated entity is not yet a phase-7 entity, so a Decl-carrying rule + // fires only on its instantiation -- where D is the instantiated, + // non-templated declaration -- not on the template pattern (the [[uninit]] + // marker checks and the binding checks that pass a Decl are re-run on the + // instantiated field / variable, so they defer here). Checking the pattern + // too would double-fire, once at parse and again per instantiation. // - // Decl-less expression check sites whose Build* routine is re-run at - // instantiation (the ref_to_uninit binding checks: call argument, pointer - // assignment, return) instead defer in a dependent context from their own - // wrapper, checkInitProfileRefToUninit, since no Decl is available here. The - // [[uninit]] marker checks pass D (via checkInitProfileMarkerPlacement) and - // are re-run on the instantiated field / variable, so they defer here too. - // The reinterpret_cast check still passes D == nullptr and is not re-checked - // at instantiation, so it keeps running once at parse time (a separate gap). + // The Decl-less expression check sites (D == nullptr here) instead defer + // from their own entry points, and only when their check-relevant operands + // are instantiation-dependent -- exactly the constructs TreeTransform + // always rebuilds, so the hosting Build* routine re-runs the deferred check + // at instantiation. A fully non-dependent construct may be returned + // unchanged by TreeTransform (its Build* never re-runs), so it is checked + // at definition time instead; when such a construct is rebuilt at + // instantiation anyway (a local operand, a call argument, a return), the + // definition-time diagnostic repeats there -- accepted for now. This + // deliberately trades strict phase-7 purity for reuse-proof diagnostics: a + // non-dependent violation in a never-instantiated template, or in an + // if-constexpr branch not yet known to be discarded, diagnoses at + // definition time -- the same model the test::type_cast profile and the + // reinterpret_cast check follow. if (D && D->isTemplated()) return false; if (SemaRef.isUnevaluatedContext()) @@ -1047,19 +1054,6 @@ bool SemaProfiles::refersToUninitializedMemory(const Expr *E, UninitStorage::Uninitialized; } -// The Decl-less ref_to_uninit check sites (call argument, default argument, -// assignment, return, read-through) can't rely on the D->isTemplated() -// deferral in shouldEmitProfileViolation, but their Build* routines are -// re-run at instantiation, so defer on a template pattern here instead: -// firing on the pattern double-diagnoses and wrongly fires in discarded -// if-constexpr branches / never-instantiated templates. Sites that do pass a -// Decl are unaffected. (This must stay out of shouldEmitProfileViolation: -// its other Decl-less caller, the reinterpret_cast check, is *not* re-run at -// instantiation, so deferring there would lose the diagnostic entirely.) -static bool deferUninitCheckOnTemplatePattern(Sema &S, const Decl *D) { - return !D && S.CurContext && S.CurContext->isDependentContext(); -} - void SemaProfiles::checkInitProfileRefToUninit(SourceLocation Loc, bool TargetIsRefToUninit, bool IsReference, const Expr *Src, @@ -1068,7 +1062,15 @@ void SemaProfiles::checkInitProfileRefToUninit(SourceLocation Loc, // not a source the user wrote, so it must not drive this rule. if (!Src || isa(Src->IgnoreParens())) return; - if (deferUninitCheckOnTemplatePattern(SemaRef, D)) + // An instantiation-dependent source cannot be classified yet; its construct + // is always rebuilt at instantiation, re-running this funnel with the + // substituted source. A non-dependent source is checked here, at definition + // time; if the construct is rebuilt at instantiation anyway (a local + // operand, a call argument, a return), the same diagnostic repeats there -- + // accepted for now. Decl-carrying callers are exempt: they defer via the + // D->isTemplated() check in shouldEmitProfileViolation and fire on the + // instantiated declaration. + if (!D && Src->isInstantiationDependent()) return; static constexpr StringRef Profile = "std::init"; static constexpr StringRef Rule = "ref_to_uninit"; @@ -1132,7 +1134,11 @@ void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, if (!Var->hasAttr() && !(Var->getType()->isReferenceType() && Var->hasAttr())) return; - if (deferUninitCheckOnTemplatePattern(SemaRef, /*D=*/nullptr)) + // The only Expr-less deferral here: an instantiation-dependent captured + // type defers to instantiation, where TreeTransform's unconditional lambda + // rebuild re-processes the capture. A concrete capture fires at definition + // time and repeats on that same rebuild -- accepted for now. + if (Var->getType()->isInstantiationDependentType()) return; if (!shouldEmitProfileViolation("std::init", "ref_to_uninit", Loc)) return; @@ -1190,7 +1196,11 @@ void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, // a read the user wrote, so it must not drive this rule. if (!Glvalue || isa(Glvalue->IgnoreParens())) return; - if (deferUninitCheckOnTemplatePattern(SemaRef, /*D=*/nullptr)) + // An instantiation-dependent glvalue cannot be classified yet; its read is + // always rebuilt at instantiation, where this check re-runs with the + // substituted operand. A non-dependent read fires at definition time and + // repeats if the read is rebuilt at instantiation anyway -- accepted. + if (Glvalue->isInstantiationDependent()) return; // Paper §4.5: reading an uninitialized std::byte is permitted. if (getASTContext().getBaseElementType(ValueType)->isStdByteType()) @@ -1210,7 +1220,12 @@ void SemaProfiles::checkInitProfileSubobjectWrite(SourceLocation Loc, // a store the user wrote, so it must not drive this rule. if (!LHS || isa(LHS->IgnoreParens())) return; - if (deferUninitCheckOnTemplatePattern(SemaRef, /*D=*/nullptr)) + // An instantiation-dependent store target cannot be classified yet; its + // assignment is always rebuilt at instantiation, where this check re-runs + // with the substituted LHS. A non-dependent store fires at definition time + // and repeats if the assignment is rebuilt at instantiation anyway -- + // accepted. + if (LHS->isInstantiationDependent()) return; // Paper §4.5: an uninitialized std::byte may be manipulated freely. if (getASTContext().getBaseElementType(LHS->getType())->isStdByteType()) @@ -1233,6 +1248,13 @@ void SemaProfiles::checkInitProfilePointerAssignment(Expr *LHS, Expr *RHS, // uninitialized memory. if (!LHS->getType()->isPointerType()) return; + // An instantiation-dependent LHS (e.g. an unresolved member access) has no + // readable marker yet -- getDirectlyNamedDecl would report it unmarked, a + // false positive when the instantiated entity is [[ref_to_uninit]]. The + // assignment is rebuilt at instantiation, where the marker is concrete. + // The source's dependence is the funnel's to defer on. + if (LHS->isInstantiationDependent()) + return; const ValueDecl *VD = getDirectlyNamedDecl(LHS); checkInitProfileRefToUninit(OpLoc, VD && VD->hasAttr(), /*IsReference=*/false, RHS); @@ -1282,8 +1304,11 @@ void SemaProfiles::checkInitProfileNewInitializer(QualType AllocType, // [[ref_to_uninit]], so binding it to uninitialized memory is always the // unmarked-direction violation. A braced `new T*{&x}` presents the // InitListExpr, which the recognizer's single-element pass-through looks - // through. - if (!AllocType->isPointerType() || !Init) + // through. An instantiation-dependent allocated type (note that a + // dependent-pointee `T*` still passes isPointerType) defers to the + // instantiation rebuild, which re-runs this check with the concrete type. + if (!AllocType->isPointerType() || + AllocType->isInstantiationDependentType() || !Init) return; checkInitProfileRefToUninit(Init->getExprLoc(), /*TargetIsRefToUninit=*/false, diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 3f66b54efe2a8..37c3e6b2d36e8 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -319,8 +319,8 @@ void test_fnptr_call_arguments(void (*fp)(int *), void (*fr)(int &)) { (void)rtu; } -// Decl-less like the declared-callee argument site: defers on the pattern and -// fires once, at instantiation. +// Decl-less like the declared-callee argument site; the dependent callee type +// keeps the call unchecked on the pattern, so it fires once, at instantiation. template void template_fnptr_call_arg(void (*fp)(T *)) { fp(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} @@ -353,10 +353,12 @@ void test_variadic_arguments() { (void)rtu; } -// Decl-less: defers on the pattern, fires once at instantiation. +// Decl-less with a non-dependent argument: fires at definition time, and +// again when the call (always rebuilt) re-promotes the argument at +// instantiation -- the accepted repetition. template void template_variadic_arg() { - vf(0, &g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + vf(0, &g_uninit); // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } template void template_variadic_arg(); // expected-note {{in instantiation of function template specialization 'template_variadic_arg' requested here}} @@ -441,12 +443,14 @@ void test_ref_captures() { (void)c1; (void)c2; (void)c3; (void)c4; (void)c5; } -// A by-reference capture inside a template body defers on the pattern and -// fires once, at instantiation (TreeTransform rebuilds the lambda). +// A by-reference capture of a variable with a non-dependent type fires at +// definition time, and again when TreeTransform's unconditional lambda +// rebuild re-processes the capture at instantiation -- the accepted +// repetition. template void template_ref_capture_bad() { int x [[uninit]]; - auto c = [&x] { x = 1; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c = [&x] { x = 1; }; // expected-error 2 {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} (void)c; } template void template_ref_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_ref_capture_bad' requested here}} @@ -468,12 +472,13 @@ void test_operator_arguments() { a = &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} } -// A functor-call argument inside a template body defers on the pattern and -// fires once, at instantiation, like every other Decl-less binding site. +// A non-dependent functor-call argument fires at definition time like every +// other Decl-less binding site, and repeats when the call is rebuilt at +// instantiation (the local functor forces the rebuild). template void template_functor_bad() { MarkedFunctor mf; - mf(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + mf(&g_init); // expected-error 2 {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} } template void template_functor_bad(); // expected-note {{in instantiation of function template specialization 'template_functor_bad' requested here}} @@ -719,58 +724,157 @@ void template_new_bad() { template void template_new_bad(); // expected-note {{in instantiation of function template specialization 'template_new_bad' requested here}} // The call-argument, pointer-assignment, and return sites pass no Decl, so -// (unlike the variable-init site, template_nondependent_bad above) their -// deferral cannot come from D->isTemplated(). They must still fire exactly -// once, at instantiation -- not twice, and not on the pattern. +// (unlike the variable-init site, template_nondependent_bad above) they defer +// only on an instantiation-dependent source. These sources are non-dependent, +// so each fires at definition time -- and each construct is rebuilt at +// instantiation anyway (the callee's implicit cast is stripped, forcing a +// call rebuild; the local p is remapped; a return statement always rebuilds), +// so the diagnostic repeats there. The repetition is accepted for now. template void template_call_arg_unmarked() { - take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_ptr(&g_uninit); // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } template void template_call_arg_unmarked(); // expected-note {{in instantiation of function template specialization 'template_call_arg_unmarked' requested here}} template void template_call_arg_marked() { - take_uninit_ptr(&g_init); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + take_uninit_ptr(&g_init); // expected-error 2 {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} } template void template_call_arg_marked(); // expected-note {{in instantiation of function template specialization 'template_call_arg_marked' requested here}} template void template_assignment_bad() { int *p = nullptr; - p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + p = &g_uninit; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)p; } template void template_assignment_bad(); // expected-note {{in instantiation of function template specialization 'template_assignment_bad' requested here}} template int *template_return_bad() { - return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + return &g_uninit; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } template int *template_return_bad(); // expected-note {{in instantiation of function template specialization 'template_return_bad' requested here}} -// A never-instantiated template stays silent: the deferred checks never run. +// The definition-time fire repeats once per instantiation that rebuilds the +// construct: two explicit instantiations pin the exact counts (one pattern +// fire plus one per specialization). +template +void template_assignment_repeats() { + int *p = nullptr; + p = &g_uninit; // expected-error 3 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_assignment_repeats(); // expected-note {{in instantiation of function template specialization 'template_assignment_repeats' requested here}} +template void template_assignment_repeats(); // expected-note {{in instantiation of function template specialization 'template_assignment_repeats' requested here}} + +template +int *template_return_repeats() { + return &g_uninit; // expected-error 3 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template int *template_return_repeats(); // expected-note {{in instantiation of function template specialization 'template_return_repeats' requested here}} +template int *template_return_repeats(); // expected-note {{in instantiation of function template specialization 'template_return_repeats' requested here}} + +// An instantiation-dependent source is not checkable on the pattern: no +// definition-time fire. The construct is rebuilt at every instantiation, so +// each violating specialization diagnoses once, with its note chain. +template +void template_dependent_per_spec() { + T *p = nullptr; + p = (T *)&g_uninit; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)p; +} +template void template_dependent_per_spec(); // expected-note {{in instantiation of function template specialization 'template_dependent_per_spec' requested here}} +template void template_dependent_per_spec(); // expected-note {{in instantiation of function template specialization 'template_dependent_per_spec' requested here}} + +// Fully non-dependent constructs whose operands transform to themselves are +// *reused* by TreeTransform at instantiation -- their Build* never re-runs. +// Deferring would silently lose the diagnostic (these all-global shapes were +// silent before), so they are checked at definition time: exactly one error, +// on the pattern, with no instantiation note. +int *g_ptr_sink = nullptr; +int **g_pp_sink = nullptr; + +template +void template_allglobal_bad() { + g_ptr_sink = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + throw &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + g_pp_sink = new (int *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_allglobal_bad(); + +// The same shapes diagnose in a never-instantiated template: definition-time +// checking deliberately trades strict "as-if after phase 7" purity for +// reuse-proof diagnostics. +template +void template_allglobal_never_instantiated() { + g_ptr_sink = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + throw &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + g_pp_sink = new (int *)(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A never-instantiated template diagnoses its non-dependent violations at +// definition time; only instantiation-dependent constructs (the (T *) cast) +// stay silent without an instantiation. template void template_never_instantiated() { - take_ptr(&g_uninit); + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} int *p = nullptr; - p = &g_uninit; + p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + p = (T *)&g_uninit; (void)p; } -// A violation in a discarded if-constexpr branch is never instantiated, so it -// is not diagnosed -- even though the dependent condition keeps the branch live -// on the pattern (where the check now defers). +// A value-dependent if-constexpr condition is not yet known discarded at the +// pattern, so the branch is live at parse and its non-dependent violations +// diagnose at definition time. The f instantiation discards the branch +// (never rebuilding its statements), so nothing repeats. template void template_discarded_branch() { if constexpr (sizeof(T) > 1000) { - take_ptr(&g_uninit); + take_ptr(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} int *p = nullptr; - p = &g_uninit; + p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)p; } } template void template_discarded_branch(); +// A generic lambda's body is a template pattern even in a non-template +// function: a non-dependent violation diagnoses at definition time whether or +// not the lambda is ever invoked, and an all-global shape is reused (not +// rebuilt) when the call operator is instantiated, so invoking does not +// repeat it. +void generic_lambda_never_invoked() { + auto l = [](auto x) { g_ptr_sink = &g_uninit; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)l; +} + +void generic_lambda_invoked() { + auto l = [](auto x) { g_ptr_sink = &g_uninit; }; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + l(1); +} + +// A late-parsed inline member of a class template is a pattern too: a +// non-dependent violation diagnoses when its body is parsed. Suppression on +// the method or on the class covers the definition-time fire like any other. +template +struct LateParsedMember { + void m() { g_ptr_sink = &g_uninit; } // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +}; + +template +struct LateParsedSuppressMethod { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] void m() { g_ptr_sink = &g_uninit; } // OK: suppressed +}; + +template +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] LateParsedSuppressClass { + void m() { g_ptr_sink = &g_uninit; } // OK: suppressed +}; + // std::init / uninit_read (paper §4.5): a read *through* a [[ref_to_uninit]] // pointer or reference yields an uninitialized value, diagnosed at the // lvalue-to-rvalue conversion (Sema::DefaultLvalueConversion). These reads fire @@ -992,8 +1096,8 @@ void test_element_read_byte_exempt() { (void)v; } -// Like the member read, the element read defers on a template pattern and -// fires once, at instantiation. +// The dependent element type makes the read instantiation-dependent, so it +// defers on the pattern and fires once, at instantiation. template void template_element_read_bad() { [[uninit]] T a[2]; @@ -1020,24 +1124,26 @@ struct HasAggMember { int get() { return agg.x; } // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} }; -// Like every Decl-less read check, the member read defers on a template -// pattern and fires once, at instantiation. +// Like every Decl-less check, the member read fires at definition time when +// its glvalue is non-dependent, and repeats when the read is rebuilt at +// instantiation (the local s is remapped) -- the accepted repetition. template void template_member_read_bad() { Pair s [[uninit]]; - int y = s.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + int y = s.x; // expected-error 2 {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} (void)y; } template void template_member_read_bad(); // expected-note {{in instantiation of function template specialization 'template_member_read_bad' requested here}} -// A read through a [[ref_to_uninit]] parameter inside a template body defers on -// the pattern (a dependent context) and fires once, at instantiation -- whether -// the read's operand is non-dependent (template_read_nondependent_bad) or -// dependent (template_read_dependent_bad). A never-instantiated template stays -// silent. Mirrors the binding template_* cases above. +// A read through a [[ref_to_uninit]] parameter inside a template body fires at +// definition time when the operand is non-dependent +// (template_read_nondependent_bad; the parameter remap rebuilds the read at +// instantiation, repeating the diagnostic) and defers to instantiation when it +// is dependent (template_read_dependent_bad). Mirrors the binding template_* +// cases above. template void template_read_nondependent_bad(int *p [[ref_to_uninit]]) { - int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y = *p; // expected-error 2 {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} (void)y; } template void template_read_nondependent_bad(int *); // expected-note {{in instantiation of function template specialization 'template_read_nondependent_bad' requested here}} @@ -1048,9 +1154,29 @@ T template_read_dependent_bad(T *p [[ref_to_uninit]]) { } template int template_read_dependent_bad(int *); // expected-note {{in instantiation of function template specialization 'template_read_dependent_bad' requested here}} +// A never-instantiated pattern diagnoses its non-dependent read at definition +// time, exactly once. template void template_read_never_instantiated(int *p [[ref_to_uninit]]) { - int y = *p; + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// An all-global read: the definition-time fire, plus a repeat when the +// initialization of the local y is rebuilt at instantiation. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] Pair g_uninit_pair; + +template +void template_global_read_bad() { + int y = g_uninit_pair.x; // expected-error 2 {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} +template void template_global_read_bad(); // expected-note {{in instantiation of function template specialization 'template_global_read_bad' requested here}} + +template +void template_global_read_never_instantiated() { + int y = g_uninit_pair.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} (void)y; } @@ -1081,11 +1207,12 @@ void test_compound_read_suppress(int *p [[ref_to_uninit]]) { [[profiles::suppress(std::init, rule: "uninit_read")]] { *p += 1; } // OK: rule-targeted suppress } -// Like every Decl-less read check, the compound read defers on a template -// pattern and fires once, at instantiation. +// Like every Decl-less read check, the compound read fires at definition time +// on a non-dependent operand and repeats when the parameter remap rebuilds the +// assignment at instantiation. template void template_compound_read_bad(int *p [[ref_to_uninit]]) { - *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p += 1; // expected-error 2 {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} } template void template_compound_read_bad(int *); // expected-note {{in instantiation of function template specialization 'template_compound_read_bad' requested here}} @@ -1289,18 +1416,19 @@ void test_aggregate_suppress() { } // Aggregate field init inside a template body is non-dependent here, so it -// defers on the pattern (a dependent context) and fires once at instantiation; -// a never-instantiated template stays silent. +// fires at definition time and repeats when the local variable's +// initialization is rebuilt at instantiation; a never-instantiated template +// diagnoses at definition, once. template void template_aggregate_bad() { - AggPtr a{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr a{&g_uninit}; // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)a; } template void template_aggregate_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_bad' requested here}} template void template_aggregate_never() { - AggPtr a{&g_uninit}; + AggPtr a{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)a; } @@ -1315,16 +1443,23 @@ void throw_ptr_suppressed() { [[profiles::suppress(std::init)]] { throw &g_uninit; } // OK: suppressed } -// A dependent thrown operand is rebuilt at instantiation, where the check -// fires. A fully non-dependent throw inside a template is not rebuilt by -// TreeTransform, so -- like the reinterpret_cast site -- the pattern defers -// and the case goes undiagnosed; a known limitation. +// A dependent thrown operand defers on the pattern and is rebuilt at +// instantiation, where the check fires per specialization. A fully +// non-dependent throw fires at definition time instead (see +// template_allglobal_bad): TreeTransform reuses it unchanged, so deferring +// would lose the diagnostic. template void template_throw_bad() { throw (T *)&g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } template void template_throw_bad(); // expected-note {{in instantiation of function template specialization 'template_throw_bad' requested here}} +template +void template_throw_nondependent_bad() { + throw &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_throw_nondependent_bad(); + // C++20 parenthesized aggregate initialization performs the same per-field // bindings as the braced form and is checked identically. void test_aggregate_paren() { @@ -1342,7 +1477,7 @@ void test_aggregate_paren() { template void template_aggregate_paren_bad() { - AggPtr a(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtr a(&g_uninit); // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)a; } template void template_aggregate_paren_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_paren_bad' requested here}} diff --git a/clang/test/SemaCXX/safety-profile-init-write.cpp b/clang/test/SemaCXX/safety-profile-init-write.cpp index 9a85d15579f6d..5c1bbc0a68e04 100644 --- a/clang/test/SemaCXX/safety-profile-init-write.cpp +++ b/clang/test/SemaCXX/safety-profile-init-write.cpp @@ -175,13 +175,15 @@ void test_suppress_decl() { s.x = 1; // OK: the function-level suppression covers the body } -// Like every Decl-less expression check, the store check defers on a template -// pattern and fires once, at instantiation; a never-instantiated template and -// a discarded if-constexpr branch stay silent. +// Like every Decl-less expression check, the store check fires at definition +// time when its target is non-dependent, and repeats when the local s is +// remapped and the assignment rebuilt at instantiation -- the accepted +// repetition. A dependent target (template_write_dependent_bad) defers on the +// pattern and fires once per violating specialization. template void template_write_bad() { Pair s [[uninit]]; - s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + s.x = 1; // expected-error 2 {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} } template void template_write_bad(); // expected-note {{in instantiation of function template specialization 'template_write_bad' requested here}} @@ -192,19 +194,66 @@ void template_write_dependent_bad() { } template void template_write_dependent_bad(); // expected-note {{in instantiation of function template specialization 'template_write_dependent_bad' requested here}} +// A never-instantiated pattern diagnoses its non-dependent store at +// definition time, exactly once. template void template_write_never_instantiated() { Pair s [[uninit]]; - s.x = 1; + s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} } +// A literal-false if-constexpr condition makes the then-branch a discarded +// statement context already at pattern parse, so its store stays silent; the +// live else-branch fires at definition time and repeats when the branch is +// rebuilt at instantiation. template void template_write_discarded_branch() { Pair s [[uninit]]; if constexpr (false) { s.x = 1; } else { - s.x = 2; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + s.x = 2; // expected-error 2 {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} } } template void template_write_discarded_branch(); // expected-note {{in instantiation of function template specialization 'template_write_discarded_branch' requested here}} + +// An all-global store is *reused* by TreeTransform at instantiation (its +// Build* never re-runs), so deferring would silently lose the diagnostic; it +// is checked at definition time -- exactly one error, with no instantiation +// note, whether or not the template is ever instantiated. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] Pair g_uninit_pair; + +template +void template_allglobal_write_bad() { + g_uninit_pair.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} +template void template_allglobal_write_bad(); + +template +void template_allglobal_write_never_instantiated() { + g_uninit_pair.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} +} + +// A store that violates two rules at one location -- the subobject write into +// the [[uninit]] object and the unmarked-pointer binding of its member -- +// fires both at definition time, and both repeat on the instantiation +// rebuild. +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "static_marker")]] [[uninit]] int g_uninit_int; +struct WithPtrMember { int x; int *p; }; + +template +void template_two_rules_bad() { + WithPtrMember s [[uninit]]; + s.p = &g_uninit_int; // expected-error 2 {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error 2 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} +template void template_two_rules_bad(); // expected-note {{in instantiation of function template specialization 'template_two_rules_bad' requested here}} + +template +void template_two_rules_never_instantiated() { + WithPtrMember s [[uninit]]; + s.p = &g_uninit_int; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ + // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} From fe243ec4469575ec4a1c4dcf555caec501a3be13 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Tue, 7 Jul 2026 20:07:35 -0400 Subject: [PATCH 229/289] Update stale aggregate-hook comments to the instantiation-dependence deferral --- clang/lib/Sema/SemaInit.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 353dfc372722b..0f298d3a1eb2f 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -1600,8 +1600,8 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, // variable/argument/return is left to its own site, which already // handles a braced source via the recognizer. No Decl is passed: the // init list can appear in a template independently of whether the - // aggregate is one, so deferral comes from the dependent-context - // guard in checkInitProfileRefToUninit and suppression from the + // aggregate is one, so checkInitProfileRefToUninit defers on an + // instantiation-dependent source and suppression comes from the // parse-time stack. (A reference field is routed to // CheckReferenceType above.) if (!Result.isInvalid() && @@ -1913,8 +1913,8 @@ void InitListChecker::CheckReferenceType(const InitializedEntity &Entity, // an NSDMI) has a null parent and is checked at its own Decl-aware site, so // the parent guard prevents a double diagnostic there. No Decl is passed: the // init list can appear in a template independently of whether the aggregate - // is one, so deferral comes from the dependent-context guard in - // checkInitProfileRefToUninit and suppression from the parse-time stack. + // is one, so checkInitProfileRefToUninit defers on an instantiation-dependent + // source and suppression comes from the parse-time stack. if (!VerifyOnly && !Result.isInvalid() && Entity.getKind() == InitializedEntity::EK_Member && Entity.getParent()) SemaRef.Profiles().checkInitProfileRefToUninitBinding(Src->getExprLoc(), From 13da5a2bb816cc8bf6d2428a0dfaeac09447ffc7 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 14:34:32 -0400 Subject: [PATCH 230/289] Check ref_to_uninit on the implicit object argument --- clang/docs/ProfilesFramework.rst | 30 ++-- .../clang/Basic/DiagnosticSemaKinds.td | 3 + clang/include/clang/Sema/SemaProfiles.h | 16 +++ clang/lib/Sema/SemaOverload.cpp | 6 + clang/lib/Sema/SemaProfiles.cpp | 31 ++++ .../safety-profile-init-ref-to-uninit.cpp | 135 ++++++++++++++++++ 6 files changed, 213 insertions(+), 8 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index ba3327db76622..be3e55374a826 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1157,7 +1157,18 @@ recognizers symmetric. a pointer argument is checked as an unmarked target, paper §7.2, while a promoted *value* read stays the read-through chokepoint's), return statements - (``Sema::BuildReturnStmt``), and lambda captures -- an init-capture binds + (``Sema::BuildReturnStmt``), implicit object arguments + (``Sema::PerformImplicitObjectArgumentInitialization``, the funnel every + member-call flavor's object argument converts through -- dot and arrow + calls, member operators including whole-object ``operator=``, functor + ``operator()``, ``operator->``, and conversion operators; the implicit + object parameter can never carry ``[[ref_to_uninit]]``, so a call on an + object recognized as uninitialized storage is checked as an unmarked + target, paper §7.2, with suppress as the escape -- an *explicit* object + member function instead initializes its object as an ordinary parameter, + which the parameter site above already owns and whose parameter can carry + the marker; a destructor call is skipped, destruction being the deferred + destroy_at slice), and lambda captures -- an init-capture binds like a variable initialization when its variable is created (``Sema::createLambdaInitCaptureVarDecl``), and a plain by-reference capture of an entity denoting uninitialized storage (an ``[[uninit]]`` variable, or @@ -1173,7 +1184,8 @@ recognizers symmetric. passes the enclosing constructor and is re-run by ``BuildMemberInitializer`` at instantiation, exactly like ``ctor_uninit_member``. The Decl-less call-argument, assignment, return, - aggregate-field, and capture sites instead defer only when the source (for + aggregate-field, object-argument, and capture sites instead defer only when + the source (for the capture, the captured variable's type; for pointer assignment, also the LHS) is *instantiation-dependent* -- such constructs are always rebuilt at instantiation, where the re-run ``Build*`` / ``InitListChecker`` routine @@ -1395,12 +1407,14 @@ initialization sequence. promotion, the rest through R7's operator-site hooks). - ``std::byte`` stores are exempt (paper §4.5), matching every read-side rule. -- Known gaps: whole-object assignment to a marked class object - (``s = S{...}``) goes through the overloaded ``operator=`` path and is not - checked -- class-type writes are uniformly deferred with construct_at -- - and writes through ``[[ref_to_uninit]]`` are deliberately out of scope (for - a scalar the write is the initialization; verifying class-type writes needs - construct_at modeling). +- Whole-object assignment to a marked class object (``s = S{...}``) diverts + to the overloaded ``operator=`` path and so never reaches this rule; it is + caught instead by the ``ref_to_uninit`` object-argument check (R7) when the + member ``operator=`` binds its implicit object parameter to the marked + object. Class-type writes remain uniformly deferred with construct_at. +- Known gaps: writes through ``[[ref_to_uninit]]`` are deliberately out of + scope (for a scalar the write is the initialization; verifying class-type + writes needs construct_at modeling). Diagnostic suppression ~~~~~~~~~~~~~~~~~~~~~~ diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 402c0acee9f6f..c42e208e349ac 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14203,4 +14203,7 @@ def err_init_uninit_subobject_write : ProfileRuleError< def err_init_uninit_ref_capture : ProfileRuleError< "capturing %1 by reference binds a reference to uninitialized memory " "under profile '%0'">; +def err_init_member_call_on_uninit : ProfileRuleError< + "calling member function %1 binds its implicit object parameter to " + "uninitialized memory under profile '%0'">; } // end of sema component. diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index b3da925b95c72..b3d094664cec9 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -304,6 +304,22 @@ class SemaProfiles : public SemaBase { /// (accepted). void checkInitProfileRefCapture(SourceLocation Loc, const ValueDecl *Var); + /// std::init / ref_to_uninit (paper §7.2): a member call binds its implicit + /// object parameter to \p Object, and that parameter can never carry + /// [[ref_to_uninit]], so a call on an object recognized as uninitialized + /// storage is always the unmarked-direction violation. Called from + /// \c Sema::PerformImplicitObjectArgumentInitialization, the funnel every + /// member-call flavor's object argument converts through -- dot and arrow + /// calls, member operators, functor operator(), operator->, and conversion + /// operators. Explicit-object member functions initialize their object as an + /// ordinary parameter and are already checked there; a destructor call is + /// skipped (destruction of uninitialized storage is the deferred destroy_at + /// slice). Defers only on an instantiation-dependent \p Object -- the call + /// is rebuilt at instantiation, re-running the funnel -- and otherwise fires + /// at definition time, repeating if the call is rebuilt anyway (accepted). + void checkInitProfileObjectArgument(const Expr *Object, + const CXXMethodDecl *Method); + /// std::init / ref_to_uninit (paper §4.3): assigning to a pointer must /// respect the assigned-to pointer's [[ref_to_uninit]] marking; a no-op for /// a non-pointer LHS. Hosts the cluster from Sema::CreateBuiltinBinOp's diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp index f37e26e88268e..763ad2c5f7e76 100644 --- a/clang/lib/Sema/SemaOverload.cpp +++ b/clang/lib/Sema/SemaOverload.cpp @@ -6317,6 +6317,12 @@ ExprResult Sema::PerformImplicitObjectArgumentInitialization( << From->getSourceRange(); } + // std::init / ref_to_uninit (paper §7.2): the implicit object parameter can + // never carry [[ref_to_uninit]], so a member call on an object recognized + // as uninitialized storage is the unmarked-direction violation. + if (getLangOpts().Profiles) + Profiles().checkInitProfileObjectArgument(From, Method); + if (ICS.Standard.Second == ICK_Derived_To_Base) { ExprResult FromRes = PerformObjectMemberConversion(From, Qualifier, FoundDecl, Method); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 7d1183fdbe410..ea25640b37578 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1145,6 +1145,37 @@ void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, Diag(Loc, diag::err_init_uninit_ref_capture) << "std::init" << Var; } +void SemaProfiles::checkInitProfileObjectArgument(const Expr *Object, + const CXXMethodDecl *Method) { + // A RecoveryExpr is a placeholder for an expression that already failed, not + // an object argument the user wrote, so it must not drive this rule. + if (!getLangOpts().Profiles || !Object || + isa(Object->IgnoreParens())) + return; + // Destroying uninitialized storage is the deferred destroy_at slice (the + // paper models destruction, like construct_at, as a lifetime operation); + // implicit scope-exit destructions never reach this funnel, so diagnosing + // only the explicit s.~S() spelling would be an inconsistent sliver. + if (isa(Method)) + return; + // An instantiation-dependent object argument cannot be classified yet; its + // call is always rebuilt at instantiation, re-running this funnel with the + // substituted object. A non-dependent call fires at definition time and + // repeats if the call is rebuilt at instantiation anyway -- accepted. + if (Object->isInstantiationDependent()) + return; + if (!shouldEmitProfileViolation("std::init", "ref_to_uninit", + Object->getExprLoc())) + return; + // An arrow call's object argument arrives as the pointer expression, a dot + // call's as the object glvalue; dispatch the recognizer accordingly. + bool IsPointer = Object->getType()->isPointerType(); + if (!refersToUninitializedMemory(Object, /*IsReference=*/!IsPointer)) + return; + Diag(Object->getExprLoc(), diag::err_init_member_call_on_uninit) + << "std::init" << Method; +} + // The read-through diagnostic distinguishes indirection through a // [[ref_to_uninit]] pointer/reference from a subobject read of a named // [[uninit]] object, which involves no [[ref_to_uninit]] entity. Approximate diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 37c3e6b2d36e8..f884ab5520a1a 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1562,3 +1562,138 @@ void test_unknown_regression_guard() { int *u1 = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} (void)m1; (void)m2; (void)u1; } + +// A member call binds its implicit object parameter to the object argument +// (paper §7.2), and that parameter can never carry [[ref_to_uninit]], so a +// call on an object recognized as uninitialized storage is always the +// unmarked-direction violation. Every member-call flavor converts its object +// argument through the same funnel: dot and arrow calls, member operators, +// functor operator(), operator->, and conversion operators. +struct Callee { + int m; + int f() { return m; } + static int sf() { return 0; } + Callee &operator=(const Callee &); + bool operator==(const Callee &) const; + operator int() const; + int *operator->(); + int operator()(int); +}; + +void test_member_call_on_uninit_object() { + Callee s [[uninit]]; + s.f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (&s)->f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +void test_member_call_through_marked_pointer(Callee *p [[ref_to_uninit]]) { + p->f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (*p).f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +// Member operators bind the same implicit object parameter, so whole-object +// assignment to an [[uninit]] class object -- previously unchecked through +// the overloaded operator= path -- is caught here too. +void test_member_operators_on_uninit_object() { + Callee s [[uninit]]; + Callee t{}; + s = t; // expected-error {{calling member function 'operator=' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (void)(s == t); // expected-error {{calling member function 'operator==' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + int v = s; // expected-error {{calling member function 'operator int' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + s(1); // expected-error {{calling member function 'operator()' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + (void)v; +} + +void test_member_arrow_on_uninit_object() { + Callee s [[uninit]]; + (void)*(s.operator->()); // expected-error {{calling member function 'operator->' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +// A [[ref_to_uninit]]-returning reference function yields an uninitialized +// referent; calling a member function on it is the same violation. +[[ref_to_uninit]] Callee &get_uninit_callee(); +void test_member_call_on_marked_call_result() { + get_uninit_callee().f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +Callee make_callee(); +struct WithDtor { + int m; + void g(); + ~WithDtor(); +}; + +void test_member_call_silent_forms() { + Callee s [[uninit]]; + s.sf(); // OK: a static member uses no object argument + (void)sizeof(s.f()); // OK: unevaluated + Callee t{}; + t.f(); // OK: initialized object + Callee{}.f(); // OK: a prvalue object is not uninitialized storage + make_callee().f(); // OK: unmarked call result is trusted initialized + WithDtor d [[uninit]]; + d.~WithDtor(); // OK: destruction is the deferred destroy_at slice +} + +// The object itself being unmarked keeps the trust decision: a class whose +// *member* is [[uninit]] may still have its member functions called (its +// constructor body may have assigned the member, paper §5.1/§5.2). +struct MemberOnlyUninit { + int m [[uninit]]; + MemberOnlyUninit() { m = 1; } + int get() { return m; } +}; +void test_member_call_unmarked_object_trusted(MemberOnlyUninit &r) { + MemberOnlyUninit o; + o.get(); // OK + r.get(); // OK: unknown-state reference parameter is not affirmatively uninit +} + +// An explicit object member function initializes its object as an ordinary +// parameter, so the existing parameter binding check owns it (and its +// parameter *could* carry the marker). +struct ExplicitObj { + int m; + void f(this ExplicitObj &self); +}; +void test_member_call_explicit_object() { + ExplicitObj x [[uninit]]; + x.f(); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A member with enable_if converts availability-check arguments under a +// SFINAE trap; the real call still diagnoses exactly once. +struct WithEnableIf { + int m; + void f() __attribute__((enable_if(true, ""))); +}; +void test_member_call_enable_if() { + WithEnableIf s [[uninit]]; + s.f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +void test_member_call_suppressed() { + Callee s [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { s.f(); } // OK: suppressed + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] { s.f(); } // OK +} + +// A dependent object argument defers to instantiation, where the rebuilt call +// re-runs the funnel; a non-dependent call in a template fires at definition +// time and repeats when the call is rebuilt at instantiation (the local is +// remapped) -- the accepted repetition. +template +void template_member_call_dependent_bad() { + T s [[uninit]]; + s.f(); // expected-error {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} +template void template_member_call_dependent_bad(); // expected-note {{in instantiation of function template specialization 'template_member_call_dependent_bad' requested here}} + +template +void template_member_call_nondependent_bad() { + Callee s [[uninit]]; + s.f(); // expected-error 2 {{calling member function 'f' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} +template void template_member_call_nondependent_bad(); // expected-note {{in instantiation of function template specialization 'template_member_call_nondependent_bad' requested here}} From d8e68414d8f5f32216bf1e30814eba21d8ace419 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 14:38:03 -0400 Subject: [PATCH 231/289] Extract the ctor-body pass's tracked-member collection into shared helpers --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 126 +++++++++++++---------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 01373f92abf19..3c849b1e4dde0 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1729,6 +1729,63 @@ static const FieldDecl *getCurrentObjectMember(const Expr *E) { return dyn_cast(ME->getMemberDecl()); } +// A class with a user-provided constructor is trusted (paper §5.1): its +// constructor body may have assigned a member, which local analysis cannot +// see, so its members are not flow-tracked. +static bool hasUserProvidedCtor(const CXXRecordDecl *RD) { + return llvm::any_of(RD->ctors(), [](const CXXConstructorDecl *C) { + return C->isUserProvided(); + }); +} + +// Visit the candidate fields of RD and of its non-virtual, constructor-less +// base classes, recursively. +template +static void forEachCandidateUninitField(const CXXRecordDecl *RD, Fn Visit) { + SmallVector RecordStack(1, RD); + while (!RecordStack.empty()) { + const CXXRecordDecl *Cur = RecordStack.pop_back_val(); + for (const FieldDecl *F : Cur->fields()) + Visit(F); + for (const CXXBaseSpecifier &BS : Cur->bases()) { + if (BS.isVirtual()) + continue; + const CXXRecordDecl *BRD = BS.getType()->getAsCXXRecordDecl(); + if (BRD && BRD->hasDefinition() && + !hasUserProvidedCtor(BRD->getDefinition())) + RecordStack.push_back(BRD->getDefinition()); + } + } +} + +// Collect the flow-trackable members of RD into Members/Index: [[uninit]] +// built-in scalar (arithmetic or enum) members whose assignment counts as +// initialization (§4.5), including those inherited from non-virtual, +// constructor-less bases -- nothing can have assigned them before the +// containing object's user code runs, so tracking them is sound. std::byte is +// exempt (§4.5), matching R1/R2. Class-type and array members (which would +// need construct_at flow modeling) and pointers (banned with [[uninit]] by R8) +// are out of scope. +static void collectTrackedUninitMembers( + Sema &S, const CXXRecordDecl *RD, + SmallVectorImpl &Members, + llvm::DenseMap &Index) { + forEachCandidateUninitField(RD, [&](const FieldDecl *F) { + if (!F->hasAttr() || !F->getDeclName() || + F->hasInClassInitializer()) + return; + QualType T = F->getType(); + if (!T->isIntegralOrEnumerationType() && !T->isFloatingType()) + return; + if (S.Context.getBaseElementType(T)->isStdByteType()) + return; + if (Index.count(F)) + return; + Index[F] = Members.size(); + Members.push_back(F); + }); +} + // std::init constructor-body check (paper §7.1 "initialized ... before use"). // // A [[uninit]] scalar data member is deliberately *not* required to be @@ -1759,58 +1816,12 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, if (!cfg) return; - // Target members: [[uninit]] built-in scalar (arithmetic or enum) members - // whose assignment counts as initialization (§4.5). std::byte is exempt - // (§4.5), matching R1/R2. Class-type and array members (which would need - // construct_at flow modeling) and pointers (banned with [[uninit]] by R8) are - // out of scope here. - // - // Members inherited from a non-virtual base with NO user-provided - // constructor are tracked too: nothing can have assigned them before the - // derived body runs, so tracking them is sound. A base with a user-provided - // constructor is trusted (paper §5.1) -- its constructor body may have - // assigned the member, which this local analysis cannot see -- and its - // members are left alone. - auto HasUserProvidedCtor = [](const CXXRecordDecl *RD) { - return llvm::any_of(RD->ctors(), [](const CXXConstructorDecl *C) { - return C->isUserProvided(); - }); - }; - // Visit the candidate fields of RD and of its non-virtual, constructor-less - // base classes, recursively. - auto ForEachCandidateField = [&](const CXXRecordDecl *RD, auto &&Visit) { - SmallVector RecordStack(1, RD); - while (!RecordStack.empty()) { - const CXXRecordDecl *Cur = RecordStack.pop_back_val(); - for (const FieldDecl *F : Cur->fields()) - Visit(F); - for (const CXXBaseSpecifier &BS : Cur->bases()) { - if (BS.isVirtual()) - continue; - const CXXRecordDecl *BRD = BS.getType()->getAsCXXRecordDecl(); - if (BRD && BRD->hasDefinition() && - !HasUserProvidedCtor(BRD->getDefinition())) - RecordStack.push_back(BRD->getDefinition()); - } - } - }; - + // Target members: the shared flow-trackable filter (a base with a + // user-provided constructor is trusted per paper §5.1; nothing can have + // assigned a constructor-less base's members before this body runs). SmallVector Members; llvm::DenseMap Index; - ForEachCandidateField(Ctor->getParent(), [&](const FieldDecl *F) { - if (!F->hasAttr() || !F->getDeclName() || - F->hasInClassInitializer()) - return; - QualType T = F->getType(); - if (!T->isIntegralOrEnumerationType() && !T->isFloatingType()) - return; - if (S.Context.getBaseElementType(T)->isStdByteType()) - return; - if (Index.count(F)) - return; - Index[F] = Members.size(); - Members.push_back(F); - }); + collectTrackedUninitMembers(S, Ctor->getParent(), Members, Index); if (Members.empty()) return; const unsigned N = Members.size(); @@ -1880,13 +1891,14 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, const auto *BRD = CI->getBaseClass()->getAsCXXRecordDecl(); if (!BRD || !BRD->hasDefinition()) continue; - ForEachCandidateField(BRD->getDefinition(), [&](const FieldDecl *F) { - auto It = Index.find(F); - if (It == Index.end()) - return; - BlockEvents.push_back({Write, It->second, CI->getInit()}); - Gen[B->getBlockID()].set(It->second); - }); + forEachCandidateUninitField( + BRD->getDefinition(), [&](const FieldDecl *F) { + auto It = Index.find(F); + if (It == Index.end()) + return; + BlockEvents.push_back({Write, It->second, CI->getInit()}); + Gen[B->getBlockID()].set(It->second); + }); } continue; } From ead2a733f814dfb1674d81a4eef94b52f2f8c7f2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 14:53:16 -0400 Subject: [PATCH 232/289] Track [[uninit]] members of constructor-less aggregate locals --- clang/docs/ProfilesFramework.rst | 56 ++- clang/lib/Sema/AnalysisBasedWarnings.cpp | 340 +++++++++++++++++- clang/lib/Sema/SemaProfiles.cpp | 33 +- .../safety-profile-init-local-member-read.cpp | 243 +++++++++++++ 4 files changed, 644 insertions(+), 28 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-init-local-member-read.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index be3e55374a826..e28ccaa8f2ce9 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -936,6 +936,47 @@ read-then-write of that member. Details: (``ref_to_uninit``) territory and is treated as neither a read nor an initialization by this pass. +The same guarantee covers an ``[[uninit]]`` scalar member of a +*constructor-less aggregate local* (the paper §5.3 "class exposing +uninitialized members" pattern used with a local: ``struct Agg { int m +[[uninit]]; }; Agg a; a.m = 5;``). ``checkInitProfileLocalMembers`` in +``clang/lib/Sema/AnalysisBasedWarnings.cpp`` -- the local-variable analog of +the ctor-body pass, sharing its tracked-member filter, event/replay shape, +suppression lookup, and post-error rerun -- runs the same forward +definite-assignment dataflow over *every* function definition's CFG. This is +the flow tracking that lets the parse-time read-through preset (R7) drop the +top-level member marker without losing the read-before-write diagnosis. +Details: + +- Tracked pairs are (local variable, member): an automatic-storage, + non-parameter, non-reference local whose class -- with any base subtree + contributing tracked members -- has **no user-provided constructor** (one + is trusted per paper §5.1: its body may have assigned the member, which + local analysis cannot see) and whose declaration is the bare ``Agg a;`` + form. Any written initialization (``Agg a{}``, ``= {}``, ``= Agg()``, a + copy) gives every member a value and leaves nothing to track, as does a + local that is itself ``[[uninit]]``-marked -- its subobject accesses are the + parse-time read-through / ``uninit_write`` rules' territory. +- A plain member store ``a.m = e`` assigns the member (§4.5); a compound + assignment and a built-in ``++``/``--`` read the old value first and are a + read-then-write, exactly as in the ctor-body pass. +- Soundness over completeness: **any** other appearance of the variable -- + ``&a``, ``&a.m``, a reference binding, passing ``a`` to any function + (``construct_at``, ``memcpy``), a member call, a lambda capture -- + conservatively marks every tracked member assigned from that point (the + address may be used to initialize the object), so no legal program is + rejected. A backward ``goto`` across the declaration re-default-initializes + the object, which the gen-only dataflow cannot model -- a possible missed + diagnostic, matching the ctor-body pass's accepted imprecision level. +- Objects reached through parameters, references, or other objects are not + tracked; with the user-provided-constructor trust above this makes the + remaining "``uu.y`` read through another object" case a deliberate, + documented trust decision (see the read-through preset under R7). +- Reports reuse ``err_init_member_read_before_init`` under the shared + ``uninit_read`` rule, so ``[[profiles::suppress(std::init, rule: + "uninit_read")]]`` at the read site covers it and the ``std::byte`` + exemption applies (a ``std::byte`` member is never tracked). + R2. ``uninit_decl`` -- pattern 1 ................................. @@ -1218,10 +1259,17 @@ recognizers symmetric. (this rule), and a scalar *store* (``uninit_write``, R10, which shares the drop and additionally trusts the ``[[ref_to_uninit]]`` arms). The read preset drops the ``[[uninit]]`` marker only for the - *top-level* named entity -- a directly named ``[[uninit]]`` object and a - current-object member are flow-tracked (by the CFG ``uninit_read`` pass and - the ctor-body pass, which credit assignments), so their direct reads are left - to those passes. A *subobject* read of a named ``[[uninit]]`` object + *top-level* named entity, whose direct reads are owned three ways: a + directly named ``[[uninit]]`` object is flow-tracked by the CFG + ``uninit_read`` pass, a current-object ``[[uninit]]`` member by the + ctor-body pass, and an ``[[uninit]]`` member of a constructor-less + aggregate local by the local-aggregate pass (all three credit assignments; + see R1). A marked member of an object with a *user-provided* constructor + reached through any other object (``uu.y`` after ``Slot uu;``) is instead + **trusted**, deliberately: the constructor's body may have assigned the + member (the §5.2 pattern), which local analysis cannot see, so paper §5.1's + trust-the-constructor principle applies and its reads are not diagnosed + anywhere. A *subobject* read of a named ``[[uninit]]`` object (``s.x``, ``o.agg.f``) or array (``a[0]``, ``*a``, ``s.a[i]``) is recognized and diagnosed here: neither flow pass tracks members or array elements, and subobject-wise delayed initialization of an ``[[uninit]]`` object is itself diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 3c849b1e4dde0..7d4faff289305 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2068,6 +2068,321 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, } } +// If E (stripped of parens and implicit casts, including the derived-to-base +// cast of an inherited-member access) is a member access `V.m` on a directly +// named variable, return the base DeclRefExpr and the field through \p F. +// An arrow access or an access through any other expression is not a tracked +// local's member. +static const DeclRefExpr *getLocalMemberAccess(const Expr *E, + const FieldDecl *&F) { + const auto *ME = dyn_cast(E->IgnoreParenImpCasts()); + if (!ME || ME->isArrow()) + return nullptr; + const auto *FD = dyn_cast(ME->getMemberDecl()); + if (!FD) + return nullptr; + const auto *DRE = dyn_cast(ME->getBase()->IgnoreParenImpCasts()); + if (!DRE) + return nullptr; + F = FD; + return DRE; +} + +// If V is a local whose [[uninit]] members this pass may soundly flow-track, +// return its class definition; null otherwise. Sound means nothing can have +// assigned the members before V's declaration: the class (and any base +// subtree contributing tracked members) has no user-provided constructor +// (paper §5.1 trusts one -- its body may assign, which local analysis cannot +// see), and the declaration ran nothing but the implicit no-op +// default-construction. A local that is itself [[uninit]]-marked is excluded: +// its subobject accesses are the parse-time read-through / uninit_write +// rules' territory, and tracking it here would double-diagnose. +static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { + if (!V->hasLocalStorage() || isa(V) || isa(V)) + return nullptr; + if (V->isInvalidDecl() || V->hasAttr()) + return nullptr; + QualType T = V->getType(); + if (T->isReferenceType() || T->isDependentType()) + return nullptr; + const CXXRecordDecl *RD = T->getAsCXXRecordDecl(); + if (!RD || !RD->hasDefinition()) + return nullptr; + RD = RD->getDefinition(); + if (RD->isUnion() || RD->isDependentType() || hasUserProvidedCtor(RD)) + return nullptr; + // Declared without a real initializer: for a record local that is the + // synthesized call to the implicit default constructor (`Agg a;`). Any + // written form -- `Agg a{}` / `= {}` (an InitListExpr), `Agg a = Agg()` + // (a CXXTemporaryObjectExpr), a copy (a non-default constructor) -- gives + // every member a value and leaves nothing to track. + if (const Expr *Init = V->getInit()) { + const auto *CCE = dyn_cast(Init->IgnoreImplicit()); + if (!CCE || isa(CCE) || + !CCE->getConstructor()->isDefaultConstructor() || + CCE->isListInitialization() || CCE->getParenOrBraceRange().isValid()) + return nullptr; + } + return RD; +} + +// std::init local-aggregate member check (paper §7.1 "initialized ... before +// use", the local-variable analog of the ctor-body pass above). +// +// An [[uninit]] scalar member of a constructor-less aggregate local is given +// a value by a plain member store (`a.m = e`; for a built-in type a write is +// its initialization, §4.5) -- the §5.3 "class exposing uninitialized +// members" pattern. A read of such a member before it is definitely assigned +// (on every path, §1.3) is the violation. This is the flow tracking the +// parse-time read-through rule's top-level drop relies on for locals: the +// drop trusts a direct member read so the legal write-then-read sequence is +// not rejected, and this pass supplies the missing read-before-write +// diagnosis. +// +// Soundness over completeness: any appearance of the variable outside a +// recognized member read or write -- &a, &a.m, a reference binding, passing a +// to any function (construct_at, memcpy), a member call, a lambda capture -- +// conservatively marks every member assigned from that point (the address may +// be used to initialize the object). Members of an object with a +// user-provided constructor stay untracked (trusted, §5.1), as do objects +// reached through parameters, references, or other objects. A backward goto +// across the declaration re-default-initializes the object, which the +// gen-only dataflow cannot model -- a possible missed diagnostic, matching +// the ctor-body pass's accepted imprecision level. +static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { + CFG *cfg = AC.getCFG(); + if (!cfg) + return; + + // Harvest tracked (local, member) pairs from the CFG's (single-decl) + // DeclStmt elements; each variable's pairs are contiguous so an escape can + // set a range. + SmallVector PairField; + llvm::DenseMap FieldIdxScratch; + llvm::DenseMap> VarRange; + llvm::DenseMap, unsigned> + PairIdx; + for (const CFGBlock *B : *cfg) { + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const auto *DS = dyn_cast(CS->getStmt()); + if (!DS) + continue; + for (const Decl *Dcl : DS->decls()) { + const auto *V = dyn_cast(Dcl); + if (!V || VarRange.count(V)) + continue; + const CXXRecordDecl *RD = getTrackedLocalAggregate(V); + if (!RD) + continue; + SmallVector Members; + FieldIdxScratch.clear(); + collectTrackedUninitMembers(S, RD, Members, FieldIdxScratch); + if (Members.empty()) + continue; + unsigned Begin = PairField.size(); + for (const FieldDecl *F : Members) { + PairIdx[{V, F}] = PairField.size(); + PairField.push_back(F); + } + VarRange[V] = {Begin, PairField.size()}; + } + } + } + if (PairField.empty()) + return; + const unsigned N = PairField.size(); + + // The tracked pair index for E when it is a `V.m` access on a tracked + // local; ~0u otherwise. \p BaseOut receives the access's base DeclRefExpr. + auto LookupPair = [&](const Expr *E, + const DeclRefExpr *&BaseOut) -> unsigned { + const FieldDecl *F = nullptr; + const DeclRefExpr *DRE = getLocalMemberAccess(E, F); + if (!DRE) + return ~0u; + const auto *V = dyn_cast(DRE->getDecl()); + if (!V) + return ~0u; + auto It = PairIdx.find({V, F}); + if (It == PairIdx.end()) + return ~0u; + BaseOut = DRE; + return It->second; + }; + + // First pass: the base DeclRefExprs consumed by a recognized member read or + // write are benign -- they do not escape the object. A DeclRefExpr element + // precedes its consuming cast/operator element in a block, so the benign + // set must be complete before elements are classified. + llvm::SmallPtrSet Benign; + for (const CFGBlock *B : *cfg) { + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const Stmt *St = CS->getStmt(); + const DeclRefExpr *Base = nullptr; + if (const auto *ICE = dyn_cast(St)) { + if (ICE->getCastKind() == CK_LValueToRValue) + LookupPair(ICE->getSubExpr(), Base); + } else if (const auto *BO = dyn_cast(St)) { + if (BO->isAssignmentOp()) + LookupPair(BO->getLHS(), Base); + } else if (const auto *UO = dyn_cast(St)) { + if (UO->isIncrementDecrementOp()) + LookupPair(UO->getSubExpr(), Base); + } + if (Base) + Benign.insert(Base); + } + } + + // Second pass: per-block ordered events. A load of `V.m` is a Read; a + // member store is a Write (a compound assignment or ++/-- reads the old + // value first); any non-benign DeclRefExpr naming a tracked local is an + // escape, modeled as a Write of every one of its tracked members. + enum EventKind { Read, Write, ReadWrite }; + struct Event { + EventKind Kind; + unsigned Idx; + const Expr *E; + }; + const unsigned NumBlocks = cfg->getNumBlockIDs(); + std::vector> Events(NumBlocks); + std::vector Gen(NumBlocks, llvm::BitVector(N, false)); + for (const CFGBlock *B : *cfg) { + auto &BlockEvents = Events[B->getBlockID()]; + auto &BlockGen = Gen[B->getBlockID()]; + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const Stmt *St = CS->getStmt(); + const DeclRefExpr *Base = nullptr; + if (const auto *ICE = dyn_cast(St)) { + if (ICE->getCastKind() != CK_LValueToRValue) + continue; + unsigned Idx = LookupPair(ICE->getSubExpr(), Base); + if (Idx != ~0u) + BlockEvents.push_back({Read, Idx, ICE}); + } else if (const auto *BO = dyn_cast(St)) { + if (!BO->isAssignmentOp()) + continue; + unsigned Idx = LookupPair(BO->getLHS(), Base); + if (Idx == ~0u) + continue; + BlockEvents.push_back( + {BO->isCompoundAssignmentOp() ? ReadWrite : Write, Idx, BO}); + BlockGen.set(Idx); + } else if (const auto *UO = dyn_cast(St)) { + if (!UO->isIncrementDecrementOp()) + continue; + unsigned Idx = LookupPair(UO->getSubExpr(), Base); + if (Idx == ~0u) + continue; + BlockEvents.push_back({ReadWrite, Idx, UO}); + BlockGen.set(Idx); + } else if (const auto *DRE = dyn_cast(St)) { + if (Benign.count(DRE)) + continue; + const auto *V = dyn_cast(DRE->getDecl()); + if (!V) + continue; + auto It = VarRange.find(V); + if (It == VarRange.end()) + continue; + for (unsigned Idx = It->second.first; Idx != It->second.second; ++Idx) { + BlockEvents.push_back({Write, Idx, DRE}); + BlockGen.set(Idx); + } + } + } + } + + // Forward "definitely assigned" dataflow, identical in shape to the + // ctor-body pass's: nothing is assigned at function entry (a tracked local + // cannot be referenced before its DeclStmt anyway); a block's entry is the + // intersection over its predecessors' exits; unprocessed (unreachable) + // predecessors keep the all-assigned top, so unreachable code is never + // flagged. + std::vector EntryState(NumBlocks, llvm::BitVector(N, true)); + std::vector ExitState(NumBlocks, llvm::BitVector(N, true)); + const CFGBlock &CFGEntry = cfg->getEntry(); + ForwardDataflowWorklist Worklist(*cfg, AC); + Worklist.enqueueBlock(&CFGEntry); + while (const CFGBlock *B = Worklist.dequeue()) { + llvm::BitVector In(N, true); + if (B == &CFGEntry) { + In = llvm::BitVector(N, false); + } else { + bool First = true; + for (const CFGBlock *Pred : B->preds()) { + if (!Pred) + continue; + if (First) { + In = ExitState[Pred->getBlockID()]; + First = false; + } else { + In &= ExitState[Pred->getBlockID()]; + } + } + } + EntryState[B->getBlockID()] = In; + In |= Gen[B->getBlockID()]; + if (In != ExitState[B->getBlockID()]) { + ExitState[B->getBlockID()] = In; + Worklist.enqueueSuccessors(B); + } + } + + // Replay each block from its fixpoint entry state and collect reads of a + // member that is not yet definitely assigned at that point. + std::vector> Offending(N); + for (const CFGBlock *B : *cfg) { + llvm::BitVector Assigned = EntryState[B->getBlockID()]; + for (const Event &Ev : Events[B->getBlockID()]) { + switch (Ev.Kind) { + case Read: + if (!Assigned.test(Ev.Idx)) + Offending[Ev.Idx].push_back(Ev.E); + break; + case Write: + Assigned.set(Ev.Idx); + break; + case ReadWrite: + if (!Assigned.test(Ev.Idx)) + Offending[Ev.Idx].push_back(Ev.E); + Assigned.set(Ev.Idx); + break; + } + } + } + + // Report at the first offending read (in source order) that is not + // suppressed, once per (local, member) pair, mirroring the ctor-body pass. + for (unsigned I = 0; I != N; ++I) { + if (Offending[I].empty()) + continue; + llvm::sort(Offending[I], [&](const Expr *A, const Expr *Bx) { + return S.SourceMgr.isBeforeInTranslationUnit(A->getBeginLoc(), + Bx->getBeginLoc()); + }); + for (const Expr *R : Offending[I]) { + if (!S.Profiles().shouldEmitProfileViolation("std::init", "uninit_read", + R, AC)) + continue; + S.Diag(R->getBeginLoc(), diag::err_init_member_read_before_init) + << "std::init" << PairField[I]->getDeclName(); + S.Diag(PairField[I]->getLocation(), diag::note_init_uninit_member_here) + << PairField[I]->getDeclName(); + break; + } + } +} + class UninitValsDiagReporter : public UninitVariablesHandler { Sema &S; AnalysisDeclContext &AC; @@ -3292,15 +3607,18 @@ static void addNonLinearizedAlwaysAddClasses(AnalysisDeclContext &AC) { .setAlwaysAdd(Stmt::LambdaExprClass); } -// std::init: diagnose a read of a [[uninit]] scalar member before it is -// assigned in the constructor body. Shared by the normal per-function pass -// and the post-error rerun so both paths stay in step. -static void runCtorBodyInitCheckIfEnforced(Sema &S, const Decl *D, - AnalysisDeclContext &AC) { +// std::init: the CFG-based std::init checks -- the constructor-body +// read-before-init check and the local-aggregate member check (which runs +// for every definition, constructors included: the two track disjoint +// storage, this-members versus locals). Shared by the normal per-function +// pass and the post-error rerun so both paths stay in step. +static void runInitProfileCFGChecksIfEnforced(Sema &S, const Decl *D, + AnalysisDeclContext &AC) { if (!S.Profiles().isProfileEnforced("std::init")) return; if (const auto *Ctor = dyn_cast(D)) checkInitProfileCtorBody(S, Ctor, AC); + checkInitProfileLocalMembers(S, AC); } // Pattern-2 profiles (the CFGUninitProfiles table) ride the uninitialized- @@ -3320,9 +3638,9 @@ static void runUninitProfileAnalysisAfterError(Sema &S, const Decl *D) { UninitVariablesAnalysisStats stats = {}; runUninitializedVariablesAnalysis(*cast(D), *cfg, AC, reporter, stats); - // Keep the constructor-body read-before-init check alive after a TU error, - // for parity with the normal path. - runCtorBodyInitCheckIfEnforced(S, D, AC); + // Keep the read-before-init checks (constructor members and local + // aggregates) alive after a TU error, for parity with the normal path. + runInitProfileCFGChecksIfEnforced(S, D, AC); } } @@ -3831,8 +4149,10 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings( } // std::init: diagnose a read of a [[uninit]] scalar member before it is - // assigned in the constructor body, reusing the CFG built above. - runCtorBodyInitCheckIfEnforced(S, D, AC); + // assigned -- in the constructor body for the current object's members, in + // any body for a constructor-less aggregate local's -- reusing the CFG + // built above. + runInitProfileCFGChecksIfEnforced(S, D, AC); // TODO: Enable lifetime safety analysis for other languages once it is // stable. diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index ea25640b37578..7c6d4f22794de 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -802,14 +802,19 @@ enum class UninitStorage { Initialized, Uninitialized, Unknown }; // How an expression is being used, for the uninit recognizers. // // DropTopLevelUninit: a *directly named* [[uninit]] entity does not count as -// uninitialized. A value access of such an entity is the flow-based passes' -// responsibility (the CFG uninit_read pass and the ctor-body pass credit -// assignments), so the read-through check must not second-guess them -- and -// for a store, writing the whole named entity IS its initialization (paper -// §4.5: for a built-in type, a write is its initialization). The flag is -// cleared at the first subobject step (member access, array element), where -// no flow pass tracks the storage and only whole-object construct_at could -// re-initialize (paper §5.4). +// uninitialized. A value access of such an entity is owned elsewhere: a named +// [[uninit]] object by the CFG uninit_read pass, a current-object member by +// the ctor-body pass, an [[uninit]] member of a constructor-less aggregate +// local by the local-aggregate pass (all three credit assignments), and a +// marked member of an object with a user-provided constructor reached through +// any other object is deliberately trusted (paper §5.1: its constructor body +// may have assigned it, which local analysis cannot see). So the read-through +// check must not second-guess them -- and for a store, writing the whole +// named entity IS its initialization (paper §4.5: for a built-in type, a +// write is its initialization). The flag is cleared at the first *deeper* +// subobject step (a member's member, an array element), where no flow pass +// tracks the storage and only whole-object construct_at could re-initialize +// (paper §5.4). // // TrustRefToUninit: [[ref_to_uninit]] markers are ignored -- the storage // reached through a marked pointer/reference (or returned by a marked @@ -1006,12 +1011,12 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, // a->m reaches m through the pointer a (object *a); a.m through the // glvalue a. When m does not itself denote uninit storage, the subobject is // uninit exactly when its base is. The base recursion clears the top-level - // drop: the drop exists because a *directly named* [[uninit]] object and - // a current-object member are flow-tracked (by the CFG uninit_read pass - // and the ctor-body pass, which credit assignments), but neither pass - // tracks a subobject reached through a member access -- and member-wise - // delayed initialization of an [[uninit]] object is itself banned (paper - // §5.4; only whole-object construct_at re-initializes, which is uniformly + // drop: the drop exists because a directly named [[uninit]] entity's value + // accesses are owned by the flow passes or deliberately trusted (see the + // UninitAccessOpts comment above), but nothing tracks a subobject reached + // through a *further* member access -- and member-wise delayed + // initialization of an [[uninit]] object is itself banned (paper §5.4; + // only whole-object construct_at re-initializes, which is uniformly // unmodeled) -- so below the top level the marker counts for every access. if (DeclDenotesUninit(ME->getMemberDecl())) return UninitStorage::Uninitialized; diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp new file mode 100644 index 0000000000000..9533df73a53d5 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -0,0 +1,243 @@ +// All violations share one TU with a leading unrelated error: the early error +// disables the analysis-based-warnings pass for later functions, so this also +// verifies that the local-aggregate member check keeps diagnosing through the +// post-error rerun. +// RUN: %clang_cc1 -fsyntax-only -verify=expected,common -fprofiles -std=c++23 -Wno-uninitialized %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles,common -std=c++23 -Wno-uninitialized %s + +// std::init: an [[uninit]] scalar member of a constructor-less aggregate +// local (the paper §5.3 "class exposing uninitialized members" pattern) is +// given a value by a plain member store; a read before the member is +// definitely assigned on every path is diagnosed by a per-function +// definite-assignment pass, the local-variable analog of the ctor-body check. + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +namespace std { enum class byte : unsigned char {}; } + +int leading_unrelated_error = undeclared_identifier; +// common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} + +struct Agg { + int m [[uninit]]; // expected-note 10 {{member 'm' declared here}} +}; +void take_ref(Agg &); +// The pointee is uninitialized memory, so the parameter carries the marker +// (the unmarked spelling is the ref_to_uninit binding rule's to reject). +void take_ptr(int *p [[ref_to_uninit]]); + +int test_read_before_any_write() { + Agg a; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +int test_read_then_write() { + Agg a; + int v = a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + a.m = 1; + return v; +} + +int test_branch_one_path(bool c) { + Agg a; + if (c) + a.m = 1; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// A compound assignment and a built-in ++/-- read the old value before +// writing it. +void test_compound_reads_old_value() { + Agg a; + a.m += 1; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +void test_incdec_reads_old_value() { + Agg a; + a.m++; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// A loop body may run zero times, so an assignment inside it does not reach a +// read after the loop... +int test_loop_may_not_run(int n) { + Agg a; + for (int i = 0; i < n; ++i) + a.m = i; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// ...and a read at the top of the body precedes the first iteration's write. +int test_loop_read_first_iteration(int n) { + Agg a; + int t = 0; + for (int i = 0; i < n; ++i) { + t += a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + a.m = i; + } + return t; +} + +// sizeof neither reads the member (unevaluated) nor escapes the object. +int test_sizeof_neither_reads_nor_escapes() { + Agg a; + (void)sizeof(a.m); + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// An [[uninit]] member inherited from a constructor-less non-virtual base is +// tracked like the class's own (nothing can have assigned it earlier). +struct Base { + int bm [[uninit]]; // expected-note {{member 'bm' declared here}} +}; +struct Derived : Base { + int dm = 0; +}; +int test_base_subtree_member() { + Derived d; + return d.bm; // expected-error {{member 'bm' is read before initialization under profile 'std::init'}} +} + +int test_write_then_read() { + Agg a; + a.m = 5; + return a.m; // OK +} + +int test_branch_both_paths(bool c) { + Agg a; + if (c) + a.m = 1; + else + a.m = 2; + return a.m; // OK +} + +// Any appearance of the variable outside a recognized member read or write +// conservatively marks every member assigned: the address may be used to +// initialize the object (construct_at, memcpy, an initializing callee). +int test_escape_address_of_object() { + Agg a; + (void)&a; + return a.m; // OK: escaped +} + +int test_escape_address_of_member() { + Agg a; + take_ptr(&a.m); + return a.m; // OK: escaped +} + +int test_escape_reference_binding() { + Agg a; + take_ref(a); + return a.m; // OK: escaped +} + +int test_escape_lambda_capture() { + Agg a; + auto init = [&] { a.m = 5; }; + init(); + return a.m; // OK: the capture escapes the object +} + +// A class with a user-provided constructor is trusted (paper §5.1): its +// constructor body may have assigned the member, which local analysis cannot +// see. This pins the deliberate trust decision for non-current-object member +// reads. +struct Slot { + int y [[uninit]]; + Slot() {} +}; +int test_user_provided_ctor_trusted() { + Slot uu; + return uu.y; // OK: trusted +} + +// A base with a user-provided constructor keeps its members untracked, even +// under a constructor-less derived class. +struct TrustedBase { + int tm [[uninit]]; + TrustedBase() {} +}; +struct DerivedFromTrusted : TrustedBase {}; +int test_trusted_base_member() { + DerivedFromTrusted d; + return d.tm; // OK: trusted +} + +// Any written initialization gives every member a value; only the bare +// `Agg a;` form (the implicit no-op default-construction) is tracked. +Agg make_agg(); +int test_initialized_forms(Agg other) { + Agg a{}; + Agg b = {}; + Agg c = Agg(); + Agg d = other; + Agg e = make_agg(); + return a.m + b.m + c.m + d.m + e.m; // OK +} + +// Parameters and references arrive constructed by the caller and are never +// tracked. +int test_parameter_untracked(Agg p, Agg &r) { + return p.m + r.m; // OK +} + +// A local that is itself [[uninit]]-marked is the parse-time rules' +// territory: the read-through check owns its subobject reads, and exactly one +// diagnostic fires. +int test_marked_local_owned_by_read_through() { + Agg s [[uninit]]; + return s.m; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} +} + +// A static local is zero-initialized, never tracked. +int test_static_local_untracked() { + static Agg a; + return a.m; // OK +} + +// std::byte members are exempt (paper §4.5), so a byte-only aggregate has +// nothing to track. +struct ByteBox { + std::byte b [[uninit]]; +}; +std::byte test_byte_member_exempt() { + ByteBox x; + return x.b; // OK +} + +void test_suppress_stmt() { + Agg a; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_read")]] { + int v = a.m; // OK: suppressed + (void)v; + } +} + +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init)]] +int test_suppress_decl() { + Agg a; + return a.m; // OK: suppressed +} + +// A lambda body's own locals are tracked when the lambda's call operator is +// analyzed. +void test_lambda_own_local() { + auto f = [] { + Agg a; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + }; + (void)f; +} + +// A template function's body is analyzed per instantiation. +template +int template_local_member_read() { + Agg a; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} +template int template_local_member_read(); // expected-note {{in instantiation of function template specialization 'template_local_member_read' requested here}} From 72e6edacc6e90e80142c080c54dd82654764476c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:09:07 -0400 Subject: [PATCH 233/289] Skip static call operators in the object-argument check --- clang/docs/ProfilesFramework.rst | 4 +++- clang/include/clang/Sema/SemaProfiles.h | 4 +++- clang/lib/Sema/SemaProfiles.cpp | 7 +++++++ .../test/SemaCXX/safety-profile-init-ref-to-uninit.cpp | 10 ++++++++++ 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index e28ccaa8f2ce9..1d90235f21267 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1209,7 +1209,9 @@ recognizers symmetric. member function instead initializes its object as an ordinary parameter, which the parameter site above already owns and whose parameter can carry the marker; a destructor call is skipped, destruction being the deferred - destroy_at slice), and lambda captures -- an init-capture binds + destroy_at slice, and so is a static call operator, which -- like a static + member call -- evaluates the object argument without using its value), and + lambda captures -- an init-capture binds like a variable initialization when its variable is created (``Sema::createLambdaInitCaptureVarDecl``), and a plain by-reference capture of an entity denoting uninitialized storage (an ``[[uninit]]`` variable, or diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index b3d094664cec9..a9ef5b2846dd2 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -314,7 +314,9 @@ class SemaProfiles : public SemaBase { /// operators. Explicit-object member functions initialize their object as an /// ordinary parameter and are already checked there; a destructor call is /// skipped (destruction of uninitialized storage is the deferred destroy_at - /// slice). Defers only on an instantiation-dependent \p Object -- the call + /// slice), as is a static call operator (no implicit object parameter, like + /// a static member call). Defers only on an instantiation-dependent + /// \p Object -- the call /// is rebuilt at instantiation, re-running the funnel -- and otherwise fires /// at definition time, repeating if the call is rebuilt anyway (accepted). void checkInitProfileObjectArgument(const Expr *Object, diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 7c6d4f22794de..578c9144cb484 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1163,6 +1163,13 @@ void SemaProfiles::checkInitProfileObjectArgument(const Expr *Object, // only the explicit s.~S() spelling would be an inconsistent sliver. if (isa(Method)) return; + // A static call operator (C++23) has no implicit object parameter: its + // object argument is evaluated but its value is never used, exactly like a + // static member function named through an object -- whose call path never + // reaches this funnel. BuildCallToObjectOfClassType converts the object + // argument for static call operators all the same, so skip them here. + if (Method->isStatic()) + return; // An instantiation-dependent object argument cannot be classified yet; its // call is always rebuilt at instantiation, re-running this funnel with the // substituted object. A non-dependent call fires at definition time and diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index f884ab5520a1a..b476e33ee0e6b 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1623,6 +1623,14 @@ struct WithDtor { ~WithDtor(); }; +// A static call operator has no implicit object parameter; the object +// argument is evaluated but its value never used, exactly like a static +// member function named through an object. +struct StaticCall { + int m; + static int operator()(int x) { return x; } +}; + void test_member_call_silent_forms() { Callee s [[uninit]]; s.sf(); // OK: a static member uses no object argument @@ -1633,6 +1641,8 @@ void test_member_call_silent_forms() { make_callee().f(); // OK: unmarked call result is trusted initialized WithDtor d [[uninit]]; d.~WithDtor(); // OK: destruction is the deferred destroy_at slice + StaticCall c [[uninit]]; + c(1); // OK: a static call operator uses no object argument } // The object itself being unmarked keeps the trust decision: a class whose From 6c08ff64dc4c6f0ad3f85a48e07aeecc3dc212b9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:10:01 -0400 Subject: [PATCH 234/289] Document that pointer-to-member calls bypass the object-argument check --- clang/docs/ProfilesFramework.rst | 5 ++++- clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 1d90235f21267..f6617ca3431cc 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1318,7 +1318,10 @@ recognizers symmetric. ``EK_ArrayElement`` and the ``[[ref_to_uninit]]`` marking lives on the field, not the element -- as is a pointer/reference member reached through an ``IndirectFieldDecl`` (a member of an anonymous struct/union), consistent with - the scalar slice. + the scalar slice. A member call through a pointer-to-member + (``(s.*pmf)()``) resolves no ``CXXMethodDecl`` at the call and bypasses the + object-argument conversion, so its object goes unchecked -- the + pointer-to-member analog of the call-through-function-pointer gap. R8. ``pointer_marker`` -- attribute handler ........................................... diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index b476e33ee0e6b..92ec2a86d1aae 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1645,6 +1645,15 @@ void test_member_call_silent_forms() { c(1); // OK: a static call operator uses no object argument } +// A call through a pointer-to-member resolves no method at the call and +// bypasses the object-argument conversion -- the pointer-to-member analog of +// the call-through-function-pointer gap (a known gap, not an endorsement). +void test_member_call_through_pointer_to_member() { + Callee s [[uninit]]; + int (Callee::*pmf)() = &Callee::f; + (s.*pmf)(); // OK: known gap +} + // The object itself being unmarked keeps the trust decision: a class whose // *member* is [[uninit]] may still have its member functions called (its // constructor body may have assigned the member, paper §5.1/§5.2). From 2bcedf158d4403a080473fb07f17fce704b22f16 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:12:20 -0400 Subject: [PATCH 235/289] Resolve tracked member accesses through one lookup result --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 33 ++++++++++++------------ 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 7d4faff289305..8ae222dfb278f 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2195,22 +2195,24 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { return; const unsigned N = PairField.size(); - // The tracked pair index for E when it is a `V.m` access on a tracked - // local; ~0u otherwise. \p BaseOut receives the access's base DeclRefExpr. - auto LookupPair = [&](const Expr *E, - const DeclRefExpr *&BaseOut) -> unsigned { + // A `V.m` access on a tracked local, resolved to its pair index and its + // base DeclRefExpr; Idx is ~0u (and Base null) when E is not one. + struct TrackedAccess { + unsigned Idx = ~0u; + const DeclRefExpr *Base = nullptr; + }; + auto LookupPair = [&](const Expr *E) -> TrackedAccess { const FieldDecl *F = nullptr; const DeclRefExpr *DRE = getLocalMemberAccess(E, F); if (!DRE) - return ~0u; + return {}; const auto *V = dyn_cast(DRE->getDecl()); if (!V) - return ~0u; + return {}; auto It = PairIdx.find({V, F}); if (It == PairIdx.end()) - return ~0u; - BaseOut = DRE; - return It->second; + return {}; + return {It->second, DRE}; }; // First pass: the base DeclRefExprs consumed by a recognized member read or @@ -2227,13 +2229,13 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { const DeclRefExpr *Base = nullptr; if (const auto *ICE = dyn_cast(St)) { if (ICE->getCastKind() == CK_LValueToRValue) - LookupPair(ICE->getSubExpr(), Base); + Base = LookupPair(ICE->getSubExpr()).Base; } else if (const auto *BO = dyn_cast(St)) { if (BO->isAssignmentOp()) - LookupPair(BO->getLHS(), Base); + Base = LookupPair(BO->getLHS()).Base; } else if (const auto *UO = dyn_cast(St)) { if (UO->isIncrementDecrementOp()) - LookupPair(UO->getSubExpr(), Base); + Base = LookupPair(UO->getSubExpr()).Base; } if (Base) Benign.insert(Base); @@ -2261,17 +2263,16 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { if (!CS) continue; const Stmt *St = CS->getStmt(); - const DeclRefExpr *Base = nullptr; if (const auto *ICE = dyn_cast(St)) { if (ICE->getCastKind() != CK_LValueToRValue) continue; - unsigned Idx = LookupPair(ICE->getSubExpr(), Base); + unsigned Idx = LookupPair(ICE->getSubExpr()).Idx; if (Idx != ~0u) BlockEvents.push_back({Read, Idx, ICE}); } else if (const auto *BO = dyn_cast(St)) { if (!BO->isAssignmentOp()) continue; - unsigned Idx = LookupPair(BO->getLHS(), Base); + unsigned Idx = LookupPair(BO->getLHS()).Idx; if (Idx == ~0u) continue; BlockEvents.push_back( @@ -2280,7 +2281,7 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { } else if (const auto *UO = dyn_cast(St)) { if (!UO->isIncrementDecrementOp()) continue; - unsigned Idx = LookupPair(UO->getSubExpr(), Base); + unsigned Idx = LookupPair(UO->getSubExpr()).Idx; if (Idx == ~0u) continue; BlockEvents.push_back({ReadWrite, Idx, UO}); From 2b821316ad140cf1a65101081b6509cb83c88676 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:12:59 -0400 Subject: [PATCH 236/289] Test member operator[] and unevaluated decltype at the object-argument site --- clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 92ec2a86d1aae..5ceec408be675 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1578,6 +1578,7 @@ struct Callee { operator int() const; int *operator->(); int operator()(int); + int &operator[](int); }; void test_member_call_on_uninit_object() { @@ -1601,6 +1602,7 @@ void test_member_operators_on_uninit_object() { (void)(s == t); // expected-error {{calling member function 'operator==' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} int v = s; // expected-error {{calling member function 'operator int' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} s(1); // expected-error {{calling member function 'operator()' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + s[0] = 1; // expected-error {{calling member function 'operator[]' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} (void)v; } @@ -1635,6 +1637,8 @@ void test_member_call_silent_forms() { Callee s [[uninit]]; s.sf(); // OK: a static member uses no object argument (void)sizeof(s.f()); // OK: unevaluated + using Unevaluated = decltype(s.f()); // OK: unevaluated + (void)Unevaluated{}; Callee t{}; t.f(); // OK: initialized object Callee{}.f(); // OK: a prvalue object is not uninitialized storage From 6d558e6589707965dc4933975883b6c6ca2a6e69 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:14:12 -0400 Subject: [PATCH 237/289] Test union locals and placement-new escapes in the local-member pass --- .../safety-profile-init-local-member-read.cpp | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp index 9533df73a53d5..e96f25650e49a 100644 --- a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -141,6 +141,14 @@ int test_escape_lambda_capture() { return a.m; // OK: the capture escapes the object } +// Placement new takes the object's address -- the same escape as &a. +void *operator new(__SIZE_TYPE__, void *p) noexcept; +int test_escape_placement_new() { + Agg a; + new (&a) Agg{1}; + return a.m; // OK: escaped +} + // A class with a user-provided constructor is trusted (paper §5.1): its // constructor body may have assigned the member, which local analysis cannot // see. This pins the deliberate trust decision for non-current-object member @@ -198,6 +206,19 @@ int test_static_local_untracked() { return a.m; // OK } +// A union local is never tracked: the harvest rejects union types (their +// members are mutually exclusive, and [[uninit]] on a union member is banned +// by union_marker anyway -- suppressed on the function to build the fixture). +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +[[profiles::suppress(std::init, rule: "union_marker")]] +int test_union_local_untracked() { + union U { + int x [[uninit]]; + }; + U u = {1}; + return u.x; // OK +} + // std::byte members are exempt (paper §4.5), so a byte-only aggregate has // nothing to track. struct ByteBox { From 55426e76d578e85c6155221d8deab59a8fedab46 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:14:46 -0400 Subject: [PATCH 238/289] Keep the R1 local-pass example literals on one line --- clang/docs/ProfilesFramework.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index f6617ca3431cc..75bdd4f7c2aa2 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -938,8 +938,9 @@ read-then-write of that member. Details: The same guarantee covers an ``[[uninit]]`` scalar member of a *constructor-less aggregate local* (the paper §5.3 "class exposing -uninitialized members" pattern used with a local: ``struct Agg { int m -[[uninit]]; }; Agg a; a.m = 5;``). ``checkInitProfileLocalMembers`` in +uninitialized members" pattern used with a local: +``struct Agg { int m [[uninit]]; };`` and ``Agg a; a.m = 5;``). +``checkInitProfileLocalMembers`` in ``clang/lib/Sema/AnalysisBasedWarnings.cpp`` -- the local-variable analog of the ctor-body pass, sharing its tracked-member filter, event/replay shape, suppression lookup, and post-error rerun -- runs the same forward From 5881bcf0e45c4a2cfd73c29570f03b7a28f6dce3 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:54:14 -0400 Subject: [PATCH 239/289] Note the anonymous-aggregate and array gaps in the local-member pass --- clang/docs/ProfilesFramework.rst | 7 ++++++- .../safety-profile-init-local-member-read.cpp | 20 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 75bdd4f7c2aa2..b53f51bb8fcc9 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -957,7 +957,12 @@ Details: form. Any written initialization (``Agg a{}``, ``= {}``, ``= Agg()``, a copy) gives every member a value and leaves nothing to track, as does a local that is itself ``[[uninit]]``-marked -- its subobject accesses are the - parse-time read-through / ``uninit_write`` rules' territory. + parse-time read-through / ``uninit_write`` rules' territory. A member of + an anonymous struct or union is not tracked (its access is an + ``IndirectFieldDecl`` chain, not a direct ``a.m``), consistent with the + anonymous-aggregate skips in the ctor-body pass and R5; arrays of + aggregates are likewise out of scope (element tracking is the deferred + ``construct_at`` slice). - A plain member store ``a.m = e`` assigns the member (§4.5); a compound assignment and a built-in ``++``/``--`` read the old value first and are a read-then-write, exactly as in the ctor-body pass. diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp index e96f25650e49a..5095a7fb51d9f 100644 --- a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -206,6 +206,26 @@ int test_static_local_untracked() { return a.m; // OK } +// A member of an anonymous struct is reached through an IndirectFieldDecl +// chain, not a direct `a.m` access, so it is not tracked -- consistent with +// the anonymous-aggregate skips in the ctor-body pass and R5 (a known gap). +struct HasAnon { + struct { + int m [[uninit]]; + }; +}; +int test_anonymous_member_untracked() { + HasAnon a; + return a.m; // OK: known gap +} + +// An array of aggregates is not tracked (element tracking is the deferred +// construct_at slice). +int test_array_of_aggregates_untracked() { + Agg arr[2]; + return arr[0].m; // OK: known gap +} + // A union local is never tracked: the harvest rejects union types (their // members are mutually exclusive, and [[uninit]] on a union member is banned // by union_marker anyway -- suppressed on the function to build the fixture). From ebdeae105b583b00ddbff5cef68238c344b8fd41 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:54:43 -0400 Subject: [PATCH 240/289] Refresh the std::init overview for the new flow and object-argument checks --- clang/docs/ProfilesFramework.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index b53f51bb8fcc9..a6da658019004 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -770,20 +770,25 @@ constructors (no body) and delegating constructors are skipped. The ``std::init`` Profile (initial slice) ----------------------------------------- -A slice of the proposed initialization profile. It does not yet implement -classes that expose uninitialized memory to users (paper §5.3) or random-access -initialization of uninitialized arrays (paper §5.5). A read of a scalar -``[[uninit]]`` data member before it is assigned in a constructor body *is* -diagnosed by R1 via a CFG-based definite-assignment pass over the body (paper -§7.1 "initialized ... before use"). Class-type and array members (which need -``construct_at`` flow modeling), ``construct_at``/``destroy_at`` flow, and -double-initialization / double-destruction detection remain deferred. The +A slice of the proposed initialization profile. A read of a scalar +``[[uninit]]`` data member before it is assigned *is* diagnosed by R1 via +CFG-based definite-assignment passes (paper §7.1 "initialized ... before +use"): over the constructor body for the current object's members, and over +any function body for the members of a *constructor-less aggregate local* -- +the member-store slice of the paper §5.3 "classes exposing uninitialized +memory" pattern. The rest of §5.3 (``construct_at``-based initialization), +random-access initialization of uninitialized arrays (paper §5.5), class-type +and array members (which need ``construct_at`` flow modeling), +``construct_at``/``destroy_at`` flow, and double-initialization / +double-destruction detection remain deferred. The constructor is *not* required to initialize a ``[[uninit]]`` member (paper §5.1 excepts members with an uninitialized indicator, and §5.3 leaves such a member for the user): the obligation is keyed on a read before assignment, not on the constructor's end. A scalar *read through* a ``[[ref_to_uninit]]`` pointer/reference is diagnosed at the lvalue-to-rvalue conversion (R7, -``uninit_read``); *writes* through such a pointer/reference are not yet verified +``uninit_read``), and a member call on an object recognized as uninitialized +storage is diagnosed at its implicit object argument (R7, ``ref_to_uninit``); +*writes* through such a pointer/reference are not yet verified (the paper relegates them to ``construct_at`` or suppression). A scalar store to a *subobject* of a named ``[[uninit]]`` entity, by contrast, is banned as delayed piecemeal initialization (R10, ``uninit_write``). From 698e1ac73dabf1173f7ae1602a1ed2545ff2ec0b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 15:57:32 -0400 Subject: [PATCH 241/289] Update the uninit marker docs for the flow-checked member reads --- clang/include/clang/Basic/AttrDocs.td | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index f4d82f818dc9c..c4680b1baf6db 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -898,9 +898,13 @@ and an initializer is ill-formed under the initialization profile (the marker contradicts the explicit initialization). On a non-static data member the marker also excuses the member from the -profile's requirement that every constructor initialize it. (The profile -does not yet verify that such a member is assigned in the constructor -body; until it does, a marked member is trusted.) +profile's requirement that every constructor initialize it. A scalar marked +member is instead flow-checked where local analysis is sound: a read before +it is assigned is diagnosed in the constructor's own body, and -- for a +member of a class with no user-provided constructor -- in any function body +that declares a bare local of that class. A marked member of an object with +a user-provided constructor reached through any other object is trusted (the +constructor body may have assigned it, which local analysis cannot see). The attribute cannot be applied where leaving the entity uninitialized is meaningless -- a reference, a function parameter, or a structured binding -- From 59f34a4df1cedb94f712d81ec225355b257ed742 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 16:52:44 -0400 Subject: [PATCH 242/289] Split the profiles framework internals into their own doc --- clang/docs/ProfilesFramework.rst | 613 +----------------- clang/docs/ProfilesFrameworkInternals.rst | 599 +++++++++++++++++ clang/docs/index.rst | 1 + clang/include/clang/Sema/SemaProfiles.h | 3 +- clang/lib/Sema/SemaCast.cpp | 3 +- clang/lib/Sema/SemaModule.cpp | 2 +- .../SemaCXX/safety-profile-header-unit.cpp | 6 +- 7 files changed, 623 insertions(+), 604 deletions(-) create mode 100644 clang/docs/ProfilesFrameworkInternals.rst diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index a6da658019004..7b7264cd0b1cc 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -65,16 +65,6 @@ The framework's parse-time bookkeeping (``ProfileSuppressScope``, attribute custom parsing, etc.) is also no-ops when ``LangOpts.Profiles`` is false, so the flag is the single switch that turns the entire feature on or off. -The built-in ``test::`` profiles (see `Built-in Profiles`_) exist only to -exercise the framework and are additionally gated on the ``-fprofiles-test-profiles`` -flag, which sets ``LangOpts.ProfilesTestProfiles``. This flag is ``-cc1``-only -(not exposed by the driver) and is intended solely for running the test suite. -Under ``-fprofiles`` alone, ``[[profiles::enforce(test::...)]]`` is still -recognized (it is not ``warn_attribute_ignored``) and its designator is still -recorded and exported across modules, but ``SemaProfiles::isProfileEnforced`` reports -any ``test::``-prefixed profile as not enforced, so no ``test::`` rule ever -fires. Real profiles such as ``std::init`` are unaffected by this flag. - Attribute Reference =================== @@ -111,317 +101,13 @@ See the auto-generated :doc:`AttributeReference` for the AttrDocs entries linked from these attributes. -Implementing a New Profile -========================== - -Adding a new profile requires no changes to the framework itself. A profile is -defined entirely by: - -1. A **profile name** (a ``::``-separated identifier sequence such as - ``vendor::safety`` or ``std::type``). -2. Zero or more **rule names** (string identifiers such as - ``"reinterpret_cast"``). -3. **Diagnostics** emitted when a rule is violated. -4. **Check sites** in the compiler where the framework is consulted. - -There is no central registry of profiles. The framework treats profile names -as opaque strings; a profile is considered "enforced" simply because the user -wrote ``[[profiles::enforce(name)]]`` in their source. Each rule within a -profile is likewise just a string identifier; users can suppress individual -rules with ``[[profiles::suppress(profile_name, rule: "rule_name")]]``. - -There are two implementation patterns, depending on when the rule is checked. - - -Define Diagnostics ------------------- - -Add diagnostics to ``clang/include/clang/Basic/DiagnosticSemaKinds.td`` in the -``// C++ Profiles framework (P3589R2)`` group. Each diagnostic should accept -``%0`` for the profile name, since the framework passes the profile name as -the first diagnostic argument. The group declares the helper class - -.. code-block:: text - - class ProfileRuleError : Error { - let SFINAE = SFINAE_Suppress; - } - -so that profile-rule diagnostics participate in the SFINAE machinery as -suppressed errors -- they do not count as substitution failures and cannot -change overload resolution, but selected specializations replay them when -they are actually used. Define new rules using ``ProfileRuleError`` rather -than ``Error`` directly: - -.. code-block:: text +Extending the Framework +======================= - def err_profile_type_cast_reinterpret : ProfileRuleError< - "'reinterpret_cast' is unsafe under profile '%0'">; - - -Pattern 1: Sema Check-Site Profile ----------------------------------- - -Used when the rule can be checked at a single, well-defined parse-time site -in Sema (typically inside a ``Sema::Build*`` or ``Sema::Act*`` routine). -``test::type_cast`` is the in-tree example. - -At each such site, call ``SemaProfiles::checkProfileViolation``: - -.. code-block:: c++ - - checkProfileViolation("my::profile", "my_rule", Loc, - diag::err_my_profile_rule); - -This function checks whether the profile is enforced and not suppressed. -During template argument deduction, profile rule diagnostics are suppressed -by Clang's normal SFINAE machinery so they cannot affect overload resolution -or template instantiation; selected specializations replay suppressed -diagnostics when used. Unevaluated and discarded-statement contexts are -skipped. The profile name is passed as ``%0``. - -``checkProfileViolation`` fires at parse time. Inside a template, parse-time -checks follow one unified model. A *non-dependent* construct is checked on -the template *pattern*, at definition time: TreeTransform may return such a -node unchanged at instantiation (for some node kinds, such as casts, a -non-dependent ``Build*`` result is reused), so deferring would silently lose -the diagnostic. This deliberately trades strict "as-if after phase 7" purity -(P3589R2 §1.1) for reuse-proof diagnostics: a non-dependent violation -diagnoses even in a never-instantiated template or in an ``if constexpr`` -branch whose discarding is not yet known at the pattern (a branch already -known discarded -- a non-value-dependent false condition -- stays silent). -An *instantiation-dependent* construct cannot be checked on the pattern; it -is always rebuilt at instantiation, where the re-run ``Build*`` checks the -substituted form, once per specialization. A construct with non-dependent -check operands can still be rebuilt at instantiation (a local variable, a -call argument, or a return statement forces a rebuild, for example); the -re-run ``Build*`` then repeats the definition-time diagnostic at the same -location, with an ``in instantiation of ...`` note. This repetition is -accepted for now; ``test::type_cast``'s cast nodes are never rebuilt when -non-dependent, so it never repeats. - -Under ``-fdelayed-template-parsing`` the body of a never-instantiated -template is never parsed at all, so definition-time diagnosis of -non-dependent violations does not occur in that mode. - -Suppression for parse-time check sites is consulted via the -``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` -RAII guards (see :ref:`profiles-internals` below). Profile implementers do -not need to interact with the stack directly. - - -Pattern 2: Post-Parse / CFG-Based Profile ------------------------------------------ - -Used when the rule cannot be checked at a single Sema entry point because it -depends on whole-function analysis -- typically a CFG-based analysis run -after a function body is complete. ``test::uninit_read`` is the in-tree -example: it diagnoses reads of uninitialized variables on top of Clang's -existing CFG-based uninitialized-variables analysis. - -This pattern needs three pieces, all colocated with the analysis pass (the -framework intentionally does not learn the profile name). - -1. **Add the profile to the analysis pass's per-pass opt-in table.** - Each post-parse analysis owns a small table of the profiles that ride it, - one row per profile (profile name, rule name, diagnostic id). The - in-tree example is ``CFGUninitProfiles`` in - ``clang/lib/Sema/AnalysisBasedWarnings.cpp``: - - .. code-block:: c++ - - struct CFGUninitProfileEntry { - StringRef Name; - StringRef Rule; - unsigned DiagID; - }; - constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { - {"my::profile", /*Rule=*/"", diag::err_my_profile_rule}, - }; - -2. **Gate the analysis pass on the table.** - Analysis-based passes in ``AnalysisBasedWarnings.cpp::IssueWarnings`` are - normally run only when their corresponding warning flag is enabled. To - run the pass for an enforced profile even when the underlying warning is - silenced, OR an ``S.anyProfileEnforced(Table)`` check (the shared - ``SemaProfiles::anyProfileEnforced`` gate, also used by the finalization dispatch) - into the existing pass guard. The in-tree example's pass guard becomes - ``hasEnforcedCFGUninitProfile() || !Diags.isIgnored(...)`` (a small - accessor over ``S.anyProfileEnforced(CFGUninitProfiles)``). - -3. **Walk the table in the analysis's diagnostic reporter.** - For each use site the analysis would have warned about, iterate the - table and call - ``SemaProfiles::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``, - which walks parent statements and lexical declaration contexts to honor - ``[[profiles::suppress]]`` on enclosing AST nodes (the post-parse - counterpart to ``ProfileSuppressStack``). Emit the entry's diagnostic - when it returns true, and skip the default warning path. In the - in-tree example (``UninitValsDiagReporter`` in - ``AnalysisBasedWarnings.cpp``): - - .. code-block:: c++ - - for (const auto &U : *vec) { - for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { - if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) - continue; - S.Diag(U.getUser()->getBeginLoc(), E.DiagID) - << E.Name << vd->getDeclName(); - S.Diag(vd->getLocation(), diag::note_var_declared_here) - << vd->getDeclName(); - return; - } - } - -The diagnostic itself is defined with ``ProfileRuleError`` as in pattern 1. - -The post-parse Stmt-walking suppression check is intentionally separate from -the parse-time stack check: by the time CFG analysis runs, the parse stack -has been unwound, so the framework instead walks the AST. The Stmt-walking -overload of ``isProfileSuppressed`` examines: - -- ``AttributedStmt`` ancestors of the use site. -- ``DeclStmt`` ancestors (whose declared ``VarDecl``\ s carry the attribute, - not the enclosing statement). -- The enclosing ``Decl`` chain via ``getLexicalDeclContext()``. - - -Pattern 3: Class-Finalization Profile -------------------------------------- - -Used when the rule applies to a class as a whole and needs to run once after -the class definition is complete -- for example, "every non-private field of -this class must satisfy property X" or "this class's field set must look -like Y." ``test::class_final`` is the in-tree example. - -The dispatch point is ``SemaProfiles::checkProfileViolationsAtClassFinalization``, -called from the end of ``Sema::CheckCompletedCXXClass`` in -``clang/lib/Sema/SemaDeclCXX.cpp``. ``CheckCompletedCXXClass`` is the -single function reached from every class-completion path -- the parser -(``ActOnFinishCXXMemberSpecification``), template instantiation -(``InstantiateClass``), and lambda completion -- so wiring the hook there -covers all of them with no extra plumbing. - -The class-finalization entry point -``checkProfileViolationsAtClassFinalization`` filters out classes the rules -are not meant to see: - -- Dependent classes (``isDependentType()``). The hook will re-fire on each - instantiation via the template-instantiation completion path. -- Invalid classes (``isInvalidDecl()``). -- Lambdas (``isLambda()``). Closure types have no user-controlled field - shape, so class-finalization rules do not apply. - -This pattern needs two pieces, both colocated with the dispatcher. - -1. **Add the profile to the class-finalization opt-in table.** - ``ClassFinalizationProfiles`` in ``clang/lib/Sema/SemaProfiles.cpp`` is a - small per-pass table of profile name plus callback. One row per profile: - - .. code-block:: c++ - - // FinalizationProfile is shared with pattern 4. - template struct FinalizationProfile { - StringRef Name; - void (*Callback)(Sema &, Node *); - }; - constexpr FinalizationProfile ClassFinalizationProfiles[] = { - {"my::profile", &runMyProfileCallback}, - }; - - The shared ``dispatchFinalizationProfiles`` dispatcher (used by both - patterns 3 and 4) checks ``anyProfileEnforced(Table)``, iterates the - table, skips entries whose profile is not enforced, and invokes the - callback. Each callback passes the finalized ``Decl`` (here the - ``CXXRecordDecl``) to the decl-aware ``shouldEmitProfileViolation`` - overload, which walks the declaration and its lexical parents for a - matching ``[[profiles::suppress]]``, so suppression on the class or any - enclosing lexical ``Decl`` works without the dispatcher establishing a - suppress scope. Finalization can run as a side effect of an *unrelated* - template instantiation whose ``[[profiles::suppress]]`` scope is still on - the transient parse-time ``ProfileSuppressStack``; because stack entries - are matched against the violation's location (see - :ref:`profiles-token-dominion`), such a scope -- whose construct's tokens - do not cover the finalized class -- does not suppress the callback's - diagnostics. - -2. **Emit diagnostics from the callback via** - ``SemaProfiles::shouldEmitProfileViolation``. Each callback decides where on - the class to point and which diagnostic to use, possibly with notes: - - .. code-block:: c++ - - void runMyProfileCallback(Sema &S, CXXRecordDecl *RD) { - if (!S.shouldEmitProfileViolation("my::profile", /*Rule=*/"", - RD->getLocation())) - return; - S.Diag(RD->getLocation(), diag::err_my_profile_rule) - << "my::profile" << RD; - } - -The diagnostic itself is defined with ``ProfileRuleError`` as in patterns 1 -and 2. - -Class-finalization is for **structural** rules -- those answerable from the -class's declared members, their types, and their attributes. The callbacks -run while the ``CXXRecordDecl`` is being finalized (immediately before -``CheckCompletedCXXClass`` returns), which is *before any constructor body or -member-initializer list has been parsed* -- inline member bodies are -late-parsed afterward, and out-of-line and template member constructors later -still. A class-finalization callback therefore must not inspect a -constructor's ``inits()`` (they are empty here). Rules that depend on what a -constructor initializes belong on the constructor-finalization dispatch -(pattern 4); rules that need whole-function flow analysis belong on a -post-parse CFG pass (pattern 2). - - -Pattern 4: Constructor-Finalization Profile -------------------------------------------- - -Used when the rule applies to a single constructor and needs that -constructor's complete member-initializer list -- for example, "every member -must be initialized by this constructor." ``test::ctor_final`` is the in-tree -example, and the ``std::init`` ``ctor_uninit_member`` rule is the real one. - -The dispatch point is ``SemaProfiles::checkProfileViolationsAtConstructorFinalization``, -called right after ``DiagnoseUninitializedFields`` in -``Sema::ActOnMemInitializers`` and ``Sema::ActOnDefaultCtorInitializers`` in -``clang/lib/Sema/SemaDeclCXX.cpp``. Those two functions are the funnel for -every user-defined constructor -- written or implicit member-initializer -list, inline or out-of-line -- and template instantiation reaches the first -of them through ``Sema::InstantiateMemInitializers``, so the hook sees every -constructor at the point its ``inits()`` (including synthesized entries) is -complete. - -The constructor-finalization entry point -``checkProfileViolationsAtConstructorFinalization`` filters out constructors -the rules are not meant to see: - -- Dependent constructors (``isDependentContext()``). The hook re-fires on - each instantiation. -- Invalid constructors (``isInvalidDecl()``). -- Delegating constructors (``isDelegatingConstructor()``), which leave member - initialization to their target. - -The two pieces mirror pattern 3 and share its machinery: a per-pass opt-in -table ``ConstructorFinalizationProfiles`` of the same -``FinalizationProfile`` row (here -``FinalizationProfile``), and a callback that emits via -``SemaProfiles::shouldEmitProfileViolation``. The same shared -``dispatchFinalizationProfiles`` dispatcher invokes each callback, which -passes the ``CXXConstructorDecl`` to the decl-aware -``shouldEmitProfileViolation`` overload; that overload walks the declaration -and its lexical parents, so ``[[profiles::suppress]]`` on the constructor, -the class, or an enclosing lexical ``Decl`` works. As for pattern 3, a -transient parse-time suppress scope belonging to an unrelated construct does -not reach these callbacks, because stack entries only match violations whose -tokens their construct covers (see :ref:`profiles-token-dominion`). A -constructor body is normally instantiated lazily -- outside any unrelated -suppress scope -- so for pattern 4 this matters rarely; the scenario it -actually handles arises in pattern 3, where a class completes synchronously -inside an enclosing instantiation. A callback that should only apply to -user-written constructors checks ``Ctor->isUserProvided()``. +The framework is profile-agnostic: profile names are opaque strings, there is +no central registry, and adding a new profile requires no changes to the +framework itself. See :doc:`ProfilesFrameworkInternals` for the +implementation patterns and the API for adding a new profile. .. _profiles-token-dominion: @@ -471,161 +157,6 @@ and instantiation of a template that has no definition yet is deferred past the scope's death. -.. _profiles-internals: - -Framework Internals Reference -============================= - -This section describes the framework mechanisms that profile implementers -benefit from understanding, even though they do not interact with them directly. - -``ProfileSuppressScope`` ------------------------- - -An RAII guard that pushes suppression entries onto ``Sema::ProfileSuppressStack`` -and pops them on destruction. It is used by the parser and template -instantiation machinery to make ``[[profiles::suppress]]`` attributes active -during the appropriate region. Each entry records the token range of the -construct its attribute appertains to (the end only once the construct is -fully parsed) and matches only violations located within it (see -:ref:`profiles-token-dominion`). ``checkProfileViolation`` consults -``ProfileSuppressStack`` directly, so profile implementers never need to -create ``ProfileSuppressScope`` objects. - -Stmt-Tree Suppression Walker ----------------------------- - -The ``isProfileSuppressed(name, rule, Stmt*, AnalysisDeclContext&)`` overload -is the post-parse counterpart to ``ProfileSuppressStack``. It is used by -analyses that run after parsing (when the parse-time stack no longer -reflects the enclosing region) and walks the AST upward from a use site to -find any matching ``[[profiles::suppress]]`` attribute on an enclosing -``AttributedStmt``, ``DeclStmt``-declared ``VarDecl``, or lexical -``Decl`` parent. - -Template Instantiation ----------------------- - -During template instantiation, the framework ensures that -``[[profiles::suppress]]`` on the template pattern and its lexical parents -applies to instantiated code. This is done via ``ProfileSuppressScope`` with -``WalkLexicalParents=true`` at several sites: - -- ``SemaTemplateInstantiateDecl.cpp`` -- function and variable template - instantiation. -- ``SemaTemplateInstantiate.cpp`` -- default member initializer instantiation. -- ``TreeTransform.h`` -- ``TransformAttributedStmt`` (suppress on statements) - and ``TransformDeclStmt`` (suppress on declarations within a ``DeclStmt``). - -The reverse direction is *not* propagated: a suppress scope live at the -*point of instantiation* (for example on the declaration whose initializer -triggers it) covers the trigger's tokens, not the pattern's. Instantiated -code retains the pattern's source locations, so the dominion check on stack -entries (see :ref:`profiles-token-dominion`) keeps such a scope from -suppressing checks that fire inside a synchronously instantiated body, NSDMI, -default argument, or instantiated marker re-check. - -Module Enforcement ------------------- - -``[[profiles::enforce(...)]]`` on a module interface declaration records the -enforced profile designators on ``Module::EnforcedProfileDesignators``. A -(non-partition) module implementation unit ``module M;`` automatically inherits -the interface's enforcements, because it implicitly imports the primary -interface unit of ``M``. -``[[profiles::require(...)]]`` on an import-declaration validates that the -imported module's ``EnforcedProfileDesignators`` contains a matching designator. - -A *header unit* participates the same way: ``[[profiles::enforce(...)]]`` on -an empty-declaration in the header (the form P3589R2 §2.3 prescribes for -header units) is recorded on the header-unit module and serialized into its -BMI, so ``[[profiles::require]]`` on an ``import "header.h";`` validates -against it. As with named modules, importing an enforced header unit does not -enforce the profile in the importer. - -``[[profiles::enforce(...)]]`` on a *non-interface* module-declaration (a -``module M;`` implementation unit, or a ``module M:P;`` partition -implementation unit) is accepted but recorded only translation-unit-locally; -it is **not** added to ``Module::EnforcedProfileDesignators`` and so is not -visible to an importer's ``[[profiles::require]]``. - -A module partition implementation unit ``module M:P;`` is also a module -implementation unit of ``M``, so the primary interface's enforcements apply to -it as well. However, it does **not** implicitly import the primary interface, -and the primary interface is normally compiled *after* its partitions, so its -BMI is usually not available when the partition implementation unit is compiled. -Inheritance here is therefore **best-effort**: the enforcements are inherited -only when the primary interface's BMI is already resident in the compilation -(for example, supplied via an eager ``-fmodule-file=``); the BMI is never -force-loaded and its absence is never diagnosed. When it is not available the -partition implementation unit is simply not subject to the inherited profile -- -a missed diagnostic, never a change to the meaning of a well-formed program. -For guaranteed enforcement, **repeat** ``[[profiles::enforce(...)]]`` in the -partition implementation unit rather than relying on inheritance. (Best-effort -inheritance is silent when the interface BMI is absent; if the BMI *is* resident -and enforces a profile whose designator conflicts with a locally repeated -``enforce`` of the same name, that mismatch is still diagnosed.) - -Importing a module that enforces a profile does **not** enforce that profile in -the importing translation unit. Enforcement is always explicit and local. - -Redeclaration Compatibility ---------------------------- - -P3589R2 [decl.attr.enforce]p5: a declaration and its redeclarations must -appear in the dominions of mutually compatible profiles. The rule is -**symmetric** -- when a redeclaration is merged with a previous declaration -from another module unit, every profile whose dominion covered the previous -declaration must have a compatible counterpart covering the redeclaration, -and vice versa. In particular, a profile-enforcing translation unit that -redeclares an entity from a module (or header unit) compiled *without* a -compatible profile is ill-formed; the paper's escape hatch for such headers -is ``[[profiles::exempt]]`` (not yet implemented, see `Intentional -Omissions`_). The check -(``SemaProfiles::checkRedeclarationProfileCompatibility``) runs from -``Sema::CheckRedeclarationInModule``, the funnel for function, variable, tag, -alias, and class-template redeclarations; it is a framework rule -- a plain -error, not suppressible with ``[[profiles::suppress]]``, and diagnose-only -(the redeclaration still merges). - -Two profiles are *compatible* if they have the same name -- designator -arguments configure a profile without changing its identity -- or if both are -standard (``std::``-prefixed) profiles, which P3589R2 proclaims mutually -compatible. No further implementation-proclaimed compatibility is modeled. - -The previous declaration's dominion is approximated by its top-level module's -exported ``EnforcedProfileDesignators``, which is exact for declarations in -the module purview -- including purview ``extern "C"``/``extern "C++"`` -declarations (implicit global module), the common redeclarable case, since -module-attached entities cannot be redeclared in other translation units at -all. Two cases have an *unknown* dominion and are skipped rather than -guessed at (a missed diagnostic, never a wrong one): - -- A declaration in an **explicit global module fragment**: it precedes the - module-declaration, so the exported enforcements do not cover it, and its - TU's empty-declaration enforces are not serialized into the BMI. -- A previous declaration from the **same module family** (an implementation - or partition unit merging with its own interface): the exported set - under-approximates the interface TU's full dominion, and the interface's - enforcements are inherited into the current unit anyway, so checking would - false-positive on locally added profiles. - -A textual or PCH previous declaration is not checked at all: it shares the -current TU's dominion (the placement rule makes a TU's dominion uniform, and -a PCH's enforcements are restored into the including TU). Implicit template -instantiations are exempt, matching the module-ownership check. - -Serialization -------------- - -The framework serializes enforcement state automatically. Profile implementers -do not need to add any serialization code. - -- **PCH**: ``SemaProfiles::EnforcedProfiles`` is written as ``ENFORCED_PROFILES`` - records in the AST bitstream and restored when the PCH is loaded. -- **Module BMI**: ``Module::EnforcedProfileDesignators`` is written as - ``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. - Intentional Omissions ===================== @@ -641,130 +172,16 @@ The following parts of P3589R2 are deliberately not implemented: Built-in Profiles ================= -The tree ships five built-in profiles, all gated on ``-fprofiles``. The four -``test::`` profiles are additionally gated on the ``-cc1``-only -``-fprofiles-test-profiles`` flag (see `Driver Flag`_) and are inert under -``-fprofiles`` alone; ``std::init`` needs only ``-fprofiles``: - -- ``test::type_cast`` (test-only) -- pattern-1 example. -- ``test::uninit_read`` (test-only) -- pattern-2 example riding the existing - CFG uninitialized-variables analysis. -- ``test::class_final`` (test-only) -- pattern-3 example riding the - class-finalization dispatch. -- ``test::ctor_final`` (test-only) -- pattern-4 example riding the - constructor-finalization dispatch. -- ``std::init`` (initial slice of the proposed initialization profile from - Bjarne Stroustrup's "An initialization profile", P4222R1.1, on top of - P3589R2 and P3402R3). It uses all four patterns: the CFG dispatch (with - ``test::uninit_read``), the constructor-finalization dispatch, and several - parse-time check sites. Paper section references (``§``) for ``std::init`` - in this document are to P4222R1.1. - -By convention: - -- Real test profiles live under the ``test::`` namespace. Today there are - four: ``test::type_cast``, ``test::uninit_read``, ``test::class_final``, - and ``test::ctor_final``. Because the ``test::`` prefix is what - ``SemaProfiles::isProfileEnforced`` keys on to gate them behind - ``-fprofiles-test-profiles``, any new test-only profile must also live - under ``test::``. -- The names ``test::other``, ``test::bounds``, ``test::new_profile``, and - ``test::not_enforced`` are deliberately *not* implemented and appear only - in negative tests as stand-in "some other profile" names. Adding a real - profile under any of these names would invalidate those tests. - - -The ``test::type_cast`` Profile -------------------------------- - -A pattern-1 (Sema check-site) profile. Demonstrates the simple case where -a rule can be checked from a single Sema entry point. - -- **Rules**: ``reinterpret_cast``. -- **Diagnostic**: ``err_profile_type_cast_reinterpret`` - ("'reinterpret_cast' is unsafe under profile '%0'"). -- **Check site**: ``Sema::BuildCXXNamedCast`` in ``clang/lib/Sema/SemaCast.cpp``, - inside the ``reinterpret_cast`` arm. Only the ``reinterpret_cast<>`` keyword - form is checked; a C-style or functional cast with reinterpret semantics goes - through a different path and is not diagnosed. - -The entire profile implementation is the single call: - -.. code-block:: c++ +The tree ships the ``std::init`` profile (below) plus four ``test::`` +profiles, all gated on ``-fprofiles``. The ``test::`` profiles exist only to +exercise the framework: they are additionally gated on the ``-cc1``-only +``-fprofiles-test-profiles`` flag, are inert under ``-fprofiles`` alone, and +are described in :doc:`ProfilesFrameworkInternals`. - checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, - diag::err_profile_type_cast_reinterpret); - - -The ``test::uninit_read`` Profile ---------------------------------- - -A pattern-2 (post-parse / CFG-based) profile. Demonstrates the case where -the rule depends on whole-function analysis and must run after parsing. -It diagnoses reads of uninitialized variables by reusing Clang's existing -CFG-based uninitialized-variables analysis. - -- **Rules**: none (the profile has a single implicit rule, so the rule - string is empty). -- **Diagnostic**: ``err_profile_uninit_read`` - ("variable %1 is read before initialization under profile '%0'"). - A companion ``note_var_declared_here`` is emitted at the variable's - declaration. -- **Opt-in table**: ``CFGUninitProfiles`` in - ``clang/lib/Sema/AnalysisBasedWarnings.cpp``. The ``IssueWarnings`` pass - guard consults it via ``hasEnforcedCFGUninitProfile()`` so the analysis - runs even when ``-Wuninitialized`` is silenced, and - ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the - default warning path -- when an entry's - ``SemaProfiles::shouldEmitProfileViolation`` returns true the entry's diagnostic - fires and the default warning is skipped entirely. - -The Stmt-tree suppression walker is what makes ``[[profiles::suppress]]`` -work for this profile: by the time the CFG analysis runs, the parse-time -``ProfileSuppressStack`` has been unwound, so the helper consults the AST -directly via ``shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``. - - -The ``test::class_final`` Profile ---------------------------------- - -A pattern-3 (class-finalization) profile. Demonstrates the case where the -rule applies once per completed class definition and runs from the -class-finalization dispatch in ``Sema::CheckCompletedCXXClass``. - -- **Rules**: none (the profile has a single implicit rule, so the rule - string is empty). -- **Diagnostic**: ``err_profile_class_final_test`` ("test profile fired on - completion of class %1 under profile '%0'"). -- **Opt-in table**: ``ClassFinalizationProfiles`` in - ``clang/lib/Sema/SemaProfiles.cpp``. - -Because dependent classes are filtered out by the dispatcher, the -diagnostic fires on class template *instantiations* rather than on the -primary template. Lambda closures are also skipped. -``[[profiles::suppress(test::class_final)]]`` on the class or any -enclosing lexical ``Decl`` silences the diagnostic via the decl-aware -``shouldEmitProfileViolation`` overload, which walks the class and its -lexical parents for a matching suppression. - - -The ``test::ctor_final`` Profile --------------------------------- - -A pattern-4 (constructor-finalization) profile. Demonstrates the case where -the rule applies once per user-defined constructor, after its -member-initializer list is complete. - -- **Rules**: none (single implicit rule, empty rule string). -- **Diagnostic**: ``err_profile_ctor_final_test`` ("test profile fired on - finalization of a constructor for class %1 under profile '%0'"). -- **Opt-in table**: ``ConstructorFinalizationProfiles`` in - ``clang/lib/Sema/SemaProfiles.cpp``. - -The diagnostic fires once per user-defined constructor -- written or implicit -member-initializer list, inline or out-of-line -- and on constructor template -*instantiations* rather than the dependent pattern. Defaulted and implicit -constructors (no body) and delegating constructors are skipped. +``std::init`` is an initial slice of the proposed initialization profile from +Bjarne Stroustrup's "An initialization profile" (P4222R1.1), on top of P3589R2 +and P3402R3. Paper section references (``§``) for ``std::init`` in this +document are to P4222R1.1. The ``std::init`` Profile (initial slice) diff --git a/clang/docs/ProfilesFrameworkInternals.rst b/clang/docs/ProfilesFrameworkInternals.rst new file mode 100644 index 0000000000000..a8c6138cbeb1c --- /dev/null +++ b/clang/docs/ProfilesFrameworkInternals.rst @@ -0,0 +1,599 @@ +==================================== +C++ Profiles Framework Internals +==================================== + +.. contents:: + :depth: 3 + :local: + + +This document describes the implementation of the C++ Profiles framework +(`P3589R2 `_) +for Clang contributors -- in particular, how to add a new profile. For +user-facing documentation of the feature, see :doc:`ProfilesFramework`. + + +Implementing a New Profile +========================== + +Adding a new profile requires no changes to the framework itself. A profile is +defined entirely by: + +1. A **profile name** (a ``::``-separated identifier sequence such as + ``vendor::safety`` or ``std::type``). +2. Zero or more **rule names** (string identifiers such as + ``"reinterpret_cast"``). +3. **Diagnostics** emitted when a rule is violated. +4. **Check sites** in the compiler where the framework is consulted. + +There is no central registry of profiles. The framework treats profile names +as opaque strings; a profile is considered "enforced" simply because the user +wrote ``[[profiles::enforce(name)]]`` in their source. Each rule within a +profile is likewise just a string identifier; users can suppress individual +rules with ``[[profiles::suppress(profile_name, rule: "rule_name")]]``. + +There are two implementation patterns, depending on when the rule is checked. + + +Define Diagnostics +------------------ + +Add diagnostics to ``clang/include/clang/Basic/DiagnosticSemaKinds.td`` in the +``// C++ Profiles framework (P3589R2)`` group. Each diagnostic should accept +``%0`` for the profile name, since the framework passes the profile name as +the first diagnostic argument. The group declares the helper class + +.. code-block:: text + + class ProfileRuleError : Error { + let SFINAE = SFINAE_Suppress; + } + +so that profile-rule diagnostics participate in the SFINAE machinery as +suppressed errors -- they do not count as substitution failures and cannot +change overload resolution, but selected specializations replay them when +they are actually used. Define new rules using ``ProfileRuleError`` rather +than ``Error`` directly: + +.. code-block:: text + + def err_profile_type_cast_reinterpret : ProfileRuleError< + "'reinterpret_cast' is unsafe under profile '%0'">; + + +Pattern 1: Sema Check-Site Profile +---------------------------------- + +Used when the rule can be checked at a single, well-defined parse-time site +in Sema (typically inside a ``Sema::Build*`` or ``Sema::Act*`` routine). +``test::type_cast`` is the in-tree example. + +At each such site, call ``SemaProfiles::checkProfileViolation``: + +.. code-block:: c++ + + checkProfileViolation("my::profile", "my_rule", Loc, + diag::err_my_profile_rule); + +This function checks whether the profile is enforced and not suppressed. +During template argument deduction, profile rule diagnostics are suppressed +by Clang's normal SFINAE machinery so they cannot affect overload resolution +or template instantiation; selected specializations replay suppressed +diagnostics when used. Unevaluated and discarded-statement contexts are +skipped. The profile name is passed as ``%0``. + +``checkProfileViolation`` fires at parse time. Inside a template, parse-time +checks follow one unified model. A *non-dependent* construct is checked on +the template *pattern*, at definition time: TreeTransform may return such a +node unchanged at instantiation (for some node kinds, such as casts, a +non-dependent ``Build*`` result is reused), so deferring would silently lose +the diagnostic. This deliberately trades strict "as-if after phase 7" purity +(P3589R2 §1.1) for reuse-proof diagnostics: a non-dependent violation +diagnoses even in a never-instantiated template or in an ``if constexpr`` +branch whose discarding is not yet known at the pattern (a branch already +known discarded -- a non-value-dependent false condition -- stays silent). +An *instantiation-dependent* construct cannot be checked on the pattern; it +is always rebuilt at instantiation, where the re-run ``Build*`` checks the +substituted form, once per specialization. A construct with non-dependent +check operands can still be rebuilt at instantiation (a local variable, a +call argument, or a return statement forces a rebuild, for example); the +re-run ``Build*`` then repeats the definition-time diagnostic at the same +location, with an ``in instantiation of ...`` note. This repetition is +accepted for now; ``test::type_cast``'s cast nodes are never rebuilt when +non-dependent, so it never repeats. + +Under ``-fdelayed-template-parsing`` the body of a never-instantiated +template is never parsed at all, so definition-time diagnosis of +non-dependent violations does not occur in that mode. + +Suppression for parse-time check sites is consulted via the +``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` +RAII guards (see :ref:`profiles-internals` below). Profile implementers do +not need to interact with the stack directly. + + +Pattern 2: Post-Parse / CFG-Based Profile +----------------------------------------- + +Used when the rule cannot be checked at a single Sema entry point because it +depends on whole-function analysis -- typically a CFG-based analysis run +after a function body is complete. ``test::uninit_read`` is the in-tree +example: it diagnoses reads of uninitialized variables on top of Clang's +existing CFG-based uninitialized-variables analysis. + +This pattern needs three pieces, all colocated with the analysis pass (the +framework intentionally does not learn the profile name). + +1. **Add the profile to the analysis pass's per-pass opt-in table.** + Each post-parse analysis owns a small table of the profiles that ride it, + one row per profile (profile name, rule name, diagnostic id). The + in-tree example is ``CFGUninitProfiles`` in + ``clang/lib/Sema/AnalysisBasedWarnings.cpp``: + + .. code-block:: c++ + + struct CFGUninitProfileEntry { + StringRef Name; + StringRef Rule; + unsigned DiagID; + }; + constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"my::profile", /*Rule=*/"", diag::err_my_profile_rule}, + }; + +2. **Gate the analysis pass on the table.** + Analysis-based passes in ``AnalysisBasedWarnings.cpp::IssueWarnings`` are + normally run only when their corresponding warning flag is enabled. To + run the pass for an enforced profile even when the underlying warning is + silenced, OR an ``S.anyProfileEnforced(Table)`` check (the shared + ``SemaProfiles::anyProfileEnforced`` gate, also used by the finalization dispatch) + into the existing pass guard. The in-tree example's pass guard becomes + ``hasEnforcedCFGUninitProfile() || !Diags.isIgnored(...)`` (a small + accessor over ``S.anyProfileEnforced(CFGUninitProfiles)``). + +3. **Walk the table in the analysis's diagnostic reporter.** + For each use site the analysis would have warned about, iterate the + table and call + ``SemaProfiles::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``, + which walks parent statements and lexical declaration contexts to honor + ``[[profiles::suppress]]`` on enclosing AST nodes (the post-parse + counterpart to ``ProfileSuppressStack``). Emit the entry's diagnostic + when it returns true, and skip the default warning path. In the + in-tree example (``UninitValsDiagReporter`` in + ``AnalysisBasedWarnings.cpp``): + + .. code-block:: c++ + + for (const auto &U : *vec) { + for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { + if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) + continue; + S.Diag(U.getUser()->getBeginLoc(), E.DiagID) + << E.Name << vd->getDeclName(); + S.Diag(vd->getLocation(), diag::note_var_declared_here) + << vd->getDeclName(); + return; + } + } + +The diagnostic itself is defined with ``ProfileRuleError`` as in pattern 1. + +The post-parse Stmt-walking suppression check is intentionally separate from +the parse-time stack check: by the time CFG analysis runs, the parse stack +has been unwound, so the framework instead walks the AST. The Stmt-walking +overload of ``isProfileSuppressed`` examines: + +- ``AttributedStmt`` ancestors of the use site. +- ``DeclStmt`` ancestors (whose declared ``VarDecl``\ s carry the attribute, + not the enclosing statement). +- The enclosing ``Decl`` chain via ``getLexicalDeclContext()``. + + +Pattern 3: Class-Finalization Profile +------------------------------------- + +Used when the rule applies to a class as a whole and needs to run once after +the class definition is complete -- for example, "every non-private field of +this class must satisfy property X" or "this class's field set must look +like Y." ``test::class_final`` is the in-tree example. + +The dispatch point is ``SemaProfiles::checkProfileViolationsAtClassFinalization``, +called from the end of ``Sema::CheckCompletedCXXClass`` in +``clang/lib/Sema/SemaDeclCXX.cpp``. ``CheckCompletedCXXClass`` is the +single function reached from every class-completion path -- the parser +(``ActOnFinishCXXMemberSpecification``), template instantiation +(``InstantiateClass``), and lambda completion -- so wiring the hook there +covers all of them with no extra plumbing. + +The class-finalization entry point +``checkProfileViolationsAtClassFinalization`` filters out classes the rules +are not meant to see: + +- Dependent classes (``isDependentType()``). The hook will re-fire on each + instantiation via the template-instantiation completion path. +- Invalid classes (``isInvalidDecl()``). +- Lambdas (``isLambda()``). Closure types have no user-controlled field + shape, so class-finalization rules do not apply. + +This pattern needs two pieces, both colocated with the dispatcher. + +1. **Add the profile to the class-finalization opt-in table.** + ``ClassFinalizationProfiles`` in ``clang/lib/Sema/SemaProfiles.cpp`` is a + small per-pass table of profile name plus callback. One row per profile: + + .. code-block:: c++ + + // FinalizationProfile is shared with pattern 4. + template struct FinalizationProfile { + StringRef Name; + void (*Callback)(Sema &, Node *); + }; + constexpr FinalizationProfile ClassFinalizationProfiles[] = { + {"my::profile", &runMyProfileCallback}, + }; + + The shared ``dispatchFinalizationProfiles`` dispatcher (used by both + patterns 3 and 4) checks ``anyProfileEnforced(Table)``, iterates the + table, skips entries whose profile is not enforced, and invokes the + callback. Each callback passes the finalized ``Decl`` (here the + ``CXXRecordDecl``) to the decl-aware ``shouldEmitProfileViolation`` + overload, which walks the declaration and its lexical parents for a + matching ``[[profiles::suppress]]``, so suppression on the class or any + enclosing lexical ``Decl`` works without the dispatcher establishing a + suppress scope. Finalization can run as a side effect of an *unrelated* + template instantiation whose ``[[profiles::suppress]]`` scope is still on + the transient parse-time ``ProfileSuppressStack``; because stack entries + are matched against the violation's location (see + :ref:`profiles-token-dominion`), such a scope -- whose construct's tokens + do not cover the finalized class -- does not suppress the callback's + diagnostics. + +2. **Emit diagnostics from the callback via** + ``SemaProfiles::shouldEmitProfileViolation``. Each callback decides where on + the class to point and which diagnostic to use, possibly with notes: + + .. code-block:: c++ + + void runMyProfileCallback(Sema &S, CXXRecordDecl *RD) { + if (!S.shouldEmitProfileViolation("my::profile", /*Rule=*/"", + RD->getLocation())) + return; + S.Diag(RD->getLocation(), diag::err_my_profile_rule) + << "my::profile" << RD; + } + +The diagnostic itself is defined with ``ProfileRuleError`` as in patterns 1 +and 2. + +Class-finalization is for **structural** rules -- those answerable from the +class's declared members, their types, and their attributes. The callbacks +run while the ``CXXRecordDecl`` is being finalized (immediately before +``CheckCompletedCXXClass`` returns), which is *before any constructor body or +member-initializer list has been parsed* -- inline member bodies are +late-parsed afterward, and out-of-line and template member constructors later +still. A class-finalization callback therefore must not inspect a +constructor's ``inits()`` (they are empty here). Rules that depend on what a +constructor initializes belong on the constructor-finalization dispatch +(pattern 4); rules that need whole-function flow analysis belong on a +post-parse CFG pass (pattern 2). + + +Pattern 4: Constructor-Finalization Profile +------------------------------------------- + +Used when the rule applies to a single constructor and needs that +constructor's complete member-initializer list -- for example, "every member +must be initialized by this constructor." ``test::ctor_final`` is the in-tree +example, and the ``std::init`` ``ctor_uninit_member`` rule is the real one. + +The dispatch point is ``SemaProfiles::checkProfileViolationsAtConstructorFinalization``, +called right after ``DiagnoseUninitializedFields`` in +``Sema::ActOnMemInitializers`` and ``Sema::ActOnDefaultCtorInitializers`` in +``clang/lib/Sema/SemaDeclCXX.cpp``. Those two functions are the funnel for +every user-defined constructor -- written or implicit member-initializer +list, inline or out-of-line -- and template instantiation reaches the first +of them through ``Sema::InstantiateMemInitializers``, so the hook sees every +constructor at the point its ``inits()`` (including synthesized entries) is +complete. + +The constructor-finalization entry point +``checkProfileViolationsAtConstructorFinalization`` filters out constructors +the rules are not meant to see: + +- Dependent constructors (``isDependentContext()``). The hook re-fires on + each instantiation. +- Invalid constructors (``isInvalidDecl()``). +- Delegating constructors (``isDelegatingConstructor()``), which leave member + initialization to their target. + +The two pieces mirror pattern 3 and share its machinery: a per-pass opt-in +table ``ConstructorFinalizationProfiles`` of the same +``FinalizationProfile`` row (here +``FinalizationProfile``), and a callback that emits via +``SemaProfiles::shouldEmitProfileViolation``. The same shared +``dispatchFinalizationProfiles`` dispatcher invokes each callback, which +passes the ``CXXConstructorDecl`` to the decl-aware +``shouldEmitProfileViolation`` overload; that overload walks the declaration +and its lexical parents, so ``[[profiles::suppress]]`` on the constructor, +the class, or an enclosing lexical ``Decl`` works. As for pattern 3, a +transient parse-time suppress scope belonging to an unrelated construct does +not reach these callbacks, because stack entries only match violations whose +tokens their construct covers (see :ref:`profiles-token-dominion`). A +constructor body is normally instantiated lazily -- outside any unrelated +suppress scope -- so for pattern 4 this matters rarely; the scenario it +actually handles arises in pattern 3, where a class completes synchronously +inside an enclosing instantiation. A callback that should only apply to +user-written constructors checks ``Ctor->isUserProvided()``. + +.. _profiles-internals: + +Framework Internals Reference +============================= + +This section describes the framework mechanisms that profile implementers +benefit from understanding, even though they do not interact with them directly. + +``ProfileSuppressScope`` +------------------------ + +An RAII guard that pushes suppression entries onto ``Sema::ProfileSuppressStack`` +and pops them on destruction. It is used by the parser and template +instantiation machinery to make ``[[profiles::suppress]]`` attributes active +during the appropriate region. Each entry records the token range of the +construct its attribute appertains to (the end only once the construct is +fully parsed) and matches only violations located within it (see +:ref:`profiles-token-dominion`). ``checkProfileViolation`` consults +``ProfileSuppressStack`` directly, so profile implementers never need to +create ``ProfileSuppressScope`` objects. + +Stmt-Tree Suppression Walker +---------------------------- + +The ``isProfileSuppressed(name, rule, Stmt*, AnalysisDeclContext&)`` overload +is the post-parse counterpart to ``ProfileSuppressStack``. It is used by +analyses that run after parsing (when the parse-time stack no longer +reflects the enclosing region) and walks the AST upward from a use site to +find any matching ``[[profiles::suppress]]`` attribute on an enclosing +``AttributedStmt``, ``DeclStmt``-declared ``VarDecl``, or lexical +``Decl`` parent. + +Template Instantiation +---------------------- + +During template instantiation, the framework ensures that +``[[profiles::suppress]]`` on the template pattern and its lexical parents +applies to instantiated code. This is done via ``ProfileSuppressScope`` with +``WalkLexicalParents=true`` at several sites: + +- ``SemaTemplateInstantiateDecl.cpp`` -- function and variable template + instantiation. +- ``SemaTemplateInstantiate.cpp`` -- default member initializer instantiation. +- ``TreeTransform.h`` -- ``TransformAttributedStmt`` (suppress on statements) + and ``TransformDeclStmt`` (suppress on declarations within a ``DeclStmt``). + +The reverse direction is *not* propagated: a suppress scope live at the +*point of instantiation* (for example on the declaration whose initializer +triggers it) covers the trigger's tokens, not the pattern's. Instantiated +code retains the pattern's source locations, so the dominion check on stack +entries (see :ref:`profiles-token-dominion`) keeps such a scope from +suppressing checks that fire inside a synchronously instantiated body, NSDMI, +default argument, or instantiated marker re-check. + +Module Enforcement +------------------ + +``[[profiles::enforce(...)]]`` on a module interface declaration records the +enforced profile designators on ``Module::EnforcedProfileDesignators``. A +(non-partition) module implementation unit ``module M;`` automatically inherits +the interface's enforcements, because it implicitly imports the primary +interface unit of ``M``. +``[[profiles::require(...)]]`` on an import-declaration validates that the +imported module's ``EnforcedProfileDesignators`` contains a matching designator. + +A *header unit* participates the same way: ``[[profiles::enforce(...)]]`` on +an empty-declaration in the header (the form P3589R2 §2.3 prescribes for +header units) is recorded on the header-unit module and serialized into its +BMI, so ``[[profiles::require]]`` on an ``import "header.h";`` validates +against it. As with named modules, importing an enforced header unit does not +enforce the profile in the importer. + +``[[profiles::enforce(...)]]`` on a *non-interface* module-declaration (a +``module M;`` implementation unit, or a ``module M:P;`` partition +implementation unit) is accepted but recorded only translation-unit-locally; +it is **not** added to ``Module::EnforcedProfileDesignators`` and so is not +visible to an importer's ``[[profiles::require]]``. + +A module partition implementation unit ``module M:P;`` is also a module +implementation unit of ``M``, so the primary interface's enforcements apply to +it as well. However, it does **not** implicitly import the primary interface, +and the primary interface is normally compiled *after* its partitions, so its +BMI is usually not available when the partition implementation unit is compiled. +Inheritance here is therefore **best-effort**: the enforcements are inherited +only when the primary interface's BMI is already resident in the compilation +(for example, supplied via an eager ``-fmodule-file=``); the BMI is never +force-loaded and its absence is never diagnosed. When it is not available the +partition implementation unit is simply not subject to the inherited profile -- +a missed diagnostic, never a change to the meaning of a well-formed program. +For guaranteed enforcement, **repeat** ``[[profiles::enforce(...)]]`` in the +partition implementation unit rather than relying on inheritance. (Best-effort +inheritance is silent when the interface BMI is absent; if the BMI *is* resident +and enforces a profile whose designator conflicts with a locally repeated +``enforce`` of the same name, that mismatch is still diagnosed.) + +Importing a module that enforces a profile does **not** enforce that profile in +the importing translation unit. Enforcement is always explicit and local. + +Redeclaration Compatibility +--------------------------- + +P3589R2 [decl.attr.enforce]p5: a declaration and its redeclarations must +appear in the dominions of mutually compatible profiles. The rule is +**symmetric** -- when a redeclaration is merged with a previous declaration +from another module unit, every profile whose dominion covered the previous +declaration must have a compatible counterpart covering the redeclaration, +and vice versa. In particular, a profile-enforcing translation unit that +redeclares an entity from a module (or header unit) compiled *without* a +compatible profile is ill-formed; the paper's escape hatch for such headers +is ``[[profiles::exempt]]`` (not yet implemented, see the Intentional +Omissions section of :doc:`ProfilesFramework`). The check +(``SemaProfiles::checkRedeclarationProfileCompatibility``) runs from +``Sema::CheckRedeclarationInModule``, the funnel for function, variable, tag, +alias, and class-template redeclarations; it is a framework rule -- a plain +error, not suppressible with ``[[profiles::suppress]]``, and diagnose-only +(the redeclaration still merges). + +Two profiles are *compatible* if they have the same name -- designator +arguments configure a profile without changing its identity -- or if both are +standard (``std::``-prefixed) profiles, which P3589R2 proclaims mutually +compatible. No further implementation-proclaimed compatibility is modeled. + +The previous declaration's dominion is approximated by its top-level module's +exported ``EnforcedProfileDesignators``, which is exact for declarations in +the module purview -- including purview ``extern "C"``/``extern "C++"`` +declarations (implicit global module), the common redeclarable case, since +module-attached entities cannot be redeclared in other translation units at +all. Two cases have an *unknown* dominion and are skipped rather than +guessed at (a missed diagnostic, never a wrong one): + +- A declaration in an **explicit global module fragment**: it precedes the + module-declaration, so the exported enforcements do not cover it, and its + TU's empty-declaration enforces are not serialized into the BMI. +- A previous declaration from the **same module family** (an implementation + or partition unit merging with its own interface): the exported set + under-approximates the interface TU's full dominion, and the interface's + enforcements are inherited into the current unit anyway, so checking would + false-positive on locally added profiles. + +A textual or PCH previous declaration is not checked at all: it shares the +current TU's dominion (the placement rule makes a TU's dominion uniform, and +a PCH's enforcements are restored into the including TU). Implicit template +instantiations are exempt, matching the module-ownership check. + +Serialization +------------- + +The framework serializes enforcement state automatically. Profile implementers +do not need to add any serialization code. + +- **PCH**: ``SemaProfiles::EnforcedProfiles`` is written as ``ENFORCED_PROFILES`` + records in the AST bitstream and restored when the PCH is loaded. +- **Module BMI**: ``Module::EnforcedProfileDesignators`` is written as + ``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. + +Test Profiles +============= + +The built-in ``test::`` profiles exist only to +exercise the framework and are additionally gated on the ``-fprofiles-test-profiles`` +flag, which sets ``LangOpts.ProfilesTestProfiles``. This flag is ``-cc1``-only +(not exposed by the driver) and is intended solely for running the test suite. +Under ``-fprofiles`` alone, ``[[profiles::enforce(test::...)]]`` is still +recognized (it is not ``warn_attribute_ignored``) and its designator is still +recorded and exported across modules, but ``SemaProfiles::isProfileEnforced`` reports +any ``test::``-prefixed profile as not enforced, so no ``test::`` rule ever +fires. Real profiles such as ``std::init`` are unaffected by this flag. + +By convention: + +- Real test profiles live under the ``test::`` namespace. Today there are + four: ``test::type_cast``, ``test::uninit_read``, ``test::class_final``, + and ``test::ctor_final``. Because the ``test::`` prefix is what + ``SemaProfiles::isProfileEnforced`` keys on to gate them behind + ``-fprofiles-test-profiles``, any new test-only profile must also live + under ``test::``. +- The names ``test::other``, ``test::bounds``, ``test::new_profile``, and + ``test::not_enforced`` are deliberately *not* implemented and appear only + in negative tests as stand-in "some other profile" names. Adding a real + profile under any of these names would invalidate those tests. + +The ``test::type_cast`` Profile +------------------------------- + +A pattern-1 (Sema check-site) profile. Demonstrates the simple case where +a rule can be checked from a single Sema entry point. + +- **Rules**: ``reinterpret_cast``. +- **Diagnostic**: ``err_profile_type_cast_reinterpret`` + ("'reinterpret_cast' is unsafe under profile '%0'"). +- **Check site**: ``Sema::BuildCXXNamedCast`` in ``clang/lib/Sema/SemaCast.cpp``, + inside the ``reinterpret_cast`` arm. Only the ``reinterpret_cast<>`` keyword + form is checked; a C-style or functional cast with reinterpret semantics goes + through a different path and is not diagnosed. + +The entire profile implementation is the single call: + +.. code-block:: c++ + + checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, + diag::err_profile_type_cast_reinterpret); + + +The ``test::uninit_read`` Profile +--------------------------------- + +A pattern-2 (post-parse / CFG-based) profile. Demonstrates the case where +the rule depends on whole-function analysis and must run after parsing. +It diagnoses reads of uninitialized variables by reusing Clang's existing +CFG-based uninitialized-variables analysis. + +- **Rules**: none (the profile has a single implicit rule, so the rule + string is empty). +- **Diagnostic**: ``err_profile_uninit_read`` + ("variable %1 is read before initialization under profile '%0'"). + A companion ``note_var_declared_here`` is emitted at the variable's + declaration. +- **Opt-in table**: ``CFGUninitProfiles`` in + ``clang/lib/Sema/AnalysisBasedWarnings.cpp``. The ``IssueWarnings`` pass + guard consults it via ``hasEnforcedCFGUninitProfile()`` so the analysis + runs even when ``-Wuninitialized`` is silenced, and + ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the + default warning path -- when an entry's + ``SemaProfiles::shouldEmitProfileViolation`` returns true the entry's diagnostic + fires and the default warning is skipped entirely. + +The Stmt-tree suppression walker is what makes ``[[profiles::suppress]]`` +work for this profile: by the time the CFG analysis runs, the parse-time +``ProfileSuppressStack`` has been unwound, so the helper consults the AST +directly via ``shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``. + + +The ``test::class_final`` Profile +--------------------------------- + +A pattern-3 (class-finalization) profile. Demonstrates the case where the +rule applies once per completed class definition and runs from the +class-finalization dispatch in ``Sema::CheckCompletedCXXClass``. + +- **Rules**: none (the profile has a single implicit rule, so the rule + string is empty). +- **Diagnostic**: ``err_profile_class_final_test`` ("test profile fired on + completion of class %1 under profile '%0'"). +- **Opt-in table**: ``ClassFinalizationProfiles`` in + ``clang/lib/Sema/SemaProfiles.cpp``. + +Because dependent classes are filtered out by the dispatcher, the +diagnostic fires on class template *instantiations* rather than on the +primary template. Lambda closures are also skipped. +``[[profiles::suppress(test::class_final)]]`` on the class or any +enclosing lexical ``Decl`` silences the diagnostic via the decl-aware +``shouldEmitProfileViolation`` overload, which walks the class and its +lexical parents for a matching suppression. + + +The ``test::ctor_final`` Profile +-------------------------------- + +A pattern-4 (constructor-finalization) profile. Demonstrates the case where +the rule applies once per user-defined constructor, after its +member-initializer list is complete. + +- **Rules**: none (single implicit rule, empty rule string). +- **Diagnostic**: ``err_profile_ctor_final_test`` ("test profile fired on + finalization of a constructor for class %1 under profile '%0'"). +- **Opt-in table**: ``ConstructorFinalizationProfiles`` in + ``clang/lib/Sema/SemaProfiles.cpp``. + +The diagnostic fires once per user-defined constructor -- written or implicit +member-initializer list, inline or out-of-line -- and on constructor template +*instantiations* rather than the dependent pattern. Defaulted and implicit +constructors (no body) and delegating constructors are skipped. diff --git a/clang/docs/index.rst b/clang/docs/index.rst index 1fd752fe08bc1..c7f0691b3cbff 100644 --- a/clang/docs/index.rst +++ b/clang/docs/index.rst @@ -32,6 +32,7 @@ Using Clang as a Compiler DataFlowAnalysisIntro FunctionEffectAnalysis ProfilesFramework + ProfilesFrameworkInternals AddressSanitizer ThreadSanitizer MemorySanitizer diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index a9ef5b2846dd2..3586fc5457a76 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -8,7 +8,8 @@ /// \file /// This file declares semantic analysis for the C++ profiles framework /// (P3589R2) and the built-in std::init initialization profile (P4222R1.1). -/// See clang/docs/ProfilesFramework.rst for the design. +/// See clang/docs/ProfilesFrameworkInternals.rst for the design and +/// clang/docs/ProfilesFramework.rst for the user-facing documentation. /// //===----------------------------------------------------------------------===// diff --git a/clang/lib/Sema/SemaCast.cpp b/clang/lib/Sema/SemaCast.cpp index deec789eac1be..d6e3a0438cd63 100644 --- a/clang/lib/Sema/SemaCast.cpp +++ b/clang/lib/Sema/SemaCast.cpp @@ -401,7 +401,8 @@ Sema::BuildCXXNamedCast(SourceLocation OpLoc, tok::TokenKind Kind, if (Op.SrcExpr.isInvalid()) return ExprError(); DiscardMisalignedMemberAddress(DestType.getTypePtr(), E); - // test::type_cast is a built-in test profile; see ProfilesFramework.rst. + // test::type_cast is a built-in test profile; see + // ProfilesFrameworkInternals.rst. Profiles().checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, diag::err_profile_type_cast_reinterpret); diff --git a/clang/lib/Sema/SemaModule.cpp b/clang/lib/Sema/SemaModule.cpp index ca99eb08732bb..d92524f105a12 100644 --- a/clang/lib/Sema/SemaModule.cpp +++ b/clang/lib/Sema/SemaModule.cpp @@ -497,7 +497,7 @@ Sema::ActOnModuleDecl(SourceLocation StartLoc, SourceLocation ModuleLoc, // this is best-effort: inherit only when the interface is already resident; // never force a load and never diagnose its absence. For guaranteed // enforcement a partition implementation unit should repeat - // [[profiles::enforce]] (see ProfilesFramework.rst). + // [[profiles::enforce]] (see ProfilesFrameworkInternals.rst). if (Module *Primary = PP.getHeaderSearchInfo().getModuleMap().findModule( Mod->getPrimaryModuleInterfaceName())) for (const auto &EP : Primary->EnforcedProfileDesignators) diff --git a/clang/test/SemaCXX/safety-profile-header-unit.cpp b/clang/test/SemaCXX/safety-profile-header-unit.cpp index c5e2d1258fcbc..b9e403379a976 100644 --- a/clang/test/SemaCXX/safety-profile-header-unit.cpp +++ b/clang/test/SemaCXX/safety-profile-header-unit.cpp @@ -19,9 +19,9 @@ // RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -Wno-experimental-header-units -fsyntax-only %t/import_args_fail.cpp -fmodule-file=%t/args.pcm -verify // // The -fprofiles-test-profiles gate only controls whether test:: rules fire; -// the designator is recorded and exported regardless (see the Driver Flag -// section of ProfilesFramework.rst), so require validates identically under -// plain -fprofiles. +// the designator is recorded and exported regardless (see the Test Profiles +// section of ProfilesFrameworkInternals.rst), so require validates identically +// under plain -fprofiles. // RUN: %clang_cc1 -std=c++20 -fprofiles -emit-header-unit -xc++-user-header %t/enforced.h -o %t/enforced-noflag.pcm // RUN: %clang_cc1 -std=c++20 -fprofiles -Wno-experimental-header-units -fsyntax-only %t/import_ok.cpp -fmodule-file=%t/enforced-noflag.pcm -verify // From 1574a0f4e52bc62c0244060141f67ea22947093b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 16:56:36 -0400 Subject: [PATCH 243/289] Condense the profiles internals doc to design-doc register --- clang/docs/ProfilesFrameworkInternals.rst | 810 +++++++--------------- 1 file changed, 253 insertions(+), 557 deletions(-) diff --git a/clang/docs/ProfilesFrameworkInternals.rst b/clang/docs/ProfilesFrameworkInternals.rst index a8c6138cbeb1c..052f0a044aad1 100644 --- a/clang/docs/ProfilesFrameworkInternals.rst +++ b/clang/docs/ProfilesFrameworkInternals.rst @@ -3,7 +3,7 @@ C++ Profiles Framework Internals ==================================== .. contents:: - :depth: 3 + :depth: 2 :local: @@ -13,587 +13,283 @@ for Clang contributors -- in particular, how to add a new profile. For user-facing documentation of the feature, see :doc:`ProfilesFramework`. -Implementing a New Profile -========================== +Architecture +============ -Adding a new profile requires no changes to the framework itself. A profile is -defined entirely by: +The framework is profile-agnostic. Profile names are opaque strings and +there is no central registry: a profile is "enforced" simply because the user +wrote ``[[profiles::enforce(name)]]``, and each rule within a profile is just +a string identifier that ``[[profiles::suppress(name, rule: "...")]]`` can +target. ``SemaProfiles`` owns all the bookkeeping -- attribute parsing and +placement checking, enforcement tracking, suppression scoping, template +instantiation, module propagation, and PCH/BMI serialization -- so a profile +implementation consists only of its diagnostics plus calls to the framework +at its semantic check sites. -1. A **profile name** (a ``::``-separated identifier sequence such as - ``vendor::safety`` or ``std::type``). -2. Zero or more **rule names** (string identifiers such as - ``"reinterpret_cast"``). -3. **Diagnostics** emitted when a rule is violated. -4. **Check sites** in the compiler where the framework is consulted. +Profile-rule diagnostics are defined with the ``ProfileRuleError`` diagnostic +class rather than ``Error``. It marks them SFINAE-suppressed: they do not +count as substitution failures and cannot change overload resolution, but +selected specializations replay them when actually used. The framework +passes the profile name as ``%0``: -There is no central registry of profiles. The framework treats profile names -as opaque strings; a profile is considered "enforced" simply because the user -wrote ``[[profiles::enforce(name)]]`` in their source. Each rule within a -profile is likewise just a string identifier; users can suppress individual -rules with ``[[profiles::suppress(profile_name, rule: "rule_name")]]``. +.. code-block:: text -There are two implementation patterns, depending on when the rule is checked. + def err_profile_type_cast_reinterpret : ProfileRuleError< + "'reinterpret_cast' is unsafe under profile '%0'">; +There are four implementation patterns, keyed on when the rule can be +checked. -Define Diagnostics ------------------- -Add diagnostics to ``clang/include/clang/Basic/DiagnosticSemaKinds.td`` in the -``// C++ Profiles framework (P3589R2)`` group. Each diagnostic should accept -``%0`` for the profile name, since the framework passes the profile name as -the first diagnostic argument. The group declares the helper class +Pattern 1: Parse-Time Check Sites +================================= -.. code-block:: text +For a rule checkable at a single semantic entry point, the entire profile +implementation is one call at that site: - class ProfileRuleError : Error { - let SFINAE = SFINAE_Suppress; - } +.. code-block:: c++ -so that profile-rule diagnostics participate in the SFINAE machinery as -suppressed errors -- they do not count as substitution failures and cannot -change overload resolution, but selected specializations replay them when -they are actually used. Define new rules using ``ProfileRuleError`` rather -than ``Error`` directly: + checkProfileViolation("my::profile", "my_rule", Loc, + diag::err_my_profile_rule); -.. code-block:: text +The call checks that the profile is enforced and not suppressed (via the +parse-time suppress stack) and skips unevaluated and discarded-statement +contexts. ``test::type_cast`` is the in-tree example. + +Inside a template, parse-time checks follow one unified model. A +*non-dependent* construct is checked on the template pattern, at definition +time: instantiation may reuse such a node unchanged, so deferring would +silently lose the diagnostic. This deliberately trades strict "as-if after +phase 7" purity (P3589R2 §1.1) for reuse-proof diagnostics. An +*instantiation-dependent* construct is always rebuilt at instantiation, where +the re-run check sees the substituted form, once per specialization. A +non-dependent construct that happens to be rebuilt anyway repeats its +definition-time diagnostic with an ``in instantiation of ...`` note -- an +accepted duplication. Under ``-fdelayed-template-parsing`` the body of a +never-instantiated template is never parsed, so definition-time diagnosis of +non-dependent violations does not occur in that mode. - def err_profile_type_cast_reinterpret : ProfileRuleError< - "'reinterpret_cast' is unsafe under profile '%0'">; +Pattern 2: Post-Parse / CFG-Based +================================= -Pattern 1: Sema Check-Site Profile ----------------------------------- +For a rule that needs whole-function analysis. Each post-parse analysis owns +a small opt-in table of the profiles that ride it, one row per profile +(profile name, rule name, diagnostic); the framework never learns the +profile's name. ``test::uninit_read`` is the in-tree example: -Used when the rule can be checked at a single, well-defined parse-time site -in Sema (typically inside a ``Sema::Build*`` or ``Sema::Act*`` routine). -``test::type_cast`` is the in-tree example. +.. code-block:: c++ -At each such site, call ``SemaProfiles::checkProfileViolation``: + constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { + {"my::profile", /*Rule=*/"", diag::err_my_profile_rule}, + }; -.. code-block:: c++ +The analysis's pass guard ORs in ``anyProfileEnforced(Table)`` so the pass +runs for an enforced profile even when the corresponding warning is +silenced, and the analysis's diagnostic reporter walks the table calling +``shouldEmitProfileViolation(Name, Rule, Stmt, AnalysisDeclContext)`` per use +site, emitting the entry's diagnostic (and skipping the default warning) when +it returns true. - checkProfileViolation("my::profile", "my_rule", Loc, - diag::err_my_profile_rule); +That overload is the post-parse counterpart of the parse-time suppress +stack: by the time the analysis runs the stack has unwound, so it walks the +AST upward from the use site -- enclosing ``AttributedStmt``\ s, +``DeclStmt``-declared variables, and the lexical ``Decl`` chain -- for a +matching ``[[profiles::suppress]]``. -This function checks whether the profile is enforced and not suppressed. -During template argument deduction, profile rule diagnostics are suppressed -by Clang's normal SFINAE machinery so they cannot affect overload resolution -or template instantiation; selected specializations replay suppressed -diagnostics when used. Unevaluated and discarded-statement contexts are -skipped. The profile name is passed as ``%0``. - -``checkProfileViolation`` fires at parse time. Inside a template, parse-time -checks follow one unified model. A *non-dependent* construct is checked on -the template *pattern*, at definition time: TreeTransform may return such a -node unchanged at instantiation (for some node kinds, such as casts, a -non-dependent ``Build*`` result is reused), so deferring would silently lose -the diagnostic. This deliberately trades strict "as-if after phase 7" purity -(P3589R2 §1.1) for reuse-proof diagnostics: a non-dependent violation -diagnoses even in a never-instantiated template or in an ``if constexpr`` -branch whose discarding is not yet known at the pattern (a branch already -known discarded -- a non-value-dependent false condition -- stays silent). -An *instantiation-dependent* construct cannot be checked on the pattern; it -is always rebuilt at instantiation, where the re-run ``Build*`` checks the -substituted form, once per specialization. A construct with non-dependent -check operands can still be rebuilt at instantiation (a local variable, a -call argument, or a return statement forces a rebuild, for example); the -re-run ``Build*`` then repeats the definition-time diagnostic at the same -location, with an ``in instantiation of ...`` note. This repetition is -accepted for now; ``test::type_cast``'s cast nodes are never rebuilt when -non-dependent, so it never repeats. - -Under ``-fdelayed-template-parsing`` the body of a never-instantiated -template is never parsed at all, so definition-time diagnosis of -non-dependent violations does not occur in that mode. -Suppression for parse-time check sites is consulted via the -``ProfileSuppressStack`` maintained by the parser-side ``ProfileSuppressScope`` -RAII guards (see :ref:`profiles-internals` below). Profile implementers do -not need to interact with the stack directly. - - -Pattern 2: Post-Parse / CFG-Based Profile ------------------------------------------ - -Used when the rule cannot be checked at a single Sema entry point because it -depends on whole-function analysis -- typically a CFG-based analysis run -after a function body is complete. ``test::uninit_read`` is the in-tree -example: it diagnoses reads of uninitialized variables on top of Clang's -existing CFG-based uninitialized-variables analysis. - -This pattern needs three pieces, all colocated with the analysis pass (the -framework intentionally does not learn the profile name). - -1. **Add the profile to the analysis pass's per-pass opt-in table.** - Each post-parse analysis owns a small table of the profiles that ride it, - one row per profile (profile name, rule name, diagnostic id). The - in-tree example is ``CFGUninitProfiles`` in - ``clang/lib/Sema/AnalysisBasedWarnings.cpp``: - - .. code-block:: c++ - - struct CFGUninitProfileEntry { - StringRef Name; - StringRef Rule; - unsigned DiagID; - }; - constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { - {"my::profile", /*Rule=*/"", diag::err_my_profile_rule}, - }; - -2. **Gate the analysis pass on the table.** - Analysis-based passes in ``AnalysisBasedWarnings.cpp::IssueWarnings`` are - normally run only when their corresponding warning flag is enabled. To - run the pass for an enforced profile even when the underlying warning is - silenced, OR an ``S.anyProfileEnforced(Table)`` check (the shared - ``SemaProfiles::anyProfileEnforced`` gate, also used by the finalization dispatch) - into the existing pass guard. The in-tree example's pass guard becomes - ``hasEnforcedCFGUninitProfile() || !Diags.isIgnored(...)`` (a small - accessor over ``S.anyProfileEnforced(CFGUninitProfiles)``). - -3. **Walk the table in the analysis's diagnostic reporter.** - For each use site the analysis would have warned about, iterate the - table and call - ``SemaProfiles::shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``, - which walks parent statements and lexical declaration contexts to honor - ``[[profiles::suppress]]`` on enclosing AST nodes (the post-parse - counterpart to ``ProfileSuppressStack``). Emit the entry's diagnostic - when it returns true, and skip the default warning path. In the - in-tree example (``UninitValsDiagReporter`` in - ``AnalysisBasedWarnings.cpp``): - - .. code-block:: c++ - - for (const auto &U : *vec) { - for (const CFGUninitProfileEntry &E : CFGUninitProfiles) { - if (!S.shouldEmitProfileViolation(E.Name, E.Rule, U.getUser(), AC)) - continue; - S.Diag(U.getUser()->getBeginLoc(), E.DiagID) - << E.Name << vd->getDeclName(); - S.Diag(vd->getLocation(), diag::note_var_declared_here) - << vd->getDeclName(); - return; - } - } - -The diagnostic itself is defined with ``ProfileRuleError`` as in pattern 1. - -The post-parse Stmt-walking suppression check is intentionally separate from -the parse-time stack check: by the time CFG analysis runs, the parse stack -has been unwound, so the framework instead walks the AST. The Stmt-walking -overload of ``isProfileSuppressed`` examines: - -- ``AttributedStmt`` ancestors of the use site. -- ``DeclStmt`` ancestors (whose declared ``VarDecl``\ s carry the attribute, - not the enclosing statement). -- The enclosing ``Decl`` chain via ``getLexicalDeclContext()``. - - -Pattern 3: Class-Finalization Profile -------------------------------------- - -Used when the rule applies to a class as a whole and needs to run once after -the class definition is complete -- for example, "every non-private field of -this class must satisfy property X" or "this class's field set must look -like Y." ``test::class_final`` is the in-tree example. - -The dispatch point is ``SemaProfiles::checkProfileViolationsAtClassFinalization``, -called from the end of ``Sema::CheckCompletedCXXClass`` in -``clang/lib/Sema/SemaDeclCXX.cpp``. ``CheckCompletedCXXClass`` is the -single function reached from every class-completion path -- the parser -(``ActOnFinishCXXMemberSpecification``), template instantiation -(``InstantiateClass``), and lambda completion -- so wiring the hook there -covers all of them with no extra plumbing. - -The class-finalization entry point -``checkProfileViolationsAtClassFinalization`` filters out classes the rules -are not meant to see: - -- Dependent classes (``isDependentType()``). The hook will re-fire on each - instantiation via the template-instantiation completion path. -- Invalid classes (``isInvalidDecl()``). -- Lambdas (``isLambda()``). Closure types have no user-controlled field - shape, so class-finalization rules do not apply. - -This pattern needs two pieces, both colocated with the dispatcher. - -1. **Add the profile to the class-finalization opt-in table.** - ``ClassFinalizationProfiles`` in ``clang/lib/Sema/SemaProfiles.cpp`` is a - small per-pass table of profile name plus callback. One row per profile: - - .. code-block:: c++ - - // FinalizationProfile is shared with pattern 4. - template struct FinalizationProfile { - StringRef Name; - void (*Callback)(Sema &, Node *); - }; - constexpr FinalizationProfile ClassFinalizationProfiles[] = { - {"my::profile", &runMyProfileCallback}, - }; - - The shared ``dispatchFinalizationProfiles`` dispatcher (used by both - patterns 3 and 4) checks ``anyProfileEnforced(Table)``, iterates the - table, skips entries whose profile is not enforced, and invokes the - callback. Each callback passes the finalized ``Decl`` (here the - ``CXXRecordDecl``) to the decl-aware ``shouldEmitProfileViolation`` - overload, which walks the declaration and its lexical parents for a - matching ``[[profiles::suppress]]``, so suppression on the class or any - enclosing lexical ``Decl`` works without the dispatcher establishing a - suppress scope. Finalization can run as a side effect of an *unrelated* - template instantiation whose ``[[profiles::suppress]]`` scope is still on - the transient parse-time ``ProfileSuppressStack``; because stack entries - are matched against the violation's location (see - :ref:`profiles-token-dominion`), such a scope -- whose construct's tokens - do not cover the finalized class -- does not suppress the callback's - diagnostics. - -2. **Emit diagnostics from the callback via** - ``SemaProfiles::shouldEmitProfileViolation``. Each callback decides where on - the class to point and which diagnostic to use, possibly with notes: - - .. code-block:: c++ - - void runMyProfileCallback(Sema &S, CXXRecordDecl *RD) { - if (!S.shouldEmitProfileViolation("my::profile", /*Rule=*/"", - RD->getLocation())) - return; - S.Diag(RD->getLocation(), diag::err_my_profile_rule) - << "my::profile" << RD; - } - -The diagnostic itself is defined with ``ProfileRuleError`` as in patterns 1 -and 2. - -Class-finalization is for **structural** rules -- those answerable from the -class's declared members, their types, and their attributes. The callbacks -run while the ``CXXRecordDecl`` is being finalized (immediately before -``CheckCompletedCXXClass`` returns), which is *before any constructor body or -member-initializer list has been parsed* -- inline member bodies are -late-parsed afterward, and out-of-line and template member constructors later -still. A class-finalization callback therefore must not inspect a -constructor's ``inits()`` (they are empty here). Rules that depend on what a -constructor initializes belong on the constructor-finalization dispatch -(pattern 4); rules that need whole-function flow analysis belong on a -post-parse CFG pass (pattern 2). - - -Pattern 4: Constructor-Finalization Profile -------------------------------------------- - -Used when the rule applies to a single constructor and needs that -constructor's complete member-initializer list -- for example, "every member -must be initialized by this constructor." ``test::ctor_final`` is the in-tree -example, and the ``std::init`` ``ctor_uninit_member`` rule is the real one. - -The dispatch point is ``SemaProfiles::checkProfileViolationsAtConstructorFinalization``, -called right after ``DiagnoseUninitializedFields`` in -``Sema::ActOnMemInitializers`` and ``Sema::ActOnDefaultCtorInitializers`` in -``clang/lib/Sema/SemaDeclCXX.cpp``. Those two functions are the funnel for -every user-defined constructor -- written or implicit member-initializer -list, inline or out-of-line -- and template instantiation reaches the first -of them through ``Sema::InstantiateMemInitializers``, so the hook sees every -constructor at the point its ``inits()`` (including synthesized entries) is -complete. - -The constructor-finalization entry point -``checkProfileViolationsAtConstructorFinalization`` filters out constructors -the rules are not meant to see: - -- Dependent constructors (``isDependentContext()``). The hook re-fires on - each instantiation. -- Invalid constructors (``isInvalidDecl()``). -- Delegating constructors (``isDelegatingConstructor()``), which leave member - initialization to their target. - -The two pieces mirror pattern 3 and share its machinery: a per-pass opt-in -table ``ConstructorFinalizationProfiles`` of the same -``FinalizationProfile`` row (here -``FinalizationProfile``), and a callback that emits via -``SemaProfiles::shouldEmitProfileViolation``. The same shared -``dispatchFinalizationProfiles`` dispatcher invokes each callback, which -passes the ``CXXConstructorDecl`` to the decl-aware -``shouldEmitProfileViolation`` overload; that overload walks the declaration -and its lexical parents, so ``[[profiles::suppress]]`` on the constructor, -the class, or an enclosing lexical ``Decl`` works. As for pattern 3, a -transient parse-time suppress scope belonging to an unrelated construct does -not reach these callbacks, because stack entries only match violations whose -tokens their construct covers (see :ref:`profiles-token-dominion`). A -constructor body is normally instantiated lazily -- outside any unrelated -suppress scope -- so for pattern 4 this matters rarely; the scenario it -actually handles arises in pattern 3, where a class completes synchronously -inside an enclosing instantiation. A callback that should only apply to -user-written constructors checks ``Ctor->isUserProvided()``. - -.. _profiles-internals: - -Framework Internals Reference -============================= - -This section describes the framework mechanisms that profile implementers -benefit from understanding, even though they do not interact with them directly. - -``ProfileSuppressScope`` ------------------------- - -An RAII guard that pushes suppression entries onto ``Sema::ProfileSuppressStack`` -and pops them on destruction. It is used by the parser and template -instantiation machinery to make ``[[profiles::suppress]]`` attributes active -during the appropriate region. Each entry records the token range of the -construct its attribute appertains to (the end only once the construct is -fully parsed) and matches only violations located within it (see -:ref:`profiles-token-dominion`). ``checkProfileViolation`` consults -``ProfileSuppressStack`` directly, so profile implementers never need to -create ``ProfileSuppressScope`` objects. - -Stmt-Tree Suppression Walker ----------------------------- - -The ``isProfileSuppressed(name, rule, Stmt*, AnalysisDeclContext&)`` overload -is the post-parse counterpart to ``ProfileSuppressStack``. It is used by -analyses that run after parsing (when the parse-time stack no longer -reflects the enclosing region) and walks the AST upward from a use site to -find any matching ``[[profiles::suppress]]`` attribute on an enclosing -``AttributedStmt``, ``DeclStmt``-declared ``VarDecl``, or lexical -``Decl`` parent. - -Template Instantiation ----------------------- - -During template instantiation, the framework ensures that -``[[profiles::suppress]]`` on the template pattern and its lexical parents -applies to instantiated code. This is done via ``ProfileSuppressScope`` with -``WalkLexicalParents=true`` at several sites: - -- ``SemaTemplateInstantiateDecl.cpp`` -- function and variable template - instantiation. -- ``SemaTemplateInstantiate.cpp`` -- default member initializer instantiation. -- ``TreeTransform.h`` -- ``TransformAttributedStmt`` (suppress on statements) - and ``TransformDeclStmt`` (suppress on declarations within a ``DeclStmt``). - -The reverse direction is *not* propagated: a suppress scope live at the -*point of instantiation* (for example on the declaration whose initializer -triggers it) covers the trigger's tokens, not the pattern's. Instantiated -code retains the pattern's source locations, so the dominion check on stack -entries (see :ref:`profiles-token-dominion`) keeps such a scope from -suppressing checks that fire inside a synchronously instantiated body, NSDMI, -default argument, or instantiated marker re-check. - -Module Enforcement ------------------- - -``[[profiles::enforce(...)]]`` on a module interface declaration records the -enforced profile designators on ``Module::EnforcedProfileDesignators``. A -(non-partition) module implementation unit ``module M;`` automatically inherits -the interface's enforcements, because it implicitly imports the primary -interface unit of ``M``. -``[[profiles::require(...)]]`` on an import-declaration validates that the -imported module's ``EnforcedProfileDesignators`` contains a matching designator. - -A *header unit* participates the same way: ``[[profiles::enforce(...)]]`` on -an empty-declaration in the header (the form P3589R2 §2.3 prescribes for -header units) is recorded on the header-unit module and serialized into its -BMI, so ``[[profiles::require]]`` on an ``import "header.h";`` validates -against it. As with named modules, importing an enforced header unit does not -enforce the profile in the importer. - -``[[profiles::enforce(...)]]`` on a *non-interface* module-declaration (a -``module M;`` implementation unit, or a ``module M:P;`` partition -implementation unit) is accepted but recorded only translation-unit-locally; -it is **not** added to ``Module::EnforcedProfileDesignators`` and so is not -visible to an importer's ``[[profiles::require]]``. - -A module partition implementation unit ``module M:P;`` is also a module -implementation unit of ``M``, so the primary interface's enforcements apply to -it as well. However, it does **not** implicitly import the primary interface, -and the primary interface is normally compiled *after* its partitions, so its -BMI is usually not available when the partition implementation unit is compiled. -Inheritance here is therefore **best-effort**: the enforcements are inherited -only when the primary interface's BMI is already resident in the compilation -(for example, supplied via an eager ``-fmodule-file=``); the BMI is never -force-loaded and its absence is never diagnosed. When it is not available the -partition implementation unit is simply not subject to the inherited profile -- -a missed diagnostic, never a change to the meaning of a well-formed program. -For guaranteed enforcement, **repeat** ``[[profiles::enforce(...)]]`` in the -partition implementation unit rather than relying on inheritance. (Best-effort -inheritance is silent when the interface BMI is absent; if the BMI *is* resident -and enforces a profile whose designator conflicts with a locally repeated -``enforce`` of the same name, that mismatch is still diagnosed.) - -Importing a module that enforces a profile does **not** enforce that profile in -the importing translation unit. Enforcement is always explicit and local. +Patterns 3 and 4: Class and Constructor Finalization +==================================================== + +For rules that run once per completed class definition (pattern 3, +``test::class_final``) or once per user-defined constructor with its complete +member-initializer list (pattern 4, ``test::ctor_final``). Both share one +dispatcher and one per-pass table shape: + +.. code-block:: c++ + + constexpr FinalizationProfile ClassFinalizationProfiles[] = { + {"my::profile", &runMyProfileCallback}, + }; + +The class hook runs from the single function every class-completion path +funnels through (parsing, template instantiation, lambda completion); the +constructor hook runs from the two functions every constructor's +member-initializer list funnels through, including instantiation. The +dispatchers filter out dependent entities (the hooks re-fire on each +instantiation), invalid ones, lambdas (pattern 3), and delegating +constructors (pattern 4). Each callback gates its diagnostics on the +decl-aware ``shouldEmitProfileViolation`` overload, which walks the finalized +declaration and its lexical parents for a suppression. + +The split between the two patterns matters: class finalization runs *before +any constructor body or member-initializer list has been parsed*, so a +pattern-3 callback must not inspect a constructor's ``inits()``. Rules that +depend on what a constructor initializes belong on pattern 4; rules that need +flow analysis belong on pattern 2. + + +Suppression Dominion Mechanics +============================== + +A ``[[profiles::suppress]]`` attribute's dominion is the token range of the +construct it appertains to (the user-level rule is stated in +:doc:`ProfilesFramework`). The parse-time suppress stack -- pushed and +popped by ``ProfileSuppressScope`` RAII guards in the parser and the +template-instantiation machinery -- enforces this positionally: each entry +records its construct's token range, and a violation matches an entry only if +its location falls within that range. This keeps a live suppress scope from +leaking into code its tokens do not cover, which would otherwise happen in +two ways: a template pattern instantiated synchronously while the scope is +live (instantiated code retains the pattern's source locations), and a class +or constructor finalized as a side effect of such an instantiation. +Conversely, a local class or lambda defined *inside* the suppressed construct +is covered, whichever path re-enters it. + +The range's end is recorded only when the construct was already fully parsed +when the entry was pushed. For a construct still being parsed no end exists +yet (a mid-parse end location would be misleadingly early), so the entry's +scope lifetime bounds the dominion instead -- exact mid-parse, because the +construct's later tokens do not exist yet, and instantiation of a template +that has no definition yet is deferred past the scope's death. + +Suppression written on a template pattern or its lexical parents is +re-established around instantiation, so it applies to instantiated code. The +reverse is not propagated: a scope live at the *point of instantiation* +covers the trigger's tokens, not the pattern's, and the positional match +above keeps it from suppressing checks inside a synchronously instantiated +body, NSDMI, default argument, or marker re-check. + + +Modules and Serialization +========================= + +``[[profiles::enforce]]`` on a module interface declaration records the +enforced designators on ``Module::EnforcedProfileDesignators``, which is what +``[[profiles::require]]`` on an import validates against. A header unit +records enforcement the same way from the empty-declaration form P3589R2 +prescribes for headers. A non-partition implementation unit inherits the +interface's enforcements through its implicit import of the primary +interface. A partition implementation unit does not implicitly import the +interface, whose BMI is normally built later, so inheritance there is +best-effort: enforcements are inherited only when the interface's BMI is +already resident, and it is never force-loaded nor its absence diagnosed -- a +missed diagnostic, never a change to the meaning of a well-formed program. +``[[profiles::enforce]]`` on a *non-interface* module-declaration is recorded +only translation-unit-locally and is invisible to importers. + +Serialization is automatic for every profile: enforcements are written to a +PCH as ``ENFORCED_PROFILES`` records and restored on load, and +``Module::EnforcedProfileDesignators`` is written to a BMI as +``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. + Redeclaration Compatibility ---------------------------- - -P3589R2 [decl.attr.enforce]p5: a declaration and its redeclarations must -appear in the dominions of mutually compatible profiles. The rule is -**symmetric** -- when a redeclaration is merged with a previous declaration -from another module unit, every profile whose dominion covered the previous -declaration must have a compatible counterpart covering the redeclaration, -and vice versa. In particular, a profile-enforcing translation unit that -redeclares an entity from a module (or header unit) compiled *without* a -compatible profile is ill-formed; the paper's escape hatch for such headers -is ``[[profiles::exempt]]`` (not yet implemented, see the Intentional -Omissions section of :doc:`ProfilesFramework`). The check -(``SemaProfiles::checkRedeclarationProfileCompatibility``) runs from -``Sema::CheckRedeclarationInModule``, the funnel for function, variable, tag, -alias, and class-template redeclarations; it is a framework rule -- a plain -error, not suppressible with ``[[profiles::suppress]]``, and diagnose-only -(the redeclaration still merges). - -Two profiles are *compatible* if they have the same name -- designator -arguments configure a profile without changing its identity -- or if both are -standard (``std::``-prefixed) profiles, which P3589R2 proclaims mutually -compatible. No further implementation-proclaimed compatibility is modeled. +=========================== + +P3589R2 [decl.attr.enforce]p5 requires a declaration and its redeclarations +to appear in the dominions of mutually compatible profiles. +``checkRedeclarationProfileCompatibility`` runs from the module-level +redeclaration funnel and checks the rule symmetrically in both directions. +It is a framework rule: a plain error, not suppressible with +``[[profiles::suppress]]``, and diagnose-only (the redeclaration still +merges). Two profiles are compatible if they have the same name (designator +arguments configure a profile without changing its identity) or if both are +standard ``std::``-prefixed profiles. The previous declaration's dominion is approximated by its top-level module's -exported ``EnforcedProfileDesignators``, which is exact for declarations in -the module purview -- including purview ``extern "C"``/``extern "C++"`` -declarations (implicit global module), the common redeclarable case, since -module-attached entities cannot be redeclared in other translation units at -all. Two cases have an *unknown* dominion and are skipped rather than -guessed at (a missed diagnostic, never a wrong one): - -- A declaration in an **explicit global module fragment**: it precedes the - module-declaration, so the exported enforcements do not cover it, and its - TU's empty-declaration enforces are not serialized into the BMI. -- A previous declaration from the **same module family** (an implementation - or partition unit merging with its own interface): the exported set - under-approximates the interface TU's full dominion, and the interface's - enforcements are inherited into the current unit anyway, so checking would - false-positive on locally added profiles. - -A textual or PCH previous declaration is not checked at all: it shares the -current TU's dominion (the placement rule makes a TU's dominion uniform, and -a PCH's enforcements are restored into the including TU). Implicit template -instantiations are exempt, matching the module-ownership check. - -Serialization -------------- - -The framework serializes enforcement state automatically. Profile implementers -do not need to add any serialization code. - -- **PCH**: ``SemaProfiles::EnforcedProfiles`` is written as ``ENFORCED_PROFILES`` - records in the AST bitstream and restored when the PCH is loaded. -- **Module BMI**: ``Module::EnforcedProfileDesignators`` is written as - ``SUBMODULE_ENFORCED_PROFILES`` records within each submodule block. +exported designators, which is exact for declarations in the module purview. +Two cases have an *unknown* dominion and are skipped rather than guessed at +(a missed diagnostic, never a wrong one): a declaration in an explicit global +module fragment (the exported set does not cover it), and a previous +declaration from the same module family (the exported set under-approximates +the interface TU's dominion, which the current unit inherits anyway). A +textual or PCH previous declaration shares the current TU's dominion and is +not checked; implicit template instantiations are exempt. + Test Profiles ============= -The built-in ``test::`` profiles exist only to -exercise the framework and are additionally gated on the ``-fprofiles-test-profiles`` -flag, which sets ``LangOpts.ProfilesTestProfiles``. This flag is ``-cc1``-only -(not exposed by the driver) and is intended solely for running the test suite. -Under ``-fprofiles`` alone, ``[[profiles::enforce(test::...)]]`` is still -recognized (it is not ``warn_attribute_ignored``) and its designator is still -recorded and exported across modules, but ``SemaProfiles::isProfileEnforced`` reports -any ``test::``-prefixed profile as not enforced, so no ``test::`` rule ever -fires. Real profiles such as ``std::init`` are unaffected by this flag. - -By convention: - -- Real test profiles live under the ``test::`` namespace. Today there are - four: ``test::type_cast``, ``test::uninit_read``, ``test::class_final``, - and ``test::ctor_final``. Because the ``test::`` prefix is what - ``SemaProfiles::isProfileEnforced`` keys on to gate them behind - ``-fprofiles-test-profiles``, any new test-only profile must also live - under ``test::``. -- The names ``test::other``, ``test::bounds``, ``test::new_profile``, and - ``test::not_enforced`` are deliberately *not* implemented and appear only - in negative tests as stand-in "some other profile" names. Adding a real - profile under any of these names would invalidate those tests. - -The ``test::type_cast`` Profile -------------------------------- - -A pattern-1 (Sema check-site) profile. Demonstrates the simple case where -a rule can be checked from a single Sema entry point. - -- **Rules**: ``reinterpret_cast``. -- **Diagnostic**: ``err_profile_type_cast_reinterpret`` - ("'reinterpret_cast' is unsafe under profile '%0'"). -- **Check site**: ``Sema::BuildCXXNamedCast`` in ``clang/lib/Sema/SemaCast.cpp``, - inside the ``reinterpret_cast`` arm. Only the ``reinterpret_cast<>`` keyword - form is checked; a C-style or functional cast with reinterpret semantics goes - through a different path and is not diagnosed. - -The entire profile implementation is the single call: - -.. code-block:: c++ - - checkProfileViolation("test::type_cast", "reinterpret_cast", OpLoc, - diag::err_profile_type_cast_reinterpret); - - -The ``test::uninit_read`` Profile ---------------------------------- - -A pattern-2 (post-parse / CFG-based) profile. Demonstrates the case where -the rule depends on whole-function analysis and must run after parsing. -It diagnoses reads of uninitialized variables by reusing Clang's existing -CFG-based uninitialized-variables analysis. - -- **Rules**: none (the profile has a single implicit rule, so the rule - string is empty). -- **Diagnostic**: ``err_profile_uninit_read`` - ("variable %1 is read before initialization under profile '%0'"). - A companion ``note_var_declared_here`` is emitted at the variable's - declaration. -- **Opt-in table**: ``CFGUninitProfiles`` in - ``clang/lib/Sema/AnalysisBasedWarnings.cpp``. The ``IssueWarnings`` pass - guard consults it via ``hasEnforcedCFGUninitProfile()`` so the analysis - runs even when ``-Wuninitialized`` is silenced, and - ``UninitValsDiagReporter::diagnoseUnitializedVar`` walks it *before* the - default warning path -- when an entry's - ``SemaProfiles::shouldEmitProfileViolation`` returns true the entry's diagnostic - fires and the default warning is skipped entirely. - -The Stmt-tree suppression walker is what makes ``[[profiles::suppress]]`` -work for this profile: by the time the CFG analysis runs, the parse-time -``ProfileSuppressStack`` has been unwound, so the helper consults the AST -directly via ``shouldEmitProfileViolation(name, rule, Stmt*, AnalysisDeclContext&)``. - - -The ``test::class_final`` Profile ---------------------------------- - -A pattern-3 (class-finalization) profile. Demonstrates the case where the -rule applies once per completed class definition and runs from the -class-finalization dispatch in ``Sema::CheckCompletedCXXClass``. - -- **Rules**: none (the profile has a single implicit rule, so the rule - string is empty). -- **Diagnostic**: ``err_profile_class_final_test`` ("test profile fired on - completion of class %1 under profile '%0'"). -- **Opt-in table**: ``ClassFinalizationProfiles`` in - ``clang/lib/Sema/SemaProfiles.cpp``. - -Because dependent classes are filtered out by the dispatcher, the -diagnostic fires on class template *instantiations* rather than on the -primary template. Lambda closures are also skipped. -``[[profiles::suppress(test::class_final)]]`` on the class or any -enclosing lexical ``Decl`` silences the diagnostic via the decl-aware -``shouldEmitProfileViolation`` overload, which walks the class and its -lexical parents for a matching suppression. - - -The ``test::ctor_final`` Profile --------------------------------- - -A pattern-4 (constructor-finalization) profile. Demonstrates the case where -the rule applies once per user-defined constructor, after its -member-initializer list is complete. - -- **Rules**: none (single implicit rule, empty rule string). -- **Diagnostic**: ``err_profile_ctor_final_test`` ("test profile fired on - finalization of a constructor for class %1 under profile '%0'"). -- **Opt-in table**: ``ConstructorFinalizationProfiles`` in - ``clang/lib/Sema/SemaProfiles.cpp``. - -The diagnostic fires once per user-defined constructor -- written or implicit -member-initializer list, inline or out-of-line -- and on constructor template -*instantiations* rather than the dependent pattern. Defaulted and implicit -constructors (no body) and delegating constructors are skipped. +The four built-in ``test::`` profiles exist only to exercise the framework in +the test suite. They are gated on the ``-cc1``-only +``-fprofiles-test-profiles`` flag: under ``-fprofiles`` alone their +designators are still parsed, recorded, and exported across modules, but +``isProfileEnforced`` reports any ``test::``-prefixed profile as not +enforced, so no ``test::`` rule ever fires. Because that gate keys on the +``test::`` prefix, a new test-only profile must also live under ``test::``. + +- ``test::type_cast`` -- pattern 1; diagnoses ``reinterpret_cast<>`` (the + keyword form only). +- ``test::uninit_read`` -- pattern 2; rides the existing CFG + uninitialized-variables analysis. +- ``test::class_final`` -- pattern 3; fires on completion of every non-lambda + class, on instantiations rather than dependent patterns. +- ``test::ctor_final`` -- pattern 4; fires once per user-defined, + non-delegating constructor. + +The names ``test::other``, ``test::bounds``, ``test::new_profile``, and +``test::not_enforced`` are deliberately *not* implemented and appear in +negative tests as "some other profile" stand-ins; adding a real profile under +any of them would invalidate those tests. + + +The std::init Implementation Map +================================ + +``std::init`` (documented in :doc:`ProfilesFramework`) uses all four +patterns. Its rules map to mechanisms as follows: + +.. list-table:: + :header-rows: 1 + :widths: 24 12 64 + + * - Rule + - Pattern + - Primary entry points + * - ``uninit_read`` + - 2 and 1 + - ``CFGUninitProfiles`` row for local variables; + ``checkInitProfileCtorBody`` and ``checkInitProfileLocalMembers`` + (definite-assignment dataflow over ``[[uninit]]`` members); + ``checkInitProfileReadThrough`` at the lvalue-to-rvalue chokepoint, + plus compound-assignment and increment/decrement hooks + * - ``uninit_decl`` + - 1 + - ``checkInitProfileUninitDecl`` + * - ``uninit_with_initializer`` + - 1 + - ``checkInitProfileUninitWithInitializer`` + * - ``static_runtime_init`` + - 1 + - ``checkInitProfileStaticRuntimeInit`` + * - ``static_marker`` + - 1 + - ``checkInitProfileStaticMarker`` + * - ``union_marker``, ``pointer_marker`` + - attribute handler (enforcement-gated) + - ``checkInitProfileMarkerPlacement`` + * - ``ctor_uninit_member`` + - 4 + - ``ConstructorFinalizationProfiles`` row + * - ``ref_to_uninit`` + - 1 + - ``checkInitProfileRefToUninit`` behind per-site wrappers (variable + and member initialization, call arguments, returns, throws, + new-initializers, captures, object arguments) + * - ``uninit_write`` + - 1 + - ``checkInitProfileSubobjectWrite`` + +Two helpers are shared across the rules. ``refersToUninitializedMemory`` +classifies an expression as referring to initialized, uninitialized, or +unknown storage purely from its syntactic form; its ``UninitAccessOpts`` +presets distinguish a *binding* source (markers count everywhere), a value +*read*, and a scalar *store* (which differ in whether the top-level +``[[uninit]]`` marker counts and whether ``[[ref_to_uninit]]`` storage is +trusted). ``defaultInitLeavesScalarIndeterminate`` answers whether a type's +default-initialization leaves an unacknowledged scalar subobject +indeterminate, trusting user-provided default constructors -- the paper's +trust-the-constructor principle (P4222R1.1 §5.1), which is also why members +of objects initialized by a user-provided constructor are deliberately not +flow-tracked. From 7c75cd4896392c92e43da10291a0b5a32a6517da Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 17:02:15 -0400 Subject: [PATCH 244/289] Rewrite the profiles framework user doc --- clang/docs/ProfilesFramework.rst | 353 ++++++++++++++++--------------- 1 file changed, 188 insertions(+), 165 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 7b7264cd0b1cc..7e9af1bd750e1 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -1,9 +1,9 @@ -=========================== +====================== C++ Profiles Framework -=========================== +====================== .. contents:: - :depth: 3 + :depth: 2 :local: @@ -11,183 +11,180 @@ Introduction ============ The C++ Profiles framework (`P3589R2 -`_) allows a -translation unit to opt into additional language restrictions called *profiles*. -A profile is a named set of rules enforced by the compiler. A translation unit -requests enforcement with ``[[profiles::enforce(...)]]``; individual -declarations or statements can suppress enforcement with -``[[profiles::suppress(...)]]``; and module imports can require that an imported -module enforces a profile with ``[[profiles::require(...)]]``. +`_) lets a +translation unit opt into additional language restrictions called *profiles*. +A profile is a named set of rules enforced by the compiler, each formulated +to keep the program free of a certain class of problems -- for example, use +of uninitialized memory. Three attributes control it: + +- ``[[profiles::enforce(...)]]`` requests enforcement of one or more profiles + for the translation unit. +- ``[[profiles::suppress(...)]]`` locally exempts a declaration or statement + from an enforced profile, or from a single rule of it. +- ``[[profiles::require(...)]]`` on a module import verifies that the + imported module advertises a profile. Profiles do not change the meaning of well-formed programs with no undefined -behavior. Their static semantic effects are conceptually applied only after -translation phase 7: a profile cannot change the outcome of overload resolution -or template instantiation, and it is not possible to SFINAE on a profile +behavior. Their effects are conceptually applied only after translation +phase 7: a profile cannot change the outcome of overload resolution or +template instantiation, and it is not possible to SFINAE on a profile violation. -The framework is profile-agnostic. It handles attribute parsing, enforcement -tracking, suppression scoping, module integration, and serialization. -Individual profiles only need to call a single API at the appropriate semantic -check sites (``SemaProfiles::checkProfileViolation`` for parse-time checks, or -``SemaProfiles::shouldEmitProfileViolation`` from a per-pass dispatch table for -post-parse analyses). Everything else -- suppression, template instantiation, -SFINAE exclusion, module propagation, and PCH/BMI serialization -- is handled -by the framework automatically. +Profile names are open-ended: standard (``std::``-prefixed), +implementation-defined, and third-party profiles are all requested with the +same syntax, and enforcing a profile the implementation does not know is not +an error -- it simply has no rules to enforce. Clang currently implements +one real profile, an initial slice of the proposed ``std::init`` +initialization profile (see `The std::init Profile (initial slice)`_). The +feature is experimental: attribute spellings, rule names, and diagnostics may +change. -Driver Flag ------------ +Usage +===== -The entire framework is gated on the ``-fprofiles`` command-line flag, which -sets ``LangOpts.Profiles``. The flag is C++-only (declared with -``ShouldParseIf`` in ``clang/include/clang/Options/Options.td``) -and defaults to off. +The framework is gated on the C++-only ``-fprofiles`` flag, which defaults to +off: -.. code-block:: bash +.. code-block:: console clang++ -std=c++23 -fprofiles example.cpp -Without ``-fprofiles``: +The attributes accept both the ``[[profiles::name(...)]]`` and the +``[[using profiles: name(...)]]`` spelling and always require an argument +clause; see the :doc:`AttributeReference` for the per-attribute reference. -- ``[[profiles::enforce]]``, ``[[profiles::suppress]]``, and - ``[[profiles::require]]`` are diagnosed as ``warn_attribute_ignored`` and - have no semantic effect. -- Their argument clauses are **not** checked against the P3589R2 profile - grammar: like any standard attribute the implementation does not act on, - an arbitrary balanced-token argument clause -- or none at all -- is - accepted, so code annotated for a profiles-enabled build compiles cleanly - (modulo the warning) with the feature off. P3589R2's grammar is enforced - only under ``-fprofiles``. -- No profile rule check ever fires, even at sites that call - ``checkProfileViolation``. +Without ``-fprofiles`` the attributes are ignored with a warning, and their +argument clauses are not checked against the P3589R2 grammar -- like any +standard attribute the implementation does not act on, an arbitrary +balanced-token argument clause is accepted. Code annotated for a +profiles-enabled build therefore still compiles (modulo the warning) with the +feature off, and no profile rule ever fires. -The framework's parse-time bookkeeping (``ProfileSuppressScope``, attribute -custom parsing, etc.) is also no-ops when ``LangOpts.Profiles`` is false, so -the flag is the single switch that turns the entire feature on or off. +Enforcing Profiles +================== -Attribute Reference -=================== +``[[profiles::enforce(profile-designator-list)]]`` requests enforcement of +the named profiles for the whole translation unit. It may appear only on an +*empty-declaration* that precedes every other declaration at translation-unit +scope, or on a *module-declaration* (see `Profiles and Modules`_): + +.. code-block:: c++ -The three attributes are spelled in the ``profiles`` scope and accept either -the ``[[profiles::name(...)]]`` or ``[[using profiles: name(...)]]`` syntax. -Each attribute requires an argument clause; ``[[profiles::enforce]]`` and -``[[profiles::require]]`` with no parentheses are diagnosed. - -``[[profiles::enforce(profile-designator-list)]]`` - Allowed only on an *empty-declaration* at translation-unit scope or on a - *module-declaration*. At TU scope, it must precede every non-empty - declaration in the translation unit. Each profile-designator is a - ``::``-separated identifier sequence optionally followed by an - argument-clause (e.g. ``vendor(fortify: 3)``). Repeating the same name - with the same canonical designator is allowed; repeating it with a - different canonical designator is an error. - -``[[profiles::suppress(profile-name [, justification: "..."] [, rule: "..."])]]`` - Allowed on declarations and statements. Suppresses violations of the named - profile (optionally narrowed to a single rule) within the appertaining - declaration or statement; see :ref:`profiles-token-dominion` below. The - ``justification:`` argument, if present, must be a string literal; the - ``rule:`` argument may be a string literal or a bare token. Both - arguments are recorded but otherwise opaque to the framework. - -``[[profiles::require(profile-designator)]]`` - Allowed only on a *module-import-declaration*. Diagnoses if the imported - module's exported enforced-profile set does not contain a designator - matching the requested one. Importing a module does **not** retroactively - enforce its profiles in the importer. - -See the auto-generated :doc:`AttributeReference` for the AttrDocs entries -linked from these attributes. + [[profiles::enforce(std::init)]]; + [[profiles::enforce(vendor::hardened(fortify: 3))]]; // designator arguments + #include -Extending the Framework + int main() { /* ... */ } + +A *profile-designator* is a ``::``-qualified profile name, optionally +followed by a parenthesized argument list. The arguments are not subject to +name lookup; their interpretation is up to the profile. Repeating an +enforcement with the same designator is allowed and has no effect, but +requesting the same profile with a different designator is an error, as is an +enforcement placed after another declaration: + +.. code-block:: c++ + + [[profiles::enforce(vendor::hardened(fortify: 3))]]; + [[profiles::enforce(vendor::hardened(fortify: 3))]]; // OK: no effect + [[profiles::enforce(vendor::hardened(fortify: 2))]]; // error: same profile, + // different designator + int x; + [[profiles::enforce(std::init)]]; // error: does not precede 'x' + + +Suppressing Enforcement ======================= -The framework is profile-agnostic: profile names are opaque strings, there is -no central registry, and adding a new profile requires no changes to the -framework itself. See :doc:`ProfilesFrameworkInternals` for the -implementation patterns and the API for adding a new profile. +``[[profiles::suppress(profile-name)]]`` on a declaration or statement +exempts it from the named profile's rules. An optional ``rule:`` argument +narrows the suppression to a single named rule, and an optional +``justification:`` argument (a string literal) records why the suppression is +there: + +.. code-block:: c++ + + [[profiles::enforce(std::init)]]; + + void fill(char *buf, int n); + + int main() { + [[profiles::suppress(std::init, + rule: "uninit_decl", + justification: "buffer is filled in by fill()")]] + char buffer[1024]; + fill(buffer, 1024); + } + +A suppression covers exactly the tokens of the declaration or statement it +appertains to -- nothing more. For a variable declaration that includes the +initializer, so violations inside the initializer are silenced; but the +variable is *not* marked as exempt at later uses, which appear in other +declarations or statements and are checked normally: + +.. code-block:: c++ + + [[profiles::suppress(std::init)]] int x; // OK: uninit_decl suppressed + int y = x; // error: 'x' is read before initialization + +To exempt an object from a profile's checks everywhere it is used, use the +profile's own per-object marker instead (for ``std::init``, ``[[uninit]]``). + + +Profiles and Modules +==================== + +A module interface advertises the profiles it enforces through +``[[profiles::enforce]]`` on its module-declaration, and importers can insist +on that advertisement with ``[[profiles::require]]``, which may appear only +on a module-import-declaration: +.. code-block:: c++ + + // M.cppm + export module M [[profiles::enforce(std::init)]]; + + // user.cpp + import M [[profiles::require(std::init)]]; // OK: M enforces std::init + import N [[profiles::require(std::init)]]; // error unless N does too + +``[[profiles::require]]`` only verifies the advertisement; importing an +enforcing module does **not** enforce its profiles in the importer. +Enforcement is always explicit and local. A header unit participates the +same way: an ``[[profiles::enforce(...)]];`` empty-declaration in the header +is exported by the corresponding header unit and validated by +``[[profiles::require]]`` on its import. + +Enforcement on a module interface extends to the module's implementation +units: + +- A non-partition implementation unit (``module M;``) inherits the + interface's enforcements automatically. +- A partition implementation unit (``module M:P;``) inherits them only on a + best-effort basis, because the interface's BMI is usually not built yet + when the partition is compiled. Repeat the ``[[profiles::enforce]]`` there + for guaranteed enforcement. + +A declaration and its redeclarations must appear under mutually *compatible* +profiles (P3589R2 [decl.attr.enforce]p5): redeclaring an entity from a module +or header unit that was compiled without a compatible profile is diagnosed. +Two profiles are compatible when they have the same name (designator +arguments configure a profile without changing its identity), and all +standard ``std::`` profiles are mutually compatible. -.. _profiles-token-dominion: - -Suppression Dominion is Token-Based -=================================== - -A ``[[profiles::suppress(P)]]`` attribute suppresses profile ``P`` in the -token range of the declaration or statement it appertains to -- nothing more. -For a variable declaration that range covers the initializer expression (so -``[[profiles::suppress(P)]] T x = init();`` silences violations inside -``init()``), but it does *not* tag the variable as permitted-uninitialized for -subsequent uses; those uses appear in different declarations or statements -and are checked normally at their own source location. Profiles that need -per-object "opt-out of this check everywhere this value is used" semantics -(for example, the proposed ``[[uninit]]`` attribute of the -initialization profile) must introduce their own, separate, decl-scoped -marker. - -This applies identically to the parse-time suppression stack and the -post-parse Stmt-tree walker described in pattern 2. - -The parse-time stack enforces the dominion positionally: each entry records -the token range of the construct its attribute appertains to, and a -violation matches an entry only if its location falls within that range (in -translation-unit token order). This is what keeps a live suppress scope -from leaking into code whose tokens it does not cover. A check can fire -under an *unrelated* construct's scope in two ways: a template pattern -instantiated synchronously while the scope is live (instantiated code -retains the pattern's source locations, which lie outside the suppressed -construct wherever the pattern is declared -- before it, or first declared -after it), and a class or constructor finalized as a side effect of such an -instantiation (patterns 3 and 4). In both cases the violation's location is -outside the entry's range, so the suppression correctly does not apply; -conversely, a local class or lambda *defined inside* the suppressed -construct is covered, whichever path re-enters it. - -The range's end is recorded only when the construct was already fully -parsed when the entry was pushed -- a completed pattern or lexical parent at -an instantiation site, or a transformed ``AttributedStmt``. For a construct -still being parsed no end is recorded (its end location would be -misleadingly early: a mid-parse class collapses to its name token, a -body-pending function to its declarator) and the entry's -``ProfileSuppressScope`` lifetime bounds the dominion instead. That -fallback is exact mid-parse: the construct's later tokens do not exist yet, -and instantiation of a template that has no definition yet is deferred past -the scope's death. - - -Intentional Omissions -===================== - -The following parts of P3589R2 are deliberately not implemented: - -- ``[[profiles::exempt(...)]]`` (P3589R2 section 1.1.6), which would exempt - named included source files from profile enforcement. Implementing it - requires bookkeeping that connects the original spelling of an ``#include`` - to the source locations of constructs in the included file, and the feature - is not needed to exercise or validate the rest of the framework. - - -Built-in Profiles -================= - -The tree ships the ``std::init`` profile (below) plus four ``test::`` -profiles, all gated on ``-fprofiles``. The ``test::`` profiles exist only to -exercise the framework: they are additionally gated on the ``-cc1``-only -``-fprofiles-test-profiles`` flag, are inert under ``-fprofiles`` alone, and -are described in :doc:`ProfilesFrameworkInternals`. + +The ``std::init`` Profile (initial slice) +========================================= ``std::init`` is an initial slice of the proposed initialization profile from Bjarne Stroustrup's "An initialization profile" (P4222R1.1), on top of P3589R2 and P3402R3. Paper section references (``§``) for ``std::init`` in this document are to P4222R1.1. - -The ``std::init`` Profile (initial slice) ------------------------------------------ - -A slice of the proposed initialization profile. A read of a scalar +A read of a scalar ``[[uninit]]`` data member before it is assigned *is* diagnosed by R1 via CFG-based definite-assignment passes (paper §7.1 "initialized ... before use"): over the constructor body for the current object's members, and over @@ -220,7 +217,7 @@ because R7 fires only at binding sites. The slice introduces two marker attributes and the rules below. Marker attributes -~~~~~~~~~~~~~~~~~ +----------------- ``[[uninit]]`` (a standard C++11 attribute, distinct from the Clang vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` or @@ -270,10 +267,10 @@ profile rule carries weight only when ``std::init`` is enforced. - Behaviour: drives the ``ref_to_uninit`` rule (below); has no other effect. Rules -~~~~~ +----- R1. ``uninit_read`` -- pattern 2 (CFG) -...................................... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Reads of an uninitialized variable. Implemented as a second row in the existing ``CFGUninitProfiles`` table beside ``test::uninit_read``: @@ -406,7 +403,7 @@ Details: exemption applies (a ``std::byte`` member is never tracked). R2. ``uninit_decl`` -- pattern 1 -................................. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ An automatic-storage variable definition whose default-initialization leaves it (or a scalar subobject) indeterminate must either carry @@ -434,7 +431,7 @@ variable is instead rejected by ``static_marker`` (R9)). mixed type still fires for its unmarked scalars. R3. ``static_runtime_init`` -- pattern 1 -......................................... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A non-local variable whose initializer is not a constant expression must be ``constinit`` (which is already a hard error from the existing @@ -448,7 +445,7 @@ be ``constinit`` (which is already a hard error from the existing (so the profile error takes precedence when both would fire). R4. ``uninit_with_initializer`` -- pattern 1 -............................................ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``[[uninit]]`` and an initializer on the same declaration is a contradiction (the marker means "no initialization here"). @@ -471,7 +468,7 @@ contradiction (the marker means "no initialization here"). the rule must not fire (e.g. ``A a [[uninit]];`` for the ``A`` above). R5. ``ctor_uninit_member`` -- pattern 4 -....................................... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A user-provided constructor must initialize every non-static data member via its member-initializer list or an NSDMI, unless the member is marked @@ -511,7 +508,7 @@ a base with a user-provided default constructor is trusted. ``defaultInitLeavesScalarIndeterminate`` (R2). R6. ``union_marker`` -- attribute handler -......................................... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``[[uninit]]`` on a union object or a union member is banned (paper §5.6): delayed initialization by assigning a member would be an erroneous @@ -562,7 +559,7 @@ member initializer. A uninitialized union variable is therefore diagnosed by uninitialized union data member by ``ctor_uninit_member``. R7. ``ref_to_uninit`` -- pattern 1 -.................................. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A pointer or reference must be bound consistently with its ``[[ref_to_uninit]]`` marking (paper §4.3): a marked pointer/reference may only @@ -752,7 +749,7 @@ recognizers symmetric. pointer-to-member analog of the call-through-function-pointer gap. R8. ``pointer_marker`` -- attribute handler -........................................... +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``[[uninit]]`` on a pointer is banned (paper §4.3): "a reference cannot be uninitialized. The initialization profile requires the same for pointers." @@ -787,7 +784,7 @@ A pointer must instead be initialized (e.g. to ``nullptr``). out of scope. R9. ``static_marker`` -- pattern 1 -.................................. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A variable with static or thread storage duration is zero-initialized by language rule (paper §3), so it is an initialized object; marking it @@ -831,7 +828,7 @@ language rule (paper §3), so it is an initialized object; marking it fires in every case. R10. ``uninit_write`` -- pattern 1 -.................................. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ A scalar store to a *proper subobject* of a named ``[[uninit]]`` entity is banned delayed initialization (paper §1 "reading or writing uninitialized @@ -898,10 +895,36 @@ initialization sequence. writes needs construct_at modeling). Diagnostic suppression -~~~~~~~~~~~~~~~~~~~~~~ +---------------------- Every rule is suppressible per-site with ``[[profiles::suppress(std::init)]]`` (covers all rules) or ``[[profiles::suppress(std::init, rule: "rule_name")]]`` (rule-targeted). The token-based-dominion limitation noted earlier applies: a suppress attribute on a ``VarDecl`` covers only that declaration's tokens. + + +Test Profiles +============= + +Clang also ships four ``test::`` profiles (``test::type_cast``, +``test::uninit_read``, ``test::class_final``, and ``test::ctor_final``) that +exist only to exercise the framework in the test suite. They are inert +without an additional ``-cc1``-only flag; see +:doc:`ProfilesFrameworkInternals`. + + +Not Yet Implemented +=================== + +``[[profiles::exempt(...)]]`` (P3589R2 §1.1.6), which would exempt named +included source files from the enforcement of a profile, is not implemented. + + +Extending the Framework +======================= + +The framework is profile-agnostic: profile names are opaque strings, there is +no central registry, and adding a new profile requires no changes to the +framework itself. See :doc:`ProfilesFrameworkInternals` for the +implementation patterns and the API for adding a new profile. From 9a866c6e045827ceb4b2d92b489334900a02fefa Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 8 Jul 2026 17:07:42 -0400 Subject: [PATCH 245/289] Rewrite the std::init docs user-facing --- clang/docs/ProfilesFramework.rst | 967 +++++++++---------------------- 1 file changed, 281 insertions(+), 686 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 7e9af1bd750e1..f26fc5d46a8f5 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -35,7 +35,7 @@ implementation-defined, and third-party profiles are all requested with the same syntax, and enforcing a profile the implementation does not know is not an error -- it simply has no rules to enforce. Clang currently implements one real profile, an initial slice of the proposed ``std::init`` -initialization profile (see `The std::init Profile (initial slice)`_). The +initialization profile (see `The std::init Profile`_). The feature is experimental: attribute spellings, rule names, and diagnostics may change. @@ -176,671 +176,172 @@ arguments configure a profile without changing its identity), and all standard ``std::`` profiles are mutually compatible. -The ``std::init`` Profile (initial slice) -========================================= +The ``std::init`` Profile +========================= ``std::init`` is an initial slice of the proposed initialization profile from -Bjarne Stroustrup's "An initialization profile" (P4222R1.1), on top of P3589R2 -and P3402R3. Paper section references (``§``) for ``std::init`` in this -document are to P4222R1.1. - -A read of a scalar -``[[uninit]]`` data member before it is assigned *is* diagnosed by R1 via -CFG-based definite-assignment passes (paper §7.1 "initialized ... before -use"): over the constructor body for the current object's members, and over -any function body for the members of a *constructor-less aggregate local* -- -the member-store slice of the paper §5.3 "classes exposing uninitialized -memory" pattern. The rest of §5.3 (``construct_at``-based initialization), -random-access initialization of uninitialized arrays (paper §5.5), class-type -and array members (which need ``construct_at`` flow modeling), -``construct_at``/``destroy_at`` flow, and double-initialization / -double-destruction detection remain deferred. The -constructor is *not* required to initialize a ``[[uninit]]`` member (paper §5.1 -excepts members with an uninitialized indicator, and §5.3 leaves such a member -for the user): the obligation is keyed on a read before assignment, not on the -constructor's end. A scalar *read through* a ``[[ref_to_uninit]]`` -pointer/reference is diagnosed at the lvalue-to-rvalue conversion (R7, -``uninit_read``), and a member call on an object recognized as uninitialized -storage is diagnosed at its implicit object argument (R7, ``ref_to_uninit``); -*writes* through such a pointer/reference are not yet verified -(the paper relegates them to ``construct_at`` or suppression). A scalar store -to a *subobject* of a named ``[[uninit]]`` entity, by contrast, is banned as -delayed piecemeal initialization (R10, ``uninit_write``). - -Dynamically-created objects are covered when bound: a ``new`` expression that -default-initializes its allocated object and leaves a scalar subobject -indeterminate (e.g. ``new int``, ``new int[n]``, paper §1.2 / §4.3) is -recognised as a source of uninitialized memory by ``ref_to_uninit`` (R7). An -*unbound* ``new`` whose result is discarded (``new int;``) is still unchecked, -because R7 fires only at binding sites. - -The slice introduces two marker attributes and the rules below. - -Marker attributes ------------------ - -``[[uninit]]`` (a standard C++11 attribute, distinct from the Clang -vendor attribute ``[[clang::uninitialized]]``) marks a ``VarDecl`` or -``FieldDecl`` as intentionally left uninitialized. Recognised by Clang -regardless of ``-fprofiles``; its profile rules carry weight only when -``std::init`` is enforced. - -- TableGen def: ``Uninit`` in ``clang/include/clang/Basic/Attr.td``, - with a custom handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. -- Subjects: ``Var`` and ``Field``. The handler rejects placement where the - marker is meaningless -- a reference, a function parameter, or a structured - binding -- regardless of ``-fprofiles``. -- Behaviour: - - - Suppresses ``uninit_decl`` on the marked declaration (scalar or aggregate). - - Excuses a non-static data member from ``ctor_uninit_member``. - - Does **not** suppress ``uninit_read``. Per the paper, the marker excuses - the declaration but a read before any subsequent assignment is still - ill-formed. - - Triggers ``uninit_with_initializer`` when combined with an initializer, - including a language-synthesized one from a constructor that actually runs - (e.g. ``WithCtor x [[uninit]];``). A trivial/aggregate type whose - default-initialization is a no-op is *not* such an initializer, so the - marker is accepted there (the object is genuinely left uninitialized). - - Is banned on a pointer by ``pointer_marker`` (a pointer must be - initialized, paper §4.3), and on a union object or member by - ``union_marker`` (see R6 / R8 below for usage examples). Both are gated on - enforcement, and the marker is retained after the diagnostic so - ``uninit_decl`` does not re-diagnose the entity. - - Is banned on a variable with static or thread storage duration by - ``static_marker`` (such a variable is zero-initialized, so the marker - contradicts paper §4.2; see R9 below). - -``[[ref_to_uninit]]`` (also a standard C++11 attribute) marks a pointer, -reference, or pointer/reference-returning function as referring to -*uninitialized* memory. Recognised by Clang regardless of ``-fprofiles``; its -profile rule carries weight only when ``std::init`` is enforced. - -- TableGen def: ``RefToUninit`` in ``clang/include/clang/Basic/Attr.td``, with a - custom handler in ``clang/lib/Sema/SemaDeclAttr.cpp``. -- Subjects: ``Var``, ``Field``, and ``Function``. The handler rejects any - subject whose type (or, for a function, return type) is not a pointer or - reference to an object, via ``err_ref_to_uninit_attr_invalid_type`` -- - regardless of ``-fprofiles``. A function pointer or reference (and a - pointer-to-member) denotes a function or member, never uninitialized memory, - so it is rejected too. -- Behaviour: drives the ``ref_to_uninit`` rule (below); has no other effect. - -Rules ------ - -R1. ``uninit_read`` -- pattern 2 (CFG) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Reads of an uninitialized variable. Implemented as a second row in the -existing ``CFGUninitProfiles`` table beside ``test::uninit_read``: +Bjarne Stroustrup's "An initialization profile" (P4222R1.1; the ``§`` +references below are to that paper). Its guarantee: **no object is read or +written before it is initialized**, enforced entirely at compile time. +Following the paper: + +- Every object must be initialized at its point of definition, or be marked + as intentionally uninitialized with the ``[[uninit]]`` attribute. +- An object marked ``[[uninit]]`` must be assigned before it is read, which + is verified with simple, local flow analysis (§1.3). +- A pointer or reference to uninitialized memory must be marked with the + ``[[ref_to_uninit]]`` attribute, and reads through it are rejected. +- Non-local objects must be initialized at compile time (§3). +- Implicit initialization counts: a default constructor or the + zero-initialization of statics initializes an object, and a class with a + user-provided constructor is trusted to initialize its members (§5.1). + +``[[uninit]]`` and ``[[ref_to_uninit]]`` are ordinary C++11 attributes, +recognized regardless of ``-fprofiles`` (see the :doc:`AttributeReference` +entries for their placement rules); the rules below carry weight only while +``std::init`` is enforced. Reads and writes of ``std::byte`` objects are +exempt from all of them (§4.5). + +Each rule has a name, so it can be suppressed individually with +``[[profiles::suppress(std::init, rule: "name")]]`` (see `Suppressing +Enforcement`_): + +=========================== ====================================================== +Rule Diagnoses +=========================== ====================================================== +``uninit_decl`` A variable left (partially) uninitialized without + ``[[uninit]]``. +``uninit_read`` A read of an uninitialized object, or of an + ``[[uninit]]`` object before it is assigned. +``uninit_write`` A write to a subobject of an ``[[uninit]]`` object. +``ref_to_uninit`` A pointer or reference binding inconsistent with its + ``[[ref_to_uninit]]`` marking. +``ctor_uninit_member`` A constructor that leaves a member or base subobject + uninitialized. +``static_runtime_init`` A non-local variable with a runtime initializer. +``uninit_with_initializer`` ``[[uninit]]`` combined with an initializer. +``pointer_marker`` ``[[uninit]]`` on a pointer. +``union_marker`` ``[[uninit]]`` on a union object or member. +``static_marker`` ``[[uninit]]`` on a variable with static or thread + storage duration. +=========================== ====================================================== + + +Uninitialized Variables +----------------------- + +An automatic-storage variable whose default-initialization would leave it -- +or, for an aggregate, any scalar subobject (§5.4) -- indeterminate must +either be initialized or carry ``[[uninit]]`` (rule ``uninit_decl``): .. code-block:: c++ - constexpr CFGUninitProfileEntry CFGUninitProfiles[] = { - {"test::uninit_read", /*Rule=*/"", diag::err_profile_uninit_read, - /*ExemptStdByte=*/false}, - {"std::init", "uninit_read", diag::err_init_uninit_read, - /*ExemptStdByte=*/true}, - }; + struct S { int x; }; + union U { int i; float f; }; -If both ``test::uninit_read`` and ``std::init`` are enforced in the same TU, -the table-order priority makes ``test::uninit_read`` fire first. Use -``[[profiles::suppress(test::uninit_read)]]`` to demote it at a use site -and surface the ``std::init`` diagnostic. - -A read of an uninitialized ``std::byte`` is not diagnosed (paper §4.5 exempts -``std::byte``). The exemption is per-entry via ``CFGUninitProfileEntry`` so it -applies to ``std::init`` but not the generic ``test::uninit_read`` profile. - -The same ``std::init`` guarantee also covers a ``[[uninit]]`` scalar *data -member* read before it is assigned in a constructor body. -``checkInitProfileCtorBody`` in ``clang/lib/Sema/AnalysisBasedWarnings.cpp`` -runs a forward definite-assignment dataflow over the constructor-body CFG: a -member is assigned by a plain ``m = e`` (for a built-in type a write is its -initialization, paper §4.5) and is *definitely assigned* at a point only if -assigned on every path reaching it (the meet is intersection, paper §1.3 -"consider all branches ... executed"). A value read (an lvalue-to-rvalue load, -including the RHS of an assignment) of a member that is not yet definitely -assigned is reported via ``err_init_member_read_before_init`` at the first such -read, with a ``note_init_uninit_member_here`` note. A compound assignment -``m op= e`` and a built-in increment or decrement (``++m``, ``m++``, ``--m``, -``m--``) read the member's old value before writing it, so each is treated as a -read-then-write of that member. Details: - -- It runs from ``IssueWarnings`` for an enforced ``std::init`` constructor, - reusing the CFG built for the uninitialized-variables analysis, and also from - the post-error path (``runUninitProfileAnalysisAfterError``) so an earlier TU - error does not silently disable it. -- It reuses the ``uninit_read`` rule name (it enforces the same "no read of an - uninitialized object" guarantee), so - ``[[profiles::suppress(std::init, rule: "uninit_read")]]`` -- checked at the - read site via the ``Stmt``/``AnalysisDeclContext`` suppression overload -- - covers it, and the ``std::byte`` exemption applies. -- Target members are ``[[uninit]]`` built-in scalar (arithmetic or enum) - members; a member given a value by the *written* member-initializer list is - assigned at its own initializer, in execution (declaration) order, so a later - body read is fine (no spurious "marker + list-init" contradiction) while an - *earlier* member initializer -- or a base initializer -- that reads it is a - read-before-init (``X() : o(m) {}``). An NSDMI's subexpressions are not - expanded into the CFG, so a read of a tracked member inside another member's - default initializer stays undetected. -- A member access on the current object is recognized whether spelled - ``this->m``, implicitly as ``m``, or as the equivalent ``(*this).m``; an - access through any other object (``other.m``) is not the current object's - member. -- ``[[uninit]]`` members inherited from a non-virtual base with no - user-provided constructor are tracked like the class's own members (nothing - can have assigned them before the derived body runs); a written base - initializer (``: Base{1}``) counts as assigning that base subtree's tracked - members. A base *with* a user-provided constructor is trusted (paper §5.1) - -- its constructor body may have assigned the member, which this local - analysis cannot see -- so its members are not tracked. -- A ``this``-capturing lambda created in the body may run immediately, so a - member read in its body (or a nested lambda's) counts as a read at the point - the lambda is created; a body write earns no assignment credit (the lambda - may never run). A lambda stored now but called only after the member is - assigned is flagged all the same -- an accepted imprecision. An - init-capture's initializer runs at lambda creation and is checked as an - ordinary read. -- There is **no** constructor-exit requirement: a ``[[uninit]]`` member that is - simply never read is left as-is (paper §5.1/§5.3), exactly as R5 structurally - excuses a marked member -- the two checks are complementary. -- A *delegating* constructor is skipped: its target initializes the members - before the delegating body runs, so trusting the target (paper §5.1) avoids a - false positive, matching how R5 skips delegating constructors. -- Out of scope here (deferred, conservative omissions, not extensions): - class-type and array members, ``construct_at`` flow, and double-init/destroy - detection. Taking the address of a member or binding a reference to it is R7 - (``ref_to_uninit``) territory and is treated as neither a read nor an - initialization by this pass. - -The same guarantee covers an ``[[uninit]]`` scalar member of a -*constructor-less aggregate local* (the paper §5.3 "class exposing -uninitialized members" pattern used with a local: -``struct Agg { int m [[uninit]]; };`` and ``Agg a; a.m = 5;``). -``checkInitProfileLocalMembers`` in -``clang/lib/Sema/AnalysisBasedWarnings.cpp`` -- the local-variable analog of -the ctor-body pass, sharing its tracked-member filter, event/replay shape, -suppression lookup, and post-error rerun -- runs the same forward -definite-assignment dataflow over *every* function definition's CFG. This is -the flow tracking that lets the parse-time read-through preset (R7) drop the -top-level member marker without losing the read-before-write diagnosis. -Details: - -- Tracked pairs are (local variable, member): an automatic-storage, - non-parameter, non-reference local whose class -- with any base subtree - contributing tracked members -- has **no user-provided constructor** (one - is trusted per paper §5.1: its body may have assigned the member, which - local analysis cannot see) and whose declaration is the bare ``Agg a;`` - form. Any written initialization (``Agg a{}``, ``= {}``, ``= Agg()``, a - copy) gives every member a value and leaves nothing to track, as does a - local that is itself ``[[uninit]]``-marked -- its subobject accesses are the - parse-time read-through / ``uninit_write`` rules' territory. A member of - an anonymous struct or union is not tracked (its access is an - ``IndirectFieldDecl`` chain, not a direct ``a.m``), consistent with the - anonymous-aggregate skips in the ctor-body pass and R5; arrays of - aggregates are likewise out of scope (element tracking is the deferred - ``construct_at`` slice). -- A plain member store ``a.m = e`` assigns the member (§4.5); a compound - assignment and a built-in ``++``/``--`` read the old value first and are a - read-then-write, exactly as in the ctor-body pass. -- Soundness over completeness: **any** other appearance of the variable -- - ``&a``, ``&a.m``, a reference binding, passing ``a`` to any function - (``construct_at``, ``memcpy``), a member call, a lambda capture -- - conservatively marks every tracked member assigned from that point (the - address may be used to initialize the object), so no legal program is - rejected. A backward ``goto`` across the declaration re-default-initializes - the object, which the gen-only dataflow cannot model -- a possible missed - diagnostic, matching the ctor-body pass's accepted imprecision level. -- Objects reached through parameters, references, or other objects are not - tracked; with the user-provided-constructor trust above this makes the - remaining "``uu.y`` read through another object" case a deliberate, - documented trust decision (see the read-through preset under R7). -- Reports reuse ``err_init_member_read_before_init`` under the shared - ``uninit_read`` rule, so ``[[profiles::suppress(std::init, rule: - "uninit_read")]]`` at the read site covers it and the ``std::byte`` - exemption applies (a ``std::byte`` member is never tracked). - -R2. ``uninit_decl`` -- pattern 1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An automatic-storage variable definition whose default-initialization -leaves it (or a scalar subobject) indeterminate must either carry -``[[uninit]]`` or be initialized. This covers a scalar / pointer / -enum with no initializer, and -- per paper §5.4 ("classes without -constructors") -- an aggregate or trivially-default-constructible class type -whose default-initialization leaves a scalar subobject indeterminate (e.g. -``struct S { int x; }; S s;``). A class type with a user-provided default -constructor is trusted; static / thread storage duration is excluded -(zero-initialized by language rule -- a ``[[uninit]]`` marker on such a -variable is instead rejected by ``static_marker`` (R9)). - -- Diagnostic: ``err_init_uninit_decl``. -- Check site: ``Sema::ActOnUninitializedDecl`` in - ``clang/lib/Sema/SemaDecl.cpp``, which is only reached for declarations - with no initializer (so braced or value initialization such as - ``S s = {1};`` and ``S s{};`` is unaffected -- omitted aggregate members - are value-initialized). -- The aggregate case uses ``SemaProfiles::defaultInitLeavesScalarIndeterminate`` - with ``HonorUninitMarkers=true``, which recurses through bases and members, - trusts user-provided default constructors, and skips data members marked - ``[[uninit]]`` (acknowledged uninitialized, paper §5.3). So a type - whose only indeterminate scalars are all marked is trusted - (e.g. ``struct A { int x [[uninit]]; }; A a;`` is accepted), while a - mixed type still fires for its unmarked scalars. - -R3. ``static_runtime_init`` -- pattern 1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A non-local variable whose initializer is not a constant expression must -be ``constinit`` (which is already a hard error from the existing -``ConstInitAttr`` arm). Without ``constinit``, the existing -``-Wglobal-constructors`` warning would fire (off by default); under -``std::init`` this rule promotes that to a profile error. - -- Diagnostic: ``err_init_static_runtime_init``. -- Check site: in ``Sema::CheckCompleteVariableDeclaration``, in the - constinit cascade, immediately before the ``-Wglobal-constructors`` arm - (so the profile error takes precedence when both would fire). - -R4. ``uninit_with_initializer`` -- pattern 1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``[[uninit]]`` and an initializer on the same declaration is a -contradiction (the marker means "no initialization here"). - -- Diagnostic: ``err_init_uninit_with_initializer``. -- Check site: ``SemaProfiles::checkInitProfileUninitWithInitializer``, shared by - ``Sema::CheckCompleteVariableDeclaration`` (variables) and - ``Sema::ActOnFinishCXXInClassMemberInitializer`` (data members with a - default member initializer). -- A ``RecoveryExpr`` placeholder (from a failed initialization) is not a - user-written initializer and does not trigger the rule. -- The "initializer" includes a language-synthesized one from a constructor - that actually runs (e.g. ``WithCtor x [[uninit]];``), but *not* a - no-op trivial/aggregate default-initialization, where the marker is - consistent with the object being left uninitialized. -- Unlike R2/R5, this "no-op?" test calls - ``defaultInitLeavesScalarIndeterminate`` with ``HonorUninitMarkers=false`` - (the *factual* answer): a type whose members are themselves marked still - default-initializes to a no-op, so the variable marker stays consistent and - the rule must not fire (e.g. ``A a [[uninit]];`` for the ``A`` above). - -R5. ``ctor_uninit_member`` -- pattern 4 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + void f() { + int a; // error: uninitialized (uninit_decl) + int b [[uninit]]; // OK: intentionally uninitialized + int c = 3; // OK + S s; // error: default-initialization leaves 's.x' indeterminate + S t{}; // OK: value-initialized + U u; // error: an uninitialized union (§5.6) + std::string str; // OK: a user-provided default constructor is trusted + } -A user-provided constructor must initialize every non-static data member -via its member-initializer list or an NSDMI, unless the member is marked -``[[uninit]]`` (paper §5.1). A plain assignment in the constructor -body does not count. A member whose own default-initialization leaves an -*unacknowledged* scalar subobject indeterminate (a nested aggregate) is -flagged as well; a member whose type's indeterminate scalars are all -themselves marked ``[[uninit]]`` is trusted (the same -``HonorUninitMarkers`` walk as R2, paper §5.3). A direct non-virtual -base-class subobject left indeterminate is flagged the same way: the -guarantee is over the *complete object* (paper §5.1, §7.1), and -- unlike a -member -- a base cannot carry an ``[[uninit]]`` marker, so it must always be -initialized. A written base-initializer (``: Base(...)`` / ``: Base{}``) or -a base with a user-provided default constructor is trusted. - -- Diagnostic: ``err_init_ctor_uninit_member`` (with a - ``note_init_uninit_member_here`` note at the member); for a base subobject, - ``err_init_ctor_uninit_base`` (with a ``note_init_uninit_base_here`` note). - Both share the ``ctor_uninit_member`` rule name, so one - ``[[profiles::suppress(std::init, rule: "ctor_uninit_member")]]`` covers - members and bases alike. -- Opt-in table: ``ConstructorFinalizationProfiles`` (pattern 4). -- Reference and const members keep their existing dedicated diagnostics; - anonymous-aggregate members and unnamed bit-fields are skipped (named - bit-fields are checked like any other member). -- A union's own constructor is exempt from this rule -- its members are - mutually exclusive, so a constructor initializes at most one (paper §5.6; - see R6). A union *data member* of a non-union class is still checked, and - must be initialized via the member-initializer list. -- Known gaps: *virtual* base-class subobjects are not checked. A virtual - base is initialized by the most-derived constructor, which is not a local - property of the constructor being checked, so flagging an intermediate - constructor would push a redundant (and possibly surprising) ``: V()`` onto - code that correctly relies on the most-derived class; under-diagnosing here - is the safer, paper-consistent default. Direct non-virtual bases are - checked. A const member is skipped here but is treated as indeterminate by - ``defaultInitLeavesScalarIndeterminate`` (R2). - -R6. ``union_marker`` -- attribute handler -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``[[uninit]]`` on a union object or a union member is banned (paper -§5.6): delayed initialization by assigning a member would be an erroneous -assignment when compiled without the profile. +A class type with a user-provided default constructor is trusted to +initialize its members (§5.1), and a data member that is itself marked +``[[uninit]]`` is acknowledged -- a type whose only indeterminate scalars are +all marked does not trigger the rule. -.. code-block:: c++ - union U { int x; float y; }; +Where ``[[uninit]]`` May Not Go +------------------------------- - U a [[uninit]]; // error: [[uninit]] on a union variable (union_marker) - U b; // error: a union must be initialized (uninit_decl / err_init_uninit_union) - U c = {1}; // OK - U d{}; // OK +``[[uninit]]`` asserts that an object is genuinely uninitialized, so +placements that contradict that -- or that would defeat the profile's +guarantee -- are rejected: - union M { - int x [[uninit]]; // error: [[uninit]] on a union member (union_marker) - float y; - }; +.. code-block:: c++ - [[profiles::suppress(std::init)]] U e [[uninit]]; // OK - -- Diagnostic: ``err_init_union_marker``. -- Check site: the shared helper ``SemaProfiles::checkInitProfileMarkerPlacement``, - called from the ``Uninit`` handler in ``clang/lib/Sema/SemaDeclAttr.cpp`` and - re-run on the instantiated entity from ``VisitFieldDecl`` / ``VisitVarDecl`` - in ``clang/lib/Sema/SemaTemplateInstantiateDecl.cpp``. Unlike the reference / - parameter / structured-binding rejections, which are unconditional, this is - gated on enforcement -- a union may legitimately carry the marker without the - profile. Being Decl-aware it defers on a templated pattern and fires once on - the instantiation (a dependent member or local that substitutes to a union), - consistent with the other ``std::init`` rules. -- The rule keys on the *base element type*, so an array of unions - (``[[uninit]] U a[2];``) is banned exactly like a single union object. A - union-typed data member of a non-union class is banned as well -- delayed - initialization by assigning one of its members is just as erroneous there -- - in addition to the members *of* a union shown above. -- The banned marker is retained on the declaration after it is diagnosed, so - the ``uninit_decl`` / ``ctor_uninit_member`` rules treat the entity as - acknowledged and do not emit a second, contradictory diagnostic. A member - assignment to such a marker-retaining union (the §5.6 delayed-initialization - ban) is caught by ``uninit_write`` (R10). - -An *unmarked* union left uninitialized is itself the error (paper §5.6): -``SemaProfiles::defaultInitLeavesScalarIndeterminate`` reports a union as indeterminate -unless it has no members, a user-provided default constructor, or a default -member initializer. A uninitialized union variable is therefore diagnosed by -``uninit_decl`` (with the union-specific ``err_init_uninit_union``) and an -uninitialized union data member by ``ctor_uninit_member``. - -R7. ``ref_to_uninit`` -- pattern 1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + int g [[uninit]]; // error: statics are zero-initialized (static_marker) + int *p [[uninit]]; // error: a pointer must be initialized, e.g. to + // nullptr (pointer_marker, §4.3) -A pointer or reference must be bound consistently with its -``[[ref_to_uninit]]`` marking (paper §4.3): a marked pointer/reference may only -refer to uninitialized memory, and an unmarked one may only refer to -initialized memory. "Refers to uninitialized memory" is recognised purely -locally from the source expression's syntactic form (no flow analysis): the -address of, or a subobject of, a ``[[uninit]]`` entity; a value of a -``[[ref_to_uninit]]`` pointer/reference or array; a dereference of such a -pointer; a cast of such a pointer to another pointer type (paper §4.3), or of -such a glvalue to another reference; a call to a -``[[ref_to_uninit]]``-returning function; or a ``new`` expression that -default-initializes its allocated object and leaves a scalar subobject -indeterminate (e.g. ``new int``, ``new int[n]``, paper §1.2 / §4.3). -Pass-through forms are transparent to the operand they forward: a single-element -braced initializer (``{e}``) is looked through to its element, a conditional -(``c ? a : b``) is uninitialized if either arm is, and a comma (``(a, b)``) -takes its right operand -- so each is handled like the direct binding it -forwards. A source whose form is recognized as neither uninitialized nor -trusted-initialized is classified as *unknown* rather than assumed initialized, -so it is diagnosed for neither a marked target (avoiding a false rejection) nor -an unmarked one (leaving a possible missed diagnostic). The -reference cast and the ``[[ref_to_uninit]]``-returning reference call are not -spelled out by the paper but follow from the profile's guarantee that -uninitialized objects are not used, and keep the pointer and reference -recognizers symmetric. - -- Diagnostics: ``err_init_ref_to_uninit_requires_uninit`` (marked target, - initialized source) and ``err_init_uninit_requires_ref_to_uninit`` (unmarked - target, uninitialized source). -- Recognizer + shared check: ``SemaProfiles::refersToUninitializedMemory`` and - ``SemaProfiles::checkInitProfileRefToUninit`` in ``clang/lib/Sema/SemaProfiles.cpp``. -- A ``new`` expression is recognised only when it default-initializes its - object (none init style, no written initializer); ``new T(...)`` and - ``new T{...}`` are value- or list-initialized and excluded. Whether the - allocated type leaves a scalar indeterminate reuses - ``SemaProfiles::defaultInitLeavesScalarIndeterminate`` (R2), so a ``T`` with a - user-provided default constructor is trusted and the ``std::byte`` exemption - is inherited. ``getAllocatedType`` yields the element type, so array - ``new`` (``new int[n]``) is handled uniformly. -- Check sites: variable initialization - (``Sema::CheckCompleteVariableDeclaration``), default member initializers - (``Sema::ActOnFinishCXXInClassMemberInitializer``), constructor - member-initializers (``Sema::BuildMemberInitializer``), aggregate/list field - initialization (``InitListChecker``), pointer assignment - (``Sema::CreateBuiltinBinOp``), call arguments at parameter - copy-initialization (``Sema::PerformCopyInitialization``, the funnel for - every call form -- plain calls, constructor calls, overloaded operators, - and calls to objects of class type such as functors and lambdas; a call - with *no declared callee*, through a function pointer, is checked there - too, its parameters treated as unmarked targets since no declaration could - carry ``[[ref_to_uninit]]`` (paper §7.2) -- so passing uninitialized - memory through a function pointer diagnoses even when the pointed-to - function's own parameter is marked, the marker being a declaration - property invisible through the pointer; suppress at the call if the flow - is intended), arguments supplied by a parameter's default argument - (``Sema::GatherArgumentsForCall``, which reuses the pre-built expression - rather than re-running copy-initialization), variadic (``...``) arguments - (the promotion loops in ``GatherArgumentsForCall`` and - ``Sema::BuildCallToObjectOfClassType``, so variadic functors and variadic - lambdas are covered too -- a ``...`` parameter cannot carry the marker, so - a pointer argument is checked as an unmarked target, paper §7.2, while a - promoted *value* read stays the read-through chokepoint's), return - statements - (``Sema::BuildReturnStmt``), implicit object arguments - (``Sema::PerformImplicitObjectArgumentInitialization``, the funnel every - member-call flavor's object argument converts through -- dot and arrow - calls, member operators including whole-object ``operator=``, functor - ``operator()``, ``operator->``, and conversion operators; the implicit - object parameter can never carry ``[[ref_to_uninit]]``, so a call on an - object recognized as uninitialized storage is checked as an unmarked - target, paper §7.2, with suppress as the escape -- an *explicit* object - member function instead initializes its object as an ordinary parameter, - which the parameter site above already owns and whose parameter can carry - the marker; a destructor call is skipped, destruction being the deferred - destroy_at slice, and so is a static call operator, which -- like a static - member call -- evaluates the object argument without using its value), and - lambda captures -- an init-capture binds - like a variable initialization when its variable is created - (``Sema::createLambdaInitCaptureVarDecl``), and a plain by-reference capture - of an entity denoting uninitialized storage (an ``[[uninit]]`` variable, or - a ``[[ref_to_uninit]]`` reference) is checked when the closure is built - (``Sema::BuildLambdaExpr``). A capture cannot carry the marker, so only the - unmarked-direction violation can fire there; a *copy* capture is not a - binding -- it reads the variable in the enclosing function's CFG, which is - the flow-based ``uninit_read`` pass's territory. Inside a template the - sites split by timing. The Decl-carrying variable, data-member, and - constructor member-initializer sites defer on the pattern (the - ``D->isTemplated()`` check in ``shouldEmitProfileViolation``) and fire - once, at instantiation, on the instantiated ``Decl``; the constructor site - passes the enclosing constructor and is re-run by - ``BuildMemberInitializer`` at instantiation, exactly like - ``ctor_uninit_member``. The Decl-less call-argument, assignment, return, - aggregate-field, object-argument, and capture sites instead defer only when - the source (for - the capture, the captured variable's type; for pointer assignment, also - the LHS) is *instantiation-dependent* -- such constructs are always rebuilt - at instantiation, where the re-run ``Build*`` / ``InitListChecker`` routine - checks the substituted form. A non-dependent construct fires at - *definition time* (TreeTransform can reuse it unchanged, so deferring - would lose the diagnostic) and repeats if the construct is rebuilt at - instantiation anyway; see Pattern 1 above for the unified model, its - accepted phase-7 trade, and the accepted repetition. The two timings are - visible side by side: - ``int *p = &g_uninit;`` in a template fires at instantiation (Decl-carrying - variable site), while ``p = &g_uninit;`` fires at definition time - (Decl-less assignment site) -- a deliberate asymmetry. The aggregate-field - hooks - (``CheckSubElementType`` for a pointer field, ``CheckReferenceType`` for a - reference field) are scoped to a member subobject (``EK_Member`` with a - non-null parent), so the enclosing variable/argument/return is left to its own - site, and a top-level member braced-initializer -- which the constructor or - NSDMI site already checks -- is not diagnosed twice. -- Read-through enforcement (paper §4.5): a scalar *read* through a - ``[[ref_to_uninit]]`` pointer or reference loads an uninitialized value and is - diagnosed at the single lvalue-to-rvalue chokepoint - (``Sema::DefaultLvalueConversion`` calling ``SemaProfiles::checkInitProfileReadThrough``), - which by-value reads -- copy-initialization, by-value arguments, returns, and - operator/condition operands -- all funnel through. It reuses the recognizer - with its read access preset (``UninitAccessOpts``), reports the shared rule - ``uninit_read`` via ``err_init_uninit_read_through``, and exempts ``std::byte`` - (paper §4.5). ``UninitAccessOpts`` carries two axes -- the top-level - ``[[uninit]]`` drop and a ``[[ref_to_uninit]]`` trust flag -- whose presets - distinguish a *binding* source (markers count everywhere), a value *read* - (this rule), and a scalar *store* (``uninit_write``, R10, which shares the - drop and additionally trusts the ``[[ref_to_uninit]]`` arms). - The read preset drops the ``[[uninit]]`` marker only for the - *top-level* named entity, whose direct reads are owned three ways: a - directly named ``[[uninit]]`` object is flow-tracked by the CFG - ``uninit_read`` pass, a current-object ``[[uninit]]`` member by the - ctor-body pass, and an ``[[uninit]]`` member of a constructor-less - aggregate local by the local-aggregate pass (all three credit assignments; - see R1). A marked member of an object with a *user-provided* constructor - reached through any other object (``uu.y`` after ``Slot uu;``) is instead - **trusted**, deliberately: the constructor's body may have assigned the - member (the §5.2 pattern), which local analysis cannot see, so paper §5.1's - trust-the-constructor principle applies and its reads are not diagnosed - anywhere. A *subobject* read of a named ``[[uninit]]`` object - (``s.x``, ``o.agg.f``) or array (``a[0]``, ``*a``, ``s.a[i]``) is recognized - and diagnosed here: neither flow pass tracks members or array elements, and - subobject-wise delayed initialization of an ``[[uninit]]`` object is itself - banned (paper §5.4/§5.5; only whole-object ``construct_at`` re-initializes, - which is uniformly unmodeled), so no assignment could have given the - subobject a value. Being Decl-less, it defers only on an - instantiation-dependent glvalue (rebuilt and re-checked at instantiation) - and otherwise fires at definition time, repeating if the read is rebuilt - at instantiation anyway (accepted). An address-of (``&*p``), a reference - binding, a discarded-value expression (``(void)*p``), and a write (``*p = 5`` - or ``s.x = 1``) apply no lvalue-to-rvalue conversion and so are not reads - (``s.x = 1`` is instead banned as a subobject store by ``uninit_write``, - R10). A compound assignment (``*p += 1``) and a built-in ``++``/``--`` - also read the old value while building no lvalue-to-rvalue node; their - loads are checked at the operator sites instead - (``Sema::CheckAssignmentOperands`` for the non-shift compounds, the - increment/decrement arm of ``Sema::CreateBuiltinUnaryOp`` for - ``++``/``--``), while the shift compounds keep loading through the - chokepoint via their LHS promotion and are excluded from the operator hook, - so exactly one diagnostic fires. A *class-type* copy from ``*p`` (copy-, - direct-, braced-, argument-, or return-copy) never reaches the chokepoint -- - record glvalues undergo no lvalue-to-rvalue conversion -- but is caught all - the same by the *binding* rule at the copy constructor's reference - parameter, so the diagnostic is - ``err_init_uninit_requires_ref_to_uninit`` rather than - ``err_init_uninit_read_through``. The escape is the paper's own (§7.2): a - copy constructor declared with a ``[[ref_to_uninit]]`` reference parameter - accepts the uninitialized source. Because this is a binding, not a read, - the ``std::byte`` exemption does not apply to a record that merely - *contains* ``std::byte`` members. -- Known gaps: recognition is purely of the source's syntactic form, so a - binding whose underlying operand is unrecognized -- pointer arithmetic, an - integer-to-pointer cast, or the *result* of a call through a function - pointer (no ``FunctionDecl`` to read a return marker from) -- is classified - as *unknown* and diagnosed for neither direction. A ``[[ref_to_uninit]]`` target - therefore accepts it (rather than the earlier *false positive*), while an - unmarked target also accepts it (a remaining missed diagnostic). The - pass-through forms above forward to such an operand without laundering it, so - they inherit this gap rather than introducing one. Only plain ``=`` pointer - assignment is covered; - compound assignment is not a binding and is skipped. Aggregate field - initialization is checked per scalar field, so an array-of-pointer (or - array-of-reference) member is out of scope -- its elements are - ``EK_ArrayElement`` and the ``[[ref_to_uninit]]`` marking lives on the field, - not the element -- as is a pointer/reference member reached through an - ``IndirectFieldDecl`` (a member of an anonymous struct/union), consistent with - the scalar slice. A member call through a pointer-to-member - (``(s.*pmf)()``) resolves no ``CXXMethodDecl`` at the call and bypasses the - object-argument conversion, so its object goes unchecked -- the - pointer-to-member analog of the call-through-function-pointer gap. - -R8. ``pointer_marker`` -- attribute handler -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -``[[uninit]]`` on a pointer is banned (paper §4.3): "a reference cannot -be uninitialized. The initialization profile requires the same for pointers." -A pointer must instead be initialized (e.g. to ``nullptr``). + union U { int i; float f; }; + U u [[uninit]]; // error: delayed initialization of a union member + // would be erroneous (union_marker, §5.6) -.. code-block:: c++ + void f() { + int x [[uninit]] = 4; // error: marked *and* initialized + // (uninit_with_initializer, §4.2) + std::string s [[uninit]]; // error: the default constructor initializes + // it (uninit_with_initializer) + } - int *p; // error: must be initialized or marked [[uninit]] (uninit_decl) - int *q = nullptr; // OK: the prescribed fix - int *r [[uninit]]; // error: [[uninit]] cannot be applied to a pointer (pointer_marker) +Each of these keys on the array element type, so an array of pointers or of +unions is rejected exactly like a single one. A no-op initialization -- the +trivial default-initialization of a scalar or aggregate -- is consistent with +the marker and accepted. - struct S { - int *p [[uninit]]; // error: also fires on a pointer data member - }; - // Opt out if genuinely required: - [[profiles::suppress(std::init, rule: "pointer_marker")]] int *x [[uninit]]; // OK - -- Diagnostic: ``err_init_uninit_pointer_marker``. -- Check site: ``SemaProfiles::checkInitProfileMarkerPlacement``, alongside - ``union_marker`` (see R6 for the shared parse-time handler and the - re-check on the instantiated field / variable). Like that rule it is gated on - enforcement -- a pointer may legitimately carry the marker without the profile - -- and the marker is retained after the diagnostic so ``uninit_decl`` does not - also fire. -- The check keys on the base element type, so an array of pointers - (``[[uninit]] int *a[2];``) is banned exactly like a single pointer -- the - marker cannot smuggle uninitialized pointers past ``uninit_decl`` - element-wise. -- A pointer parameter is rejected earlier and unconditionally (the marker is - meaningless on a parameter); a pointer-to-member is not a pointer type and is - out of scope. - -R9. ``static_marker`` -- pattern 1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A variable with static or thread storage duration is zero-initialized by -language rule (paper §3), so it is an initialized object; marking it -``[[uninit]]`` contradicts paper §4.2 ("an initialized object marked -``[[uninit]]`` is an error"). A pointer must be initialized; a static is -*already* initialized. +Reads of Uninitialized Objects +------------------------------ + +An uninitialized object must not be read (rule ``uninit_read``). Local flow +analysis verifies reads of uninitialized locals, of ``[[uninit]]`` members +within the defining constructor's body, and of ``[[uninit]]`` members of +constructor-less aggregate locals; reads through ``[[ref_to_uninit]]`` +pointers and references and reads of subobjects of ``[[uninit]]`` objects are +rejected outright: .. code-block:: c++ - int glob; // OK: zero-initialized - int glob2 [[uninit]]; // error: zero-initialized (static_marker) - static int s [[uninit]]; // error: also on an explicit static - thread_local int t [[uninit]]; // error: thread storage is zero-initialized too + struct Buf { int n [[uninit]]; }; - void f() { - static int ls [[uninit]]; // error: also on a block-scope static + int f(int *p [[ref_to_uninit]]) { + int x [[uninit]]; + int a = x; // error: 'x' is read before it is assigned + x = 3; + int b = x; // OK: assigned on every path reaching the read + + Buf buf; + int c = buf.n; // error: 'buf.n' is not yet assigned + buf.n = 1; + int d = buf.n; // OK + + [[uninit]] int arr[4]; + int e = arr[0]; // error: array elements are not tracked; only a + // whole-object initialization can give 'arr' a value + + return *p; // error: read through [[ref_to_uninit]] (§4.5) } - // Opt out if genuinely required: - [[profiles::suppress(std::init, rule: "static_marker")]] int x [[uninit]]; // OK - -- Diagnostic: ``err_init_uninit_static_marker`` (a ``%select`` distinguishes - static from thread storage). -- Check site: ``Sema::ActOnUninitializedDecl`` in - ``clang/lib/Sema/SemaDecl.cpp``, beside the ``uninit_decl`` (R2) check -- - *not* the ``Uninit`` attribute handler that hosts ``union_marker`` / - ``pointer_marker``. The decl site is reached only for a definition with no - written initializer (so a non-defining ``extern`` declaration, handled - earlier, is excluded), and passing the ``VarDecl`` to - ``shouldEmitProfileViolation`` makes the rule fire on instantiations rather - than template patterns, like every other Decl-based rule. -- Unlike ``uninit_decl``, ``std::byte`` is *not* exempt here: a static - ``std::byte`` is zero-initialized (it cannot be left indeterminate the way an - automatic one can), so the marker is still contradictory. -- Partition with ``uninit_with_initializer`` (R4): a static ``[[uninit]]`` with - a real initializer -- an explicit one, or a constructor that actually runs - (e.g. ``static WithCtor w [[uninit]];``) -- is R4's; ``static_marker`` covers - only the zero-initialized, no-real-initializer case R4 treats as a consistent - no-op (it reuses ``defaultInitLeavesScalarIndeterminate`` with - ``HonorUninitMarkers=false``, R4's factual choice). Exactly one diagnostic - fires in every case. - -R10. ``uninit_write`` -- pattern 1 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -A scalar store to a *proper subobject* of a named ``[[uninit]]`` entity is -banned delayed initialization (paper §1 "reading or writing uninitialized -memory is an error"; §5.4 member-wise, §5.5 random-access element, §5.6 -union-member). Writing the whole named entity is that entity's -initialization (paper §4.5: for a built-in type, a write is its -initialization) and stays legal -- the flow passes (R1) credit it -- as does -the §5.2 constructor-body pattern, whose member stores reach the current -object through ``this``. Only whole-object ``construct_at`` could make a -piecemeal-initialized object good, and construct_at flow is uniformly -unmodeled, so no store below a marked entity can be part of a valid -initialization sequence. + struct T { + int m [[uninit]]; + T(int v) { + int r = m; // error: 'm' is read before the body assigns it + m = v; // OK: for a built-in type, a write is its + } // initialization (§4.5) + }; + +A member or variable counts as assigned only when every path to the read +assigns it (§1.3), and a compound assignment (``x += 1``) or an increment or +decrement reads the old value first, so it is diagnosed like a read. A +``this``-capturing lambda might run immediately, so member reads in its body +count at the point the lambda is created (and writes there earn no credit). +Members of an object initialized by a *user-provided* constructor are trusted +(§5.1) and not flow-tracked; only the defining constructor itself is checked. + + +Writes to Subobjects of Uninitialized Objects +--------------------------------------------- + +Piecemeal delayed initialization of an ``[[uninit]]`` object through its +members or elements cannot be validated statically (§5.4, §5.5), so a store +to a *proper subobject* of an ``[[uninit]]`` entity is rejected (rule +``uninit_write``); writing the whole entity is that entity's initialization +and stays legal: .. code-block:: c++ @@ -857,51 +358,145 @@ initialization sequence. void g(int *p [[ref_to_uninit]]) { *p = 5; // OK: a write through the marker is the pointee's - // initialization (the deferred construct_at slice) + // initialization } -- Diagnostic: ``err_init_uninit_subobject_write`` (a ``%select`` - distinguishes a member store from an element store). -- Recognizer: the shared classifier run with its *write* access preset - (``UninitAccessOpts``, see R7): the top-level drop makes a whole-entity - store legal, and ``TrustRefToUninit`` classifies storage reached through a - ``[[ref_to_uninit]]`` pointer/reference (or returned by a marked function) - as unknown, so a store through the marker is neither banned nor endorsed. - The shared arms cover member chains, array elements (``a[i]``, ``*a``, - member arrays -- on a named object or the current one), ``(&s)->x``, and - the conditional/comma/braced pass-through bases. -- Check sites: ``Sema::CheckAssignmentOperands`` -- the funnel both simple - and compound assignment converge on, so ``=`` and every ``op=`` are checked - exactly once, and class-typed ``operator=`` (which diverts to overload - resolution) never reaches it -- and the built-in increment/decrement arm of - ``Sema::CreateBuiltinUnaryOp`` (overloaded class ``++``/``--`` never - reaches it). Both are Decl-less sites: they defer only on an - instantiation-dependent store target -- always rebuilt and re-checked at - instantiation -- and otherwise fire at definition time, repeating if the - operator is rebuilt at instantiation anyway (see Pattern 1 and R7). -- A compound assignment or a built-in ``++``/``--`` also *reads* the old - value, so on a subobject of a marked object the R7 read-through diagnostic - fires alongside this rule's (the shift forms load through their LHS - promotion, the rest through R7's operator-site hooks). -- ``std::byte`` stores are exempt (paper §4.5), matching every read-side - rule. -- Whole-object assignment to a marked class object (``s = S{...}``) diverts - to the overloaded ``operator=`` path and so never reaches this rule; it is - caught instead by the ``ref_to_uninit`` object-argument check (R7) when the - member ``operator=`` binds its implicit object parameter to the marked - object. Class-type writes remain uniformly deferred with construct_at. -- Known gaps: writes through ``[[ref_to_uninit]]`` are deliberately out of - scope (for a scalar the write is the initialization; verifying class-type - writes needs construct_at modeling). - -Diagnostic suppression ----------------------- - -Every rule is suppressible per-site with -``[[profiles::suppress(std::init)]]`` (covers all rules) or -``[[profiles::suppress(std::init, rule: "rule_name")]]`` (rule-targeted). -The token-based-dominion limitation noted earlier applies: a suppress -attribute on a ``VarDecl`` covers only that declaration's tokens. +A compound assignment or increment/decrement of such a subobject also reads +its old value, so the read diagnostic fires alongside this one. Assigning a +whole *class* object marked ``[[uninit]]`` (``s = S{1, 2};``) is caught too: +the member ``operator=`` binds the uninitialized object as its implicit +object argument (see the next section). + + +Binding Pointers and References +------------------------------- + +A pointer or reference must be bound consistently with its +``[[ref_to_uninit]]`` marking (rule ``ref_to_uninit``, §4.3): a marked +pointer, reference, or function return may only refer to uninitialized +memory, and an unmarked one only to initialized memory. Whether a source +refers to uninitialized memory is recognized from its form alone, with no +flow analysis: the address or a subobject of an ``[[uninit]]`` entity, the +value or dereference of a marked pointer or reference, pointer and reference +casts of those, a call to a marked function, and a ``new`` expression that +default-initializes a type with indeterminate scalars (``new int``, +``new int[n]``; §1.2): + +.. code-block:: c++ + + int i = 7; + int u [[uninit]]; + + int *p1 = &i; // OK + int *p2 = &u; // error: needs [[ref_to_uninit]] + int *p3 [[ref_to_uninit]] = &u; // OK + int *p4 [[ref_to_uninit]] = &i; // error: 'i' is initialized + int *p5 [[ref_to_uninit]] = new int; // OK: uninitialized new (§1.2) + + void sink(int *q); + void fill(int *q [[ref_to_uninit]]); + + void f() { + sink(p3); // error: 'sink' expects initialized memory + fill(p3); // OK + } + +The check applies wherever a pointer or reference is bound: variable, member, +and aggregate initialization, assignment, call arguments (including defaulted +and variadic ones), ``return`` and ``throw`` statements, lambda captures, and +the implicit object argument of a member call -- so calling a member function +on an object recognized as uninitialized storage is rejected, and so is +copying a class object out of one. For the copy, the escape is the paper's +own (§7.2): declare the copy constructor's parameter ``[[ref_to_uninit]]``. +Positions that cannot carry the marker -- a variadic argument, a parameter of +a function called through a function pointer, the implicit object parameter +-- are checked as unmarked targets; suppress at the call site if the flow is +intended. A source whose form the recognizer cannot classify (pointer +arithmetic, an integer-to-pointer cast) is accepted for marked and unmarked +targets alike. + + +Constructors +------------ + +A user-provided constructor must initialize every non-static data member +through its member-initializer list or a default member initializer unless +the member is marked ``[[uninit]]`` (rule ``ctor_uninit_member``, §5.1); an +assignment in the constructor body does not count (reads of an ``[[uninit]]`` +member before such an assignment are flow-checked, as above). Direct +non-virtual base subobjects must be initialized the same way -- a base cannot +carry ``[[uninit]]``, so it must always be initialized, by a written base +initializer or by the base's own user-provided default constructor: + +.. code-block:: c++ + + struct X { + int a; + int b = 0; + int c [[uninit]]; + X(int v) : a(v) {} // OK: 'a' is written, 'b' has a default member + }; // initializer, 'c' is acknowledged + + struct Y { + int m; + Y() { // error: 'm' is not initialized (ctor_uninit_member) + m = 1; // (a body assignment does not count) + } + }; + +A member whose type's default-initialization leaves unacknowledged scalars +indeterminate (a nested aggregate) is flagged the same way. A delegating +constructor is exempt -- its target initializes the members -- and so is a +union's own constructor, whose members are mutually exclusive (§5.6). + + +Global and Static Variables +--------------------------- + +Variables with static or thread storage duration are zero-initialized, so +they are never uninitialized (which is why ``[[uninit]]`` is rejected on +them, above). Non-local variables with static storage duration must +additionally be initialized at compile time (rule ``static_runtime_init``, +§3), because cross-translation-unit initialization order can otherwise +produce a read of a not-yet-initialized object: + +.. code-block:: c++ + + int seed(); + + int g1 = 42; // OK: constant-initialized + int g2 = seed(); // error: runtime initializer (static_runtime_init) + thread_local int t = seed(); // OK: thread storage duration + + int &counter() { + static int c = seed(); // OK: a function-local static is initialized + return c; // on first use (§3) + } + + +Limitations +----------- + +The implemented slice is deliberately conservative; each omission below is a +missed diagnostic, never a false positive. + +- ``construct_at``/``destroy_at`` flow is not modeled: class-type + initialization of uninitialized storage, double initialization, and double + destruction are not checked, and writes through ``[[ref_to_uninit]]`` are + not verified. +- A ``new`` expression whose result is not bound to anything (``new int;``) + is not checked. +- A call through a function pointer cannot see parameter markers on the + pointed-to function, and a member call through a pointer-to-member bypasses + the object-argument check. +- Members of anonymous structs and unions, and arrays of aggregates, are not + flow-tracked. +- Virtual base subobjects are not checked by ``ctor_uninit_member`` (they are + initialized by the most-derived class). +- A read of a tracked member inside another member's default initializer is + not detected. +- In a template, a violation in non-dependent code is diagnosed at definition + time and may be repeated at instantiation. Test Profiles From c3efa1789195e57df22c65231140c1b4f8ba2ef5 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 11:59:04 -0400 Subject: [PATCH 246/289] Document strict assignment-only crediting in the std::init ctor-body pass --- clang/docs/ProfilesFramework.rst | 40 +++++++++++++++---- clang/lib/Sema/AnalysisBasedWarnings.cpp | 12 ++++++ .../SemaCXX/safety-profile-init-ctor-body.cpp | 39 ++++++++++++++++++ .../safety-profile-init-local-member-read.cpp | 3 ++ 4 files changed, 86 insertions(+), 8 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index f26fc5d46a8f5..5683c141de48d 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -327,11 +327,20 @@ rejected outright: A member or variable counts as assigned only when every path to the read assigns it (§1.3), and a compound assignment (``x += 1``) or an increment or -decrement reads the old value first, so it is diagnosed like a read. A -``this``-capturing lambda might run immediately, so member reads in its body -count at the point the lambda is created (and writes there earn no credit). -Members of an object initialized by a *user-provided* constructor are trusted -(§5.1) and not flow-tracked; only the defining constructor itself is checked. +decrement reads the old value first, so it is diagnosed like a read. Inside +a constructor body only that plain whole-member assignment earns credit: +passing ``&m`` to a function (even one whose parameter is marked +``[[ref_to_uninit]]``), binding a reference to the member, calling a member +function, or letting ``this`` escape does not count as initializing ``m`` -- +the paper rejects complex constructor code (§5.1) and reserves +callee-initialization for ``now_init()`` (§6.2); suppress the rule where +such a flow is intended. A ``this``-capturing lambda might run immediately, +so member reads in its body count at the point the lambda is created (and +writes there earn no credit -- the same strict policy). For a *local* +variable the analyses instead treat any escape as an assignment (see +`Limitations`_). Members of an object initialized by a *user-provided* +constructor are trusted (§5.1) and not flow-tracked; only the defining +constructor itself is checked. Writes to Subobjects of Uninitialized Objects @@ -477,9 +486,24 @@ produce a read of a not-yet-initialized object: Limitations ----------- -The implemented slice is deliberately conservative; each omission below is a -missed diagnostic, never a false positive. - +The implemented slice is deliberately conservative. The first entry below +is a deliberate strictness -- it rejects code the paper itself rejects -- +rather than an omission; each of the others is a missed diagnostic, never a +false positive. + +- Inside a constructor body, *only* a plain assignment to an ``[[uninit]]`` + member counts as its initialization. Taking the member's address, binding + a reference to it, calling a member function, letting ``this`` escape, or + passing ``&m`` to a ``[[ref_to_uninit]]`` parameter earns no credit, so a + later read of the member is rejected: the paper rejects complex + constructor code (§5.1) and reserves callee-initialization for + ``now_init()`` (§6.2); the remedy for an intended flow is + ``[[profiles::suppress]]``. For *locals*, by contrast, the local-aggregate + pass and the plain-local analysis conservatively treat any escape of the + variable as an assignment -- there the omission is a missed diagnostic, + never a false positive. That escape-crediting is an interim deviation + from the paper, to be replaced by explicit ``now_init()`` recognition when + it is implemented. - ``construct_at``/``destroy_at`` flow is not modeled: class-type initialization of uninitialized storage, double initialization, and double destruction are not checked, and writes through ``[[ref_to_uninit]]`` are diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 8ae222dfb278f..84f2f1a474734 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1803,6 +1803,18 @@ static void collectTrackedUninitMembers( // the violation. There is no constructor-exit requirement: a member that is // simply never read is left as-is, exactly as the structural ctor_uninit_member // check (R5) excuses a marked member. +// +// Crediting is strictly assignment-only: nothing but a plain whole-member +// store (or a written member/base initializer) marks a member assigned. +// Taking the member's address, binding a reference to it, calling a member +// function, letting `this` escape, or passing &m to a [[ref_to_uninit]] +// parameter earns no credit -- the paper rejects complex constructor code +// (§5.1) and reserves callee-initialization for now_init() (§6.2), so such +// code is deliberately rejected here (the remedy is [[profiles::suppress]]). +// This is the deliberate counterpart of checkInitProfileLocalMembers' +// "soundness over completeness" escape crediting below: locals conservatively +// credit any escape (missed diagnostics only), while constructor bodies get +// the paper's strictness. static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, AnalysisDeclContext &AC) { // A delegating constructor leaves member initialization to its target (paper diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index 0c66d83acf245..57ab2bc076ee0 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -296,6 +296,45 @@ struct LambdaReadSuppressed { } }; +// Strict assignment-only crediting: inside a constructor body, nothing but a +// plain whole-member assignment counts as initializing an [[uninit]] member. +// Passing &m to a [[ref_to_uninit]] parameter, calling a member function that +// assigns it, or letting `this` escape earns no credit -- the paper rejects +// complex constructor code (§5.1) and reserves callee-initialization for +// now_init() (§6.2); the remedy is [[profiles::suppress]]. Contrast the +// *local*-object passes, which conservatively credit any escape +// (safety-profile-init-local-member-read.cpp, test_escape_*). +void init_pointee(int *p [[ref_to_uninit]]); +struct EscapeMemberAddress { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + EscapeMemberAddress() { + init_pointee(&m); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +struct EscapeMemberCall { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + void setup() { m = 1; } + EscapeMemberCall() { + setup(); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +struct EscapeThis; +void take_this(EscapeThis *); +struct EscapeThis { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + EscapeThis() { + take_this(this); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + // [[uninit]] members inherited from a non-virtual base with no user-provided // constructor are tracked like the class's own: nothing can have assigned // them before the derived body runs. A base with a user-provided constructor diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp index 5095a7fb51d9f..a4d05f4d21038 100644 --- a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -116,6 +116,9 @@ int test_branch_both_paths(bool c) { // Any appearance of the variable outside a recognized member read or write // conservatively marks every member assigned: the address may be used to // initialize the object (construct_at, memcpy, an initializing callee). +// These pin the interim (pre-now_init()) leniency, paper §6.2; contrast the +// ctor-body pass's strict assignment-only crediting +// (safety-profile-init-ctor-body.cpp, Escape*). int test_escape_address_of_object() { Agg a; (void)&a; From 148eb6e2818aadb0e1f28c9814eeb51e3ab417f7 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 12:26:32 -0400 Subject: [PATCH 247/289] Reject [[uninit]] when default-initialization is not a no-op --- clang/docs/ProfilesFramework.rst | 40 ++++- clang/include/clang/Basic/AttrDocs.td | 7 +- .../clang/Basic/DiagnosticSemaKinds.td | 11 +- clang/include/clang/Sema/SemaProfiles.h | 16 ++ clang/lib/Sema/SemaProfiles.cpp | 157 +++++++++++++++--- .../safety-profile-init-field-marker.cpp | 97 ++++++++++- .../SemaCXX/safety-profile-init-static.cpp | 16 ++ .../safety-profile-init-with-initializer.cpp | 31 +++- 8 files changed, 341 insertions(+), 34 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 5683c141de48d..480c43d03ca89 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -219,7 +219,9 @@ Rule Diagnoses ``ctor_uninit_member`` A constructor that leaves a member or base subobject uninitialized. ``static_runtime_init`` A non-local variable with a runtime initializer. -``uninit_with_initializer`` ``[[uninit]]`` combined with an initializer. +``uninit_with_initializer`` ``[[uninit]]`` combined with an initializer, or + on an entity whose default-initialization is not + a no-op. ``pointer_marker`` ``[[uninit]]`` on a pointer. ``union_marker`` ``[[uninit]]`` on a union object or member. ``static_marker`` ``[[uninit]]`` on a variable with static or thread @@ -252,7 +254,9 @@ either be initialized or carry ``[[uninit]]`` (rule ``uninit_decl``): A class type with a user-provided default constructor is trusted to initialize its members (§5.1), and a data member that is itself marked ``[[uninit]]`` is acknowledged -- a type whose only indeterminate scalars are -all marked does not trigger the rule. +all marked does not trigger the rule. Marking a variable of such a type +``[[uninit]]`` is likewise consistent: its default-initialization is a +genuine no-op. Where ``[[uninit]]`` May Not Go @@ -281,8 +285,29 @@ guarantee -- are rejected: Each of these keys on the array element type, so an array of pointers or of unions is rejected exactly like a single one. A no-op initialization -- the -trivial default-initialization of a scalar or aggregate -- is consistent with -the marker and accepted. +trivial default-initialization of a scalar or aggregate that leaves a scalar +subobject indeterminate -- is consistent with the marker; any other +synthesized initialization contradicts it and is rejected (§5.3): a member or +base with a user-provided default constructor, a default member initializer, +a virtual table pointer, or a value-initialization (``= P()``), all of which +initialize something. The same rule covers a marked data member whose type's +default-initialization is not a no-op: + +.. code-block:: c++ + + struct Str { Str() : cap(0) {} int cap; }; + struct S { int x; Str s; }; + + void g() { + S s4 [[uninit]]; // error: 's4.s' is default-constructed, so 's4' is + // not left uninitialized (uninit_with_initializer, §5.3) + } + + struct Buf { + Str s [[uninit]]; // error: default-initialization of 'Str' runs a + // constructor, so 's' cannot be left uninitialized + int n [[uninit]]; // OK: a scalar member really is left uninitialized + }; Reads of Uninitialized Objects @@ -463,8 +488,11 @@ Global and Static Variables --------------------------- Variables with static or thread storage duration are zero-initialized, so -they are never uninitialized (which is why ``[[uninit]]`` is rejected on -them, above). Non-local variables with static storage duration must +they are never uninitialized: ``[[uninit]]`` on one is rejected -- by +``static_marker`` when nothing else initializes the object, and by +``uninit_with_initializer`` when a written initializer or a non-no-op +default-initialization already contradicts the marker (exactly one of the +pair fires). Non-local variables with static storage duration must additionally be initialized at compile time (rule ``static_runtime_init``, §3), because cross-translation-unit initialization order can otherwise produce a read of a not-yet-initialized object: diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index c4680b1baf6db..454f993aa187e 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -895,7 +895,12 @@ rule still applies. A declaration or non-static data member with both ``[[uninit]]`` and an initializer is ill-formed under the initialization profile (the -marker contradicts the explicit initialization). +marker contradicts the explicit initialization). The same applies when +default-initialization of the entity's type is not a genuine no-op -- a +non-trivial default constructor, a default member initializer, or a virtual +table pointer initializes something, and a type with nothing left +indeterminate has nothing to acknowledge -- so the marker is rejected there +too. On a non-static data member the marker also excuses the member from the profile's requirement that every constructor initialize it. A scalar marked diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index c42e208e349ac..d8d6e9bb414ae 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14162,8 +14162,15 @@ def err_init_static_runtime_init : ProfileRuleError< "non-local variable %1 requires constant initialization under " "profile '%0'">; def err_init_uninit_with_initializer : ProfileRuleError< - "variable %1 cannot be both '[[uninit]]' and have an initializer " - "under profile '%0'">; + "%select{variable|member}2 %1 cannot be both '[[uninit]]' and have an " + "initializer under profile '%0'">; +def err_init_uninit_member_initialized : ProfileRuleError< + "member %1 cannot be marked '[[uninit]]' under profile '%0'; " + "default-initialization of its type %2 does not leave it uninitialized">; +def note_init_uninit_member_type : Note< + "%select{default-initialization of %0 runs a constructor|" + "no subobject of %0 is left uninitialized|" + "%0 has no usable default constructor}1">; def err_uninit_attr_invalid_subject : Error< "'uninit' attribute cannot be applied to %select{a reference|" "a function parameter|a structured binding}0">; diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 3586fc5457a76..92fd60aff251f 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -198,6 +198,22 @@ class SemaProfiles : public SemaBase { bool defaultInitLeavesScalarIndeterminate(QualType T, bool HonorUninitMarkers = false); + /// True if default-initialization of \p T is a genuine no-op that leaves + /// the object (or every array element) uninitialized -- the only state + /// consistent with an [[uninit]] marker. A non-trivial default constructor + /// (user-provided anywhere in the subtree, a default member initializer, a + /// virtual table pointer) initializes something, contradicting the marker + /// (paper §4.2 rule 2, §5.3); a deleted or absent default constructor makes + /// default-initialization ill-formed, not a no-op (the marker is + /// unsatisfiable); an all-scalars-determinate type (e.g. an + /// empty struct) has nothing uninitialized. Type-level (not a query on the + /// synthesized construct-expression) because the field flavor and + /// static_marker's no-initializer arm have no construct-expression, and + /// getBaseElementType handles arrays and scalars uniformly. Shared by + /// uninit_with_initializer and static_marker (through their common + /// initializer guard) and the field-marker flavor. + bool defaultInitIsVacuous(QualType T); + /// If \p E (stripped of parens and implicit casts) directly names a /// declaration -- a DeclRefExpr or a MemberExpr -- return that declaration; /// otherwise null. The std::init checks read [[ref_to_uninit]] / diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 578c9144cb484..6701c413916ca 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -568,6 +568,56 @@ bool SemaProfiles::defaultInitLeavesScalarIndeterminate(QualType T, HonorUninitMarkers, Visited); } +bool SemaProfiles::defaultInitIsVacuous(QualType T) { + QualType BaseTy = getASTContext().getBaseElementType(T); + if (const auto *RD = BaseTy->getAsCXXRecordDecl()) { + // A non-trivial default constructor (user-provided anywhere in the + // subtree, a default member initializer, a virtual table pointer) + // initializes something, contradicting an [[uninit]] marker (paper §4.2 + // rule 2, §5.3). hasTrivialDefaultConstructor asserts without a + // definition. + if (!RD->hasDefinition() || !RD->hasTrivialDefaultConstructor()) + return false; + // A deleted default constructor keeps the triviality bit, but makes + // default-initialization ill-formed rather than a no-op: the entity can + // never be left default-initialized, so the marker is unsatisfiable. Only + // a declared deleted constructor is visible here; a lazily *implicitly* + // deleted one of an otherwise trivial type escapes this scan -- a missed + // diagnostic (never a false positive), like the framework's other + // conservative omissions. + for (const CXXConstructorDecl *Ctor : RD->getDefinition()->ctors()) + if (Ctor->isDefaultConstructor() && Ctor->isDeleted()) + return false; + } + // The factual (HonorUninitMarkers=false) walk: an all-scalars-determinate + // type (e.g. an empty struct) has nothing uninitialized, so the marker + // contradicts it too, while a type whose only indeterminate scalars are + // themselves marked members really is left uninitialized. + return defaultInitLeavesScalarIndeterminate(T, /*HonorUninitMarkers=*/false); +} + +// Whether the declaration initializer \p Init is a vacuous +// default-initialization of \p T: one that runs no code and leaves the object +// factually uninitialized, hence consistent with an [[uninit]] marker. No +// initializer at all (a scalar default-init synthesizes none) is vacuous; a +// synthesized trivial default-constructor call is vacuous iff the type's +// default-initialization is (defaultInitIsVacuous); anything else -- a +// user-written initializer, a `= P()` value-initialization (a +// CXXTemporaryObjectExpr, which zeroes), any zero-initializing construction +// -- initializes the object and contradicts the marker. The shared guard of +// static_marker and uninit_with_initializer keeps the pair complementary by +// construction: exactly one of the two fires for a marked static. +static bool isVacuousDefaultInit(SemaProfiles &SP, const Expr *Init, + QualType T) { + if (!Init) + return true; + if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) + return CCE->getConstructor()->isDefaultConstructor() && + !isa(CCE) && + !CCE->requiresZeroInitialization() && SP.defaultInitIsVacuous(T); + return false; +} + void SemaProfiles::checkInitProfileUninitDecl(const VarDecl *Var) { // std::init / uninit_decl: a definition without any initializer (after // attempted default-initialization) must either carry [[uninit]] or @@ -609,12 +659,14 @@ void SemaProfiles::checkInitProfileStaticMarker(const VarDecl *Var) { // duration is zero-initialized by language rule (paper §3), so it is an // initialized object; marking it [[uninit]] contradicts paper §4.2 ("an // initialized object marked [[uninit]] is an error"). The case with a real - // initializer -- explicit, or a constructor that actually runs -- is - // already caught by uninit_with_initializer (R4, in - // CheckCompleteVariableDeclaration); this covers the zero-initialized, - // no-real-initializer case R4 treats as a consistent no-op. The factual - // (HonorUninitMarkers=false) walk matches R4: a static object whose only - // indeterminate scalars are themselves marked is still zero-initialized. + // initializer -- explicit, or a default-initialization that is not a no-op + // -- is already caught by uninit_with_initializer (R4, in + // CheckCompleteVariableDeclaration); this covers the vacuous-initialization + // case R4 treats as consistent. The guard is the shared + // isVacuousDefaultInit, so the pair stays complementary by construction and + // exactly one of static_marker / uninit_with_initializer fires (this one + // runs first, from ActOnUninitializedDecl, after the synthesized + // default-initialization is attached). static constexpr StringRef Profile = "std::init"; QualType BaseTy = getASTContext().getBaseElementType(Var->getType()); if (!Var->isInvalidDecl() && @@ -628,10 +680,7 @@ void SemaProfiles::checkInitProfileStaticMarker(const VarDecl *Var) { !BaseTy->isUnionType() && !BaseTy->isPointerType() && shouldEmitProfileViolation(Profile, "static_marker", Var->getLocation(), Var) && - (!Var->getInit() || - (BaseTy->isRecordType() && - defaultInitLeavesScalarIndeterminate(Var->getType(), - /*HonorUninitMarkers=*/false)))) { + isVacuousDefaultInit(*this, Var->getInit(), Var->getType())) { bool IsThread = Var->getStorageDuration() == SD_Thread; Diag(Var->getLocation(), diag::err_init_uninit_static_marker) << Profile << Var->getDeclName() << IsThread; @@ -683,18 +732,18 @@ void SemaProfiles::checkInitProfileUninitWithInitializer(const ValueDecl *D, // Gate the (possibly recursive) type walk below on enforcement. if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) return; - // A synthesized default-initialization that leaves the object indeterminate - // (a trivial or aggregate type, no user-written initializer) is consistent - // with the marker: the object really is left uninitialized, to be - // initialized later (e.g. via construct_at), mirroring the scalar case. Only - // a real initializer -- an explicit one, or a constructor that actually runs - // -- contradicts the marker. - if (const auto *CCE = dyn_cast(Init->IgnoreImplicit())) - if (CCE->getConstructor()->isDefaultConstructor() && - defaultInitLeavesScalarIndeterminate(D->getType())) - return; + // A vacuous default-initialization -- a synthesized trivial + // default-constructor call that runs no code and leaves the object + // indeterminate -- is consistent with the marker: the object really is left + // uninitialized, to be initialized later (e.g. via construct_at), mirroring + // the scalar case. Anything else -- an explicit initializer, a `= P()` + // value-initialization, or a default-initialization that initializes + // something (a non-trivial default constructor, paper §4.2 rule 2, §5.3) -- + // contradicts it. + if (isVacuousDefaultInit(*this, Init, D->getType())) + return; Diag(Loc, diag::err_init_uninit_with_initializer) - << Profile << D->getDeclName(); + << Profile << D->getDeclName() << isa(D); } void SemaProfiles::checkInitProfileMarkerPlacement(const Decl *D) { @@ -1467,9 +1516,75 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { } } +void runStdInitUninitFieldMarkerCallback(Sema &S, CXXRecordDecl *RD) { + // std::init / uninit_with_initializer, field flavor (paper §4.2 rule 2, + // §5.3): [[uninit]] on a data member claims default-initialization leaves + // the member uninitialized. When the member type's default-initialization + // is not a no-op (a non-trivial default constructor initializes something) + // or leaves nothing indeterminate (nothing to acknowledge), the marker is a + // contradiction, just like a variable's explicit initializer. The NSDMI + // case is the ActOnFinishCXXInClassMemberInitializer flavor's to diagnose; + // this covers the initializer-less member, which only the class walk sees. + // + // A union's members are union_marker's territory (the marker is banned on + // them wholesale, paper §5.6). + if (RD->isUnion()) + return; + for (const FieldDecl *F : RD->fields()) { + const auto *UA = F->getAttr(); + // hasInClassInitializer is style-based, so it is true even while a + // late-parsed NSDMI is still pending. + if (!UA || F->isInvalidDecl() || F->hasInClassInitializer()) + continue; + QualType BaseTy = S.Context.getBaseElementType(F->getType()); + // A union- or pointer-typed member (keyed on the same base element type) + // already draws union_marker / pointer_marker and keeps the marker; do + // not pile a second diagnostic on top. Load-bearing for union members: a + // union with a non-trivial member has a deleted -- hence non-trivial -- + // default constructor and would otherwise draw both. + if (BaseTy->isUnionType() || BaseTy->isPointerType()) + continue; + // std::byte may be left uninitialized (paper §4), mirroring + // checkInitProfileUninitDecl. + if (BaseTy->isStdByteType()) + continue; + if (S.Profiles().defaultInitIsVacuous(F->getType())) + continue; + // Decl-aware gate: defers on templated patterns (instantiations re-fire + // through CheckCompletedCXXClass) and honors [[profiles::suppress]] on + // the field or the enclosing class. Diagnose at the attribute -- the + // marker is the thing to delete -- like union_marker / pointer_marker. + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "uninit_with_initializer", UA->getLocation(), F)) + continue; + S.Diag(UA->getLocation(), diag::err_init_uninit_member_initialized) + << "std::init" << F->getDeclName() << F->getType(); + // Distinguish the non-vacuity reasons for the note: a non-trivial default + // constructor runs code (0); a trivial one that leaves no subobject + // uninitialized has nothing to acknowledge (1); a deleted or absent one + // makes the marker unsatisfiable (2). + unsigned Reason = 1; + if (const auto *MemberRD = BaseTy->getAsCXXRecordDecl(); + MemberRD && MemberRD->hasDefinition()) { + const CXXRecordDecl *Def = MemberRD->getDefinition(); + bool Deleted = !Def->hasDefaultConstructor(); + for (const CXXConstructorDecl *Ctor : Def->ctors()) + if (Ctor->isDefaultConstructor() && Ctor->isDeleted()) + Deleted = true; + if (Deleted) + Reason = 2; + else if (!Def->hasTrivialDefaultConstructor()) + Reason = 0; + } + S.Diag(F->getTypeSpecStartLoc(), diag::note_init_uninit_member_type) + << BaseTy << Reason; + } +} + // Class-finalization opt-in table (pattern 3). constexpr FinalizationProfile ClassFinalizationProfiles[] = { {"test::class_final", &runTestClassFinalCallback}, + {"std::init", &runStdInitUninitFieldMarkerCallback}, }; // Constructor-finalization opt-in table (pattern 4). diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index 4a82b5e3e3207..cd5afd7930247 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -13,11 +13,11 @@ struct PlainFieldPrefix { }; struct FieldWithNSDMI { - int m [[uninit]] = 0; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + int m [[uninit]] = 0; // expected-error {{member 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; struct FieldWithNSDMIPrefix { - [[uninit]] int m = 0; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + [[uninit]] int m = 0; // expected-error {{member 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; // A static data member is a zero-initialized static object, so its definition @@ -33,7 +33,7 @@ int WithStaticDataMember::t; // expected-error {{'[[uninit]]' cannot be applied struct MultipleFields { int a [[uninit]]; int b = 0; - int c [[uninit]] = 0; // expected-error {{variable 'c' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + int c [[uninit]] = 0; // expected-error {{member 'c' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; template @@ -139,3 +139,94 @@ struct LeakMarker { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init)]] LeakMarker leak_marker_use{nullptr}; // expected-note {{in instantiation of template class 'LeakMarker' requested here}} // expected-error@#leak-marker-field {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} + +// std::init / uninit_with_initializer, field flavor (paper §4.2 rule 2, +// §5.3): [[uninit]] on a member whose type's default-initialization is not a +// genuine no-op is a contradiction -- something is initialized, or nothing +// is left uninitialized. Diagnosed at the marker, with the reason at the +// member type. +namespace std { enum class byte : unsigned char {}; } + +struct RunsCtor { RunsCtor() : cap(0) {} int cap; }; +struct TrivialAgg { int a; }; + +struct MemberRunsCtor { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} +}; + +struct MemberVacuousKinds { + int x [[uninit]]; // OK: a scalar member really is left uninitialized + TrivialAgg t [[uninit]]; // OK: trivial aggregate, a genuine no-op + std::byte b [[uninit]]; // OK: std::byte may stay uninitialized (paper §4) +}; + +// An NSDMI'd marked member is the NSDMI flavor's to diagnose -- exactly one +// diagnostic, no field-flavor double (hasInClassInitializer is style-based, +// so the skip holds even while the NSDMI is late-parse-pending). +struct MemberNSDMIOnce { + RunsCtor s [[uninit]] = RunsCtor(); // expected-error {{member 's' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} +}; + +// A union- or pointer-typed marked member draws only union_marker / +// pointer_marker -- no field-flavor pile-on. Load-bearing for the union: V's +// implicitly deleted default constructor is unusable, so without the +// base-element-type skip the member would draw both diagnostics. +union V { RunsCtor s; }; +struct MemberUnionOnly { + V v [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a data member of union type under profile 'std::init'}} +}; +struct MemberPointerOnly { + int *p [[uninit]]; // expected-error {{'[[uninit]]' cannot be applied to a pointer under profile 'std::init'; initialize the pointer (for example to 'nullptr')}} +}; + +// A member type with a deleted (or absent) default constructor can never be +// left default-initialized, so the marker is unsatisfiable. +struct NoDefault { NoDefault() = delete; int x; }; +struct MemberDeletedCtor { + NoDefault n [[uninit]]; // expected-error {{member 'n' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'NoDefault' does not leave it uninitialized}} \ + // expected-note {{'NoDefault' has no usable default constructor}} +}; + +// An all-determinate type leaves nothing to acknowledge. +struct Empty {}; +struct MemberEmpty { + Empty e [[uninit]]; // expected-error {{member 'e' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'Empty' does not leave it uninitialized}} \ + // expected-note {{no subobject of 'Empty' is left uninitialized}} +}; + +// A written member-initializer in some constructor does not rescue the +// marker: both branches contradict it (the member is initialized either +// way). +struct MemberCtorInit { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + MemberCtorInit() : s{} {} +}; + +// Arrays key on the base element type, like the other marker rules. +struct MemberArrayOfCtor { + RunsCtor arr [[uninit]][2]; // expected-error {{member 'arr' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor[2]' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} +}; + +// A dependent marked member defers on the pattern and fires once at +// instantiation, when the substituted type's vacuity is known. +template +struct DependentVacuity { + T m [[uninit]]; // #dependent-vacuity-member +}; +template struct DependentVacuity; // OK: vacuous for a scalar +template struct DependentVacuity; // expected-note {{in instantiation of template class 'DependentVacuity' requested here}} +// expected-error@#dependent-vacuity-member {{member 'm' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} +// expected-note@#dependent-vacuity-member {{default-initialization of 'RunsCtor' runs a constructor}} + +// Suppression: rule-targeted on the field, and whole-profile on the class. +struct SuppressedFieldMarker { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "uninit_with_initializer")]] RunsCtor s [[uninit]]; // OK: suppressed +}; +// no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} +struct [[profiles::suppress(std::init)]] SuppressedClassFieldMarker { + RunsCtor s [[uninit]]; // OK: suppressed by the class-level attribute +}; diff --git a/clang/test/SemaCXX/safety-profile-init-static.cpp b/clang/test/SemaCXX/safety-profile-init-static.cpp index 12d1af1fdb85f..6808ca5e8b9bd 100644 --- a/clang/test/SemaCXX/safety-profile-init-static.cpp +++ b/clang/test/SemaCXX/safety-profile-init-static.cpp @@ -129,6 +129,22 @@ void test_local_static_running_ctor() { (void)w; } +struct MixedAgg { int x; WithCtor s; }; +void test_local_static_mixed() { + // A default-initialization that is not a no-op (a member's user-provided + // constructor runs) is a real initializer too, so the mixed aggregate + // SWITCHES to uninit_with_initializer; static_marker stays silent -- the + // shared vacuity guard keeps the pair complementary (exactly one error). + static MixedAgg m [[uninit]]; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)m; +} + +// At namespace scope the same case additionally draws the independent +// static_runtime_init (the member's constructor is a runtime initializer) -- +// a pre-existing pairing, not a static_marker double. +MixedAgg g_mixed_marked [[uninit]]; // expected-error {{variable 'g_mixed_marked' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} \ + // expected-error {{non-local variable 'g_mixed_marked' requires constant initialization under profile 'std::init'}} + // Suppression: rule-targeted and whole-profile. // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "static_marker")]] int g_marker_suppressed_rule [[uninit]]; diff --git a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp index 763906b227244..2723039380029 100644 --- a/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp +++ b/clang/test/SemaCXX/safety-profile-init-with-initializer.cpp @@ -127,11 +127,40 @@ void template_never_instantiated() { (void)x; } +// Default-initialization that is not a genuine no-op contradicts the marker +// exactly like a written initializer (paper §4.2 rule 2, §5.3): something is +// initialized, so the object is not left uninitialized. +struct MixedInner { MixedInner(); }; +struct Mixed { int x; MixedInner s; }; +struct WithNSDMIMember { int a; int b = 0; }; +struct Polymorphic { virtual void f(); int x; }; +struct TrivialAgg { int x; }; + +void test_default_init_not_noop() { + Mixed s4 [[uninit]]; // expected-error {{variable 's4' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + WithNSDMIMember q [[uninit]]; // expected-error {{variable 'q' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + Polymorphic v [[uninit]]; // expected-error {{variable 'v' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + [[uninit]] Mixed arr[2]; // expected-error {{variable 'arr' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + TrivialAgg t [[uninit]]; // OK: a genuine no-op, 't.x' really is left + // indeterminate + (void)s4; (void)q; (void)v; (void)arr; (void)t; +} + +// A `= P()` value-initialization zeroes the object -- an initialization the +// marker contradicts; the `= P{}` list form was already rejected (pinned +// here), and the two must not diverge. +struct P { int a; int b; }; +void test_value_init() { + P p [[uninit]] = P(); // expected-error {{variable 'p' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + P p2 [[uninit]] = P{}; // expected-error {{variable 'p2' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + (void)p; (void)p2; +} + // An NSDMI in a class template is checked once, when its initializer is // instantiated (here forced by initializing the specialization). template struct WithTemplatedNSDMI { - T m [[uninit]] = T{}; // expected-error {{variable 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} + T m [[uninit]] = T{}; // expected-error {{member 'm' cannot be both '[[uninit]]' and have an initializer under profile 'std::init'}} }; void use_templated_nsdmi() { WithTemplatedNSDMI w = {}; // expected-note {{in instantiation of default member initializer 'WithTemplatedNSDMI::m' requested here}} From abe3ca295663f8efc154a6d00bb498bdbad02fae Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 12:32:07 -0400 Subject: [PATCH 248/289] Treat null pointer sources as unknown storage in the std::init recognizers --- clang/docs/ProfilesFramework.rst | 9 ++-- clang/lib/Sema/SemaProfiles.cpp | 42 +++++++++++++++-- .../safety-profile-init-ref-to-uninit.cpp | 46 ++++++++++++++++++- 3 files changed, 89 insertions(+), 8 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 480c43d03ca89..09f7b10909c29 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -445,9 +445,12 @@ own (§7.2): declare the copy constructor's parameter ``[[ref_to_uninit]]``. Positions that cannot carry the marker -- a variadic argument, a parameter of a function called through a function pointer, the implicit object parameter -- are checked as unmarked targets; suppress at the call site if the flow is -intended. A source whose form the recognizer cannot classify (pointer -arithmetic, an integer-to-pointer cast) is accepted for marked and unmarked -targets alike. +intended. A null pointer source -- ``nullptr``, ``0``, ``{}``, or a local +pointer initialized to null -- refers to no object, so it is accepted for +marked and unmarked targets alike (§4.3, §8: the marker means "zero or more +uninitialized objects"). A source whose form the recognizer cannot classify +(pointer arithmetic, an integer-to-pointer cast) is likewise accepted for +either target. Constructors diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 6701c413916ca..84ec7c64d6096 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -917,7 +917,8 @@ const ValueDecl *SemaProfiles::getDirectlyNamedDecl(const Expr *E) { // MismatchingNewDeleteDetector::getNewExprFromInitListOrExpr); a conditional // is uninit if either arm is, so a value that may be uninit forces a marked // target; a comma yields its right operand. \p EmptyListState classifies an -// empty braced list: {} value-initializes a pointer to null (Initialized), +// empty braced list: {} value-initializes a pointer to null, which the +// pointer recognizer classifies Unknown (the null policy at its null arm), // while a glvalue has no empty-list form (Unknown); a multi-element list is // Unknown for both. Returns std::nullopt when E is not a pass-through form. template @@ -962,13 +963,27 @@ pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, return UninitStorage::Unknown; E = E->IgnoreParenImpCasts(); + // An empty braced list value-initializes a pointer to null, so it takes the + // null classification below (Unknown), keeping `= {}` and `= nullptr` + // consistent. if (auto PassThrough = classifyUninitPassThrough( - E, /*EmptyListState=*/UninitStorage::Initialized, + E, /*EmptyListState=*/UninitStorage::Unknown, [&](const Expr *Sub) { return pointerRefersToUninitStorage(Ctx, Sub, Opts); })) return *PassThrough; + // A null pointer refers to no object, so it is consistent with both a + // marked target (the marker means "zero or more uninitialized objects", + // paper §8) and an unmarked one (paper §4.3's f1(p2) example): Unknown, + // which neither direction diagnoses. A dedicated UninitStorage::Null state + // was considered and deferred -- behaviorally identical today; add it only + // when a future rule (e.g. construct_at on null) needs to distinguish null + // from unclassifiable. + if (E->isNullPointerConstant(Ctx, Expr::NPC_ValueDependentIsNotNull) != + Expr::NPCK_NotNull) + return UninitStorage::Unknown; + // Array-to-pointer decay has been stripped above, leaving the array glvalue. // Clear the top-level drop here, like the member-access arm: neither the CFG // uninit_read pass nor the ctor-body pass tracks array elements, and @@ -987,8 +1002,29 @@ pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, // marker is trusted); an unmarked named pointer is a trusted Initialized // pointer (paper §4.3). if (const ValueDecl *VD = SemaProfiles::getDirectlyNamedDecl(E)) { - if (!VD->hasAttr()) + if (!VD->hasAttr()) { + // An unmarked *local* whose declaration initializer is null -- a null + // pointer constant, or an empty braced list, which value-initializes to + // null -- is a null source like the literal (paper §4.3's f1(p2) + // example): Unknown. Reassignment after the null init is a documented, + // accepted missed diagnostic (parse-order leniency). Deliberately + // excluded: globals/extern (an extern pointer may be initialized + // elsewhere, and keeping them Initialized preserves the + // marked-direction diagnostics) and null-NSDMI fields. A *marked* decl + // keeps its marker classification below (respect the explicit marker). + if (const auto *Var = dyn_cast(VD); + Var && Var->hasLocalStorage()) { + if (const Expr *Init = Var->getInit()) { + const Expr *InnerInit = Init->IgnoreParenImpCasts(); + const auto *ILE = dyn_cast(InnerInit); + if ((ILE && ILE->getNumInits() == 0) || + InnerInit->isNullPointerConstant( + Ctx, Expr::NPC_ValueDependentIsNotNull) != Expr::NPCK_NotNull) + return UninitStorage::Unknown; + } + } return UninitStorage::Initialized; + } return Opts.TrustRefToUninit ? UninitStorage::Unknown : UninitStorage::Uninitialized; } diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 5ceec408be675..d89892dbbc42e 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -116,8 +116,10 @@ void test_braced_pointer() { int *b2 = {&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} int *b3 [[ref_to_uninit]] = {&g_init}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} int *b4 = {&g_init}; // OK - // Empty {} value-initializes to nullptr, like = nullptr: not uninitialized. - int *b5 [[ref_to_uninit]] = {}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + // Empty {} value-initializes to nullptr, like = nullptr: a null source is + // consistent with marked and unmarked targets alike (paper §8, §4.3; see + // test_null_sources). + int *b5 [[ref_to_uninit]] = {}; // OK int *b6 = {}; // OK (void)b1; (void)b2; (void)b3; (void)b4; (void)b5; (void)b6; } @@ -238,6 +240,46 @@ void test_call_arguments() { [[profiles::suppress(std::init)]] { take_ptr(&g_uninit); } // OK: suppressed } +// A null pointer refers to no object, so it is consistent with a marked +// target -- the marker means "zero or more uninitialized objects" (paper §8) +// -- and with an unmarked one (the paper §4.3 f1(p2) example): it classifies +// as unknown storage. This covers the literal forms and a named local whose +// declaration initializer is null. +void test_null_sources() { + take_uninit_ptr(nullptr); // OK: null literal for a marked param + take_uninit_ptr(0); // OK: literal zero + take_ptr(nullptr); // OK + + int *p2 = nullptr; + take_uninit_ptr(p2); // OK: the paper §4.3 example + take_ptr(p2); // OK + + int *q [[ref_to_uninit]] = p2; // OK: null is not affirmatively initialized + int *q2 = p2; // OK + (void)q; (void)q2; + + int *z = {}; // value-initializes to null, like = nullptr + take_uninit_ptr(z); // OK + int *zb [[ref_to_uninit]] = {nullptr}; // OK: braced null recurses to the literal + (void)zb; + + // The null-init classification is parse-order lenient: a reassignment after + // the null declaration is not tracked, so passing the now-initialized + // pointer to a marked parameter is an accepted missed diagnostic. + int *r = nullptr; + r = &g_init; + take_uninit_ptr(r); // accepted: missed diagnostic (parse-order leniency) +} + +// A zero-initialized *global* null pointer stays classified initialized: +// an extern pointer may be initialized elsewhere (another translation unit), +// and keeping globals initialized preserves the marked-direction +// diagnostics. Deliberate residual strictness. +int *g_null_ptr; +void test_null_global() { + take_uninit_ptr(g_null_ptr); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + // A defaulted pointer or reference argument is checked against the parameter's // [[ref_to_uninit]] marking at the call site, like an explicit argument. The // declarations themselves stay clean; the diagnostic fires at the call. From 36d5a17a4856725fc2ba1d88af56874f9a2654ea Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 12:54:23 -0400 Subject: [PATCH 249/289] Credit parse-order whole-entity stores in the std::init recognizers --- clang/docs/ProfilesFramework.rst | 42 ++++- clang/include/clang/Sema/SemaProfiles.h | 48 ++++- clang/lib/Sema/SemaExpr.cpp | 9 + clang/lib/Sema/SemaProfiles.cpp | 164 ++++++++++++++-- .../safety-profile-init-ref-to-uninit.cpp | 176 ++++++++++++++++-- .../SemaCXX/safety-profile-init-write.cpp | 117 +++++++++++- 6 files changed, 506 insertions(+), 50 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 09f7b10909c29..27f45168a82b9 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -318,7 +318,8 @@ analysis verifies reads of uninitialized locals, of ``[[uninit]]`` members within the defining constructor's body, and of ``[[uninit]]`` members of constructor-less aggregate locals; reads through ``[[ref_to_uninit]]`` pointers and references and reads of subobjects of ``[[uninit]]`` objects are -rejected outright: +rejected outright -- unless a prior whole-entity store credits the storage +as initialized (see `Binding Pointers and References`_): .. code-block:: c++ @@ -374,8 +375,9 @@ Writes to Subobjects of Uninitialized Objects Piecemeal delayed initialization of an ``[[uninit]]`` object through its members or elements cannot be validated statically (§5.4, §5.5), so a store to a *proper subobject* of an ``[[uninit]]`` entity is rejected (rule -``uninit_write``); writing the whole entity is that entity's initialization -and stays legal: +``uninit_write``); writing the whole entity is that entity's initialization, +stays legal, and credits the entity as initialized for everything after it +in parse order (see `Binding Pointers and References`_): .. code-block:: c++ @@ -409,12 +411,12 @@ A pointer or reference must be bound consistently with its ``[[ref_to_uninit]]`` marking (rule ``ref_to_uninit``, §4.3): a marked pointer, reference, or function return may only refer to uninitialized memory, and an unmarked one only to initialized memory. Whether a source -refers to uninitialized memory is recognized from its form alone, with no -flow analysis: the address or a subobject of an ``[[uninit]]`` entity, the -value or dereference of a marked pointer or reference, pointer and reference -casts of those, a call to a marked function, and a ``new`` expression that -default-initializes a type with indeterminate scalars (``new int``, -``new int[n]``; §1.2): +refers to uninitialized memory is recognized from its form -- the address or +a subobject of an ``[[uninit]]`` entity, the value or dereference of a +marked pointer or reference, pointer and reference casts of those, a call to +a marked function, and a ``new`` expression that default-initializes a type +with indeterminate scalars (``new int``, ``new int[n]``; §1.2) -- refined by +one parse-order fact, whole-entity stores (below): .. code-block:: c++ @@ -452,6 +454,25 @@ uninitialized objects"). A source whose form the recognizer cannot classify (pointer arithmetic, an integer-to-pointer cast) is likewise accepted for either target. +The parse-order refinement: a *whole-entity store* credits its target as +initialized (§4.2, §4.5). After ``u = 5;`` the ``[[uninit]]`` variable +``u`` counts as initialized, and after ``*p = 5;`` the marked pointer's +pointee does, for every later ``*p`` access -- until ``p`` is reseated +(``p = q``, ``p += n``, ``p++``), which withdraws the credit; a store +through a marked *reference* credits its referent permanently. The credit +works in both directions: ``int *q = &u;`` after the store is accepted, and +``int *r [[ref_to_uninit]] = &u;`` is now rejected -- a credited entity +requires an unmarked target (§4.2). Element accesses (``p[i]``) are never +credited in either direction (§5.4's random-access ban applies even to +``p[0]``), element stores earn no credit, and address escapes (passing +``&u`` to a ``[[ref_to_uninit]]`` parameter) never count -- the paper +reserves callee-initialization for ``now_init()`` (§6.2). Class-typed +whole-object assignment never credits either: it is a member ``operator=`` +call on uninitialized storage, rejected as above. The credit is purely +parse-order, with no dominance or flow analysis: a store under a condition +credits everything after it, so a binding on the untaken path is a missed +diagnostic (see `Limitations`_). + Constructors ------------ @@ -550,6 +571,9 @@ false positive. initialized by the most-derived class). - A read of a tracked member inside another member's default initializer is not detected. +- Whole-entity store credit is parse-order only: a store under a condition + (or inside a lambda body) credits every later use in parse order, so a + read or binding on a path that skips the store is a missed diagnostic. - In a template, a violation in non-dependent code is diagnosed at definition time and may be repeated at instantiation. diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 92fd60aff251f..2468055306872 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -20,6 +20,7 @@ #include "clang/Basic/Profiles.h" #include "clang/Basic/SourceLocation.h" #include "clang/Sema/SemaBase.h" +#include "llvm/ADT/DenseMap.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/SmallVector.h" #include "llvm/ADT/StringRef.h" @@ -360,9 +361,54 @@ class SemaProfiles : public SemaBase { /// std::init: the check pair a built-in ++/-- hosts -- the old-value load /// (read-through) and the store (subobject-write). Hosts the cluster from - /// Sema::CreateBuiltinUnaryOp's increment/decrement arm. + /// Sema::CreateBuiltinUnaryOp's increment/decrement arm. Records the store + /// credit last, after both pre-store checks. void checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc); + /// std::init: record parse-order whole-entity store credit for \p LHS, the + /// left operand of a completed built-in assignment (called from the tail + /// of Sema::CheckAssignmentOperands) or the operand of a built-in ++/--. + /// Assigning a whole [[uninit]] local is its initialization (paper + /// §4.2/§4.5), and a store through the exact `*p` / `r` lvalue of a + /// [[ref_to_uninit]] local or parameter initializes the pointee (§4.3); a + /// store to a marked *pointer* itself reseats it and clears its pointee + /// credit. Element stores (p[i] = e) neither credit nor invalidate + /// (§5.4/§5.5 ban element-wise tracking), and escapes never credit (§6.2 + /// reserves callee-initialization for now_init()). Purely parse-order -- + /// no dominance or flow analysis -- so the credit errs only toward missed + /// diagnostics. Deliberately not gated on enforcement or + /// [[profiles::suppress]]: a suppressed store still initializes, and + /// failing to credit it would turn suppression into later false positives. + void recordInitProfileStore(const Expr *LHS); + + /// True if \p VD is a local [[uninit]] variable credited by a recorded + /// whole-entity store; the recognizers then classify it as initialized + /// (which also enables the paper's reverse-direction rule: a credited + /// entity requires an unmarked target). + bool hasWholeObjectStoreCredit(const ValueDecl *VD) const; + + /// True if \p VD is a [[ref_to_uninit]] local/parameter pointer or + /// reference credited by a recorded store through it; the storage behind + /// it then classifies as initialized (until a pointer is reseated -- + /// references cannot be reseated, so their credit is never cleared). + bool hasPointeeStoreCredit(const ValueDecl *VD) const; + + /// Store-credit bits for recordInitProfileStore. + enum InitStoreCreditFlags : unsigned { + /// The [[uninit]] entity itself was assigned (u = e, u @= e, ++u). + WholeStored = 1u << 0, + /// The storage behind the [[ref_to_uninit]] entity was written through + /// the exact *p / r lvalue. + PointeeStored = 1u << 1, + }; + + /// Parse-order store credit, keyed by the credited local/parameter (only + /// local-storage VarDecls carrying the relevant marker are ever inserted). + /// Never cleared across the translation unit: the keys are unique + /// declarations, and template instantiations build fresh declarations, so + /// pattern-time and instantiation-time state stay independent. + llvm::DenseMap InitStoreCredit; + /// std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes /// the exception object, which cannot carry [[ref_to_uninit]]; a no-op for /// a non-pointer exception object. Hosts the cluster from diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp index a1882aa655c7a..ebad61f733b9b 100644 --- a/clang/lib/Sema/SemaExpr.cpp +++ b/clang/lib/Sema/SemaExpr.cpp @@ -14553,6 +14553,15 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS, } } + // std::init: a completed built-in assignment is a store; record parse-order + // whole-entity store credit (paper §4.2/§4.5). Deliberately at the tail: a + // simple assignment's RHS lvalue-to-rvalue load is checked inside + // CheckSingleAssignmentConstraints above, so recording earlier would let + // `*p = *p;` credit itself and silently pass its own RHS read. Invalid + // assignments returned early and record nothing. + if (getLangOpts().Profiles) + Profiles().recordInitProfileStore(LHSExpr); + // C11 6.5.16p3: The type of an assignment expression is the type of the // left operand would have after lvalue conversion. // C11 6.3.2.1p2: ...this is called lvalue conversion. If the lvalue has diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 84ec7c64d6096..b0b3b7737c06c 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -871,12 +871,30 @@ enum class UninitStorage { Initialized, Uninitialized, Unknown }; // a scalar write through the marker is the pointee's initialization (paper // §4.5), and verifying class-type writes (construct_at) is a deferred slice, // so a store through the marker must be neither banned nor endorsed. +// SubscriptBase: the classification runs below an element access (p[i]), +// where pointee store credit must not apply: element-wise state is +// untrackable by design (paper §5.4/§5.5 ban random access through the +// marker), so only the whole-`*p` form is ever credited. Purely syntactic: +// p[0] is not credited even though it denotes the same storage as *p. +// +// Credit: when non-null, the recognizers consult the parse-order store +// credit recorded by SemaProfiles::recordInitProfileStore -- a credited +// entity classifies as Initialized. Null in the constexpr presets; the +// checking entry points attach it via withCredit. struct UninitAccessOpts { bool DropTopLevelUninit = false; bool TrustRefToUninit = false; + bool SubscriptBase = false; + const SemaProfiles *Credit = nullptr; UninitAccessOpts withoutTopLevelDrop() const { - return {false, TrustRefToUninit}; + return {false, TrustRefToUninit, SubscriptBase, Credit}; + } + UninitAccessOpts withSubscriptBase() const { + return {DropTopLevelUninit, TrustRefToUninit, true, Credit}; + } + UninitAccessOpts withCredit(const SemaProfiles *SP) const { + return {DropTopLevelUninit, TrustRefToUninit, SubscriptBase, SP}; } }; @@ -1025,6 +1043,17 @@ pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, } return UninitStorage::Initialized; } + // Parse-order store credit: after a whole-`*p` store, the marked + // pointer's pointee counts as initialized (paper §4.3: "p no longer + // refers to uninitialized memory") for further whole-`*p` accesses -- + // until the pointer is reseated, which clears the credit. The consult + // sits before the TrustRefToUninit branch (under the write preset the + // outcome merely changes Unknown to Initialized, both "not + // Uninitialized": no preset regression) and is skipped below an element + // access (SubscriptBase), preserving §5.4's random-access ban. + if (Opts.Credit && !Opts.SubscriptBase && + Opts.Credit->hasPointeeStoreCredit(VD)) + return UninitStorage::Initialized; return Opts.TrustRefToUninit ? UninitStorage::Unknown : UninitStorage::Uninitialized; } @@ -1083,11 +1112,16 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, // Under the top-level drop the [[uninit]] arm is skipped: a value access of // a directly named [[uninit]] object is the flow-based passes' territory, so // only a [[ref_to_uninit]] reference (or indirection, handled below) still - // counts. + // counts. Parse-order store credit clears both arms: a whole-entity store + // is the [[uninit]] entity's initialization (paper §4.2/§4.5), and a store + // through a marked reference initializes its referent (§4.3; references + // cannot be reseated, so that credit never lapses). auto DeclDenotesUninit = [&](const ValueDecl *VD) { - return (!Opts.DropTopLevelUninit && VD->hasAttr()) || + return (!Opts.DropTopLevelUninit && VD->hasAttr() && + !(Opts.Credit && Opts.Credit->hasWholeObjectStoreCredit(VD))) || (!Opts.TrustRefToUninit && VD->getType()->isReferenceType() && - VD->hasAttr()); + VD->hasAttr() && + !(Opts.Credit && Opts.Credit->hasPointeeStoreCredit(VD))); }; if (const auto *DRE = dyn_cast(E)) return DeclDenotesUninit(DRE->getDecl()) ? UninitStorage::Uninitialized @@ -1114,8 +1148,13 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, // it returns is uninitialized. if (const auto *CE = dyn_cast(E)) return classifyRefToUninitCallee(CE, Opts); + // An element access classifies like its base, but pointee store credit + // must not apply below it (SubscriptBase): `*p = 5;` never legalizes + // `p[1]` -- the pointee may be an array with only element 0 written, and + // element-wise state is untrackable by design (paper §5.4/§5.5). if (const auto *ASE = dyn_cast(E)) - return pointerRefersToUninitStorage(Ctx, ASE->getBase(), Opts); + return pointerRefersToUninitStorage(Ctx, ASE->getBase(), + Opts.withSubscriptBase()); // *p, where p points to uninitialized storage. if (const auto *UO = dyn_cast(E)) @@ -1132,15 +1171,17 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, } // Dispatches a binding source to the pointer or glvalue recognizer. -static UninitStorage classifyUninitSource(ASTContext &Ctx, const Expr *E, - bool IsReference) { - return IsReference ? glvalueDenotesUninitStorage(Ctx, E) - : pointerRefersToUninitStorage(Ctx, E); +static UninitStorage +classifyUninitSource(ASTContext &Ctx, const Expr *E, bool IsReference, + UninitAccessOpts Opts = UninitBindAccess) { + return IsReference ? glvalueDenotesUninitStorage(Ctx, E, Opts) + : pointerRefersToUninitStorage(Ctx, E, Opts); } bool SemaProfiles::refersToUninitializedMemory(const Expr *E, bool IsReference) const { - return classifyUninitSource(getASTContext(), E, IsReference) == + return classifyUninitSource(getASTContext(), E, IsReference, + UninitBindAccess.withCredit(this)) == UninitStorage::Uninitialized; } @@ -1166,8 +1207,8 @@ void SemaProfiles::checkInitProfileRefToUninit(SourceLocation Loc, static constexpr StringRef Rule = "ref_to_uninit"; if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) return; - UninitStorage SrcState = - classifyUninitSource(getASTContext(), Src, IsReference); + UninitStorage SrcState = classifyUninitSource( + getASTContext(), Src, IsReference, UninitBindAccess.withCredit(this)); unsigned IsRef = IsReference ? 1 : 0; // A marked target is a violation only against an affirmatively Initialized // source: an Unknown one (pointer arithmetic, an integer-to-pointer cast, a @@ -1218,11 +1259,17 @@ void SemaProfiles::checkInitProfileRefCapture(SourceLocation Loc, return; // Mirrors the glvalue recognizer's named-entity arm: the captured variable // denotes uninitialized storage if it is [[uninit]], or if it is a - // [[ref_to_uninit]] reference (the capture binds to its referent). A copy - // capture is not this check's: it reads the variable in the enclosing + // [[ref_to_uninit]] reference (the capture binds to its referent) -- in + // both cases unless parse-order store credit says it has been initialized + // (u = 5; then a by-ref capture of u is accepted, symmetric with &u). A + // copy capture is not this check's: it reads the variable in the enclosing // function's CFG, which is the flow-based uninit_read pass's territory. - if (!Var->hasAttr() && - !(Var->getType()->isReferenceType() && Var->hasAttr())) + bool UninitNoCredit = + Var->hasAttr() && !hasWholeObjectStoreCredit(Var); + bool RefNoCredit = Var->getType()->isReferenceType() && + Var->hasAttr() && + !hasPointeeStoreCredit(Var); + if (!UninitNoCredit && !RefNoCredit) return; // The only Expr-less deferral here: an instantiation-dependent captured // type defers to instantiation, where TreeTransform's unconditional lambda @@ -1335,7 +1382,8 @@ void SemaProfiles::checkInitProfileReadThrough(SourceLocation Loc, return; if (!shouldEmitProfileViolation("std::init", "uninit_read", Loc)) return; - if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, UninitReadAccess) != + if (glvalueDenotesUninitStorage(getASTContext(), Glvalue, + UninitReadAccess.withCredit(this)) != UninitStorage::Uninitialized) return; Diag(Loc, diag::err_init_uninit_read_through) @@ -1360,7 +1408,8 @@ void SemaProfiles::checkInitProfileSubobjectWrite(SourceLocation Loc, return; if (!shouldEmitProfileViolation("std::init", "uninit_write", Loc)) return; - if (glvalueDenotesUninitStorage(getASTContext(), LHS, UninitWriteAccess) != + if (glvalueDenotesUninitStorage(getASTContext(), LHS, + UninitWriteAccess.withCredit(this)) != UninitStorage::Uninitialized) return; Diag(Loc, diag::err_init_uninit_subobject_write) @@ -1409,6 +1458,85 @@ void SemaProfiles::checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc) { checkInitProfileReadThrough(Operand->getExprLoc(), Operand, Operand->getType()); checkInitProfileSubobjectWrite(OpLoc, Operand); + // Record last: the pre-store checks above must see pre-store state (there + // is no RHS). ++u credits the whole entity; ++p reseats a marked pointer. + recordInitProfileStore(Operand); +} + +void SemaProfiles::recordInitProfileStore(const Expr *LHS) { + if (!getLangOpts().Profiles || !LHS) + return; + // A store in an unevaluated or discarded-statement context never executes + // (mirroring shouldEmitProfileViolation's context checks), so it earns no + // credit. There is deliberately no enforcement or [[profiles::suppress]] + // gate -- a suppressed store still initializes; failing to credit it would + // turn suppression into later false positives -- and no in-template gate: + // non-dependent code in a template is checked at definition time and must + // find pattern-time credit (instantiations rebuild their DeclRefExprs + // against fresh decls, so they re-record independently). + if (SemaRef.isUnevaluatedContext()) + return; + if (SemaRef.currentEvaluationContext().isDiscardedStatementContext()) + return; + const Expr *E = LHS->IgnoreParenImpCasts(); + // *p = e: a store through the exact whole-`*p` lvalue of a marked + // local/parameter pointer is the pointee's initialization (paper + // §4.3/§4.5: for a built-in type, a write is its initialization). + // Class-typed pointees never get here: `*sp = S{...}` resolves to a member + // operator= (already rejected as a call on uninitialized storage), so + // PointeeStored is only ever set for built-in-typed pointee stores. + // Subscript stores (p[i] = e) are deliberately neither credited nor + // invalidating: the paper bans element-wise tracking (§5.4/§5.5). + if (const auto *UO = dyn_cast(E); + UO && UO->getOpcode() == UO_Deref) { + if (const auto *VD = + dyn_cast_or_null(getDirectlyNamedDecl(UO->getSubExpr())); + VD && VD->hasLocalStorage() && VD->getType()->isPointerType() && + VD->hasAttr()) + InitStoreCredit[VD] |= PointeeStored; + return; + } + // Only a directly named local-storage variable can be credited: a + // MemberExpr's FieldDecl fails the VarDecl cast (fields are the CFG + // passes' territory) and statics fail hasLocalStorage. + const auto *VD = dyn_cast_or_null(getDirectlyNamedDecl(E)); + if (!VD || !VD->hasLocalStorage()) + return; + // u = e (also u @= e and ++u, via the inc-dec host): assigning the whole + // [[uninit]] entity is its initialization (paper §4.2/§4.5). + if (VD->hasAttr()) { + InitStoreCredit[VD] |= WholeStored; + return; + } + if (!VD->hasAttr()) + return; + if (VD->getType()->isReferenceType()) { + // r = e stores through the marked reference to its referent; a reference + // cannot be reseated, so the credit is never cleared. + InitStoreCredit[VD] |= PointeeStored; + } else if (VD->getType()->isPointerType()) { + // p = q / p += n / ++p reseats the marked pointer: any pointee credit no + // longer describes the new pointee. The clear lives here in the tail + // funnel -- not in checkInitProfilePointerAssignment, which runs only + // for plain assignment and would miss compound reseats. + InitStoreCredit[VD] &= ~unsigned(PointeeStored); + } +} + +bool SemaProfiles::hasWholeObjectStoreCredit(const ValueDecl *VD) const { + const auto *Var = dyn_cast(VD); + if (!Var) + return false; + auto It = InitStoreCredit.find(Var); + return It != InitStoreCredit.end() && (It->second & WholeStored); +} + +bool SemaProfiles::hasPointeeStoreCredit(const ValueDecl *VD) const { + const auto *Var = dyn_cast(VD); + if (!Var) + return false; + auto It = InitStoreCredit.find(Var); + return It != InitStoreCredit.end() && (It->second & PointeeStored); } void SemaProfiles::checkInitProfileThrowOperand(const Expr *Operand) { diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index d89892dbbc42e..24c980a190921 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -470,8 +470,10 @@ template void template_init_capture_bad(); // expected-note {{in instantiat void test_ref_captures() { int x [[uninit]]; int ok = 0; - auto c1 = [&x] { x = 1; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} - auto c2 = [&] { x = 1; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + // The bodies must not assign x: a body store would credit it in parse + // order and silence the capture check (see test_ref_capture_body_store). + auto c1 = [&x] { (void)x; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c2 = [&] { (void)x; }; // expected-error {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} auto c3 = [&ok] { ok = 1; }; // OK: initialized int *rtu [[ref_to_uninit]] = &g_uninit; auto c4 = [&rtu] { (void)rtu; }; // OK: the pointer object itself is initialized @@ -479,20 +481,40 @@ void test_ref_captures() { auto c5 = [&ur] { (void)ur; }; // expected-error {{capturing 'ur' by reference binds a reference to uninitialized memory under profile 'std::init'}} // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "ref_to_uninit")]] { - auto c6 = [&x] { x = 2; }; // OK: suppressed + auto c6 = [&x] { (void)x; }; // OK: suppressed (void)c6; } (void)c1; (void)c2; (void)c3; (void)c4; (void)c5; } +// Parse-order store credit reaches the capture check both ways -- accepted +// leniencies of the scope-less credit map (false negatives only): +void test_ref_capture_after_store() { + int u [[uninit]]; + u = 5; + auto c = [&u] { (void)u; }; // OK: the store initialized u (symmetric + // with binding &u after the store) + (void)c; +} + +void test_ref_capture_body_store() { + int u [[uninit]]; + auto L = [&] { u = 5; }; // OK: the body's own store credits u at parse + // order, silencing this capture check -- the + // deliberate leniency (no FunctionScopeInfo + // scoping), pinned here + int *q = &u; // OK: credited by the body store above + (void)L; (void)q; +} + // A by-reference capture of a variable with a non-dependent type fires at // definition time, and again when TreeTransform's unconditional lambda // rebuild re-processes the capture at instantiation -- the accepted -// repetition. +// repetition. (The body must not assign x, as in test_ref_captures.) template void template_ref_capture_bad() { int x [[uninit]]; - auto c = [&x] { x = 1; }; // expected-error 2 {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} + auto c = [&x] { (void)x; }; // expected-error 2 {{capturing 'x' by reference binds a reference to uninitialized memory under profile 'std::init'}} (void)c; } template void template_ref_capture_bad(); // expected-note {{in instantiation of function template specialization 'template_ref_capture_bad' requested here}} @@ -991,9 +1013,11 @@ void test_read_negatives(int *p [[ref_to_uninit]], int &r [[ref_to_uninit]], (void)r; // OK: discarded value (void)*p; // OK: discarded value int *ap [[ref_to_uninit]] = &*p; // OK: address-of is not a read - *p = 5; // OK: write, not a read - ptr->m = 5; // OK: write, not a read int &r2 [[ref_to_uninit]] = *p; // OK: reference binding, not a read + *p = 5; // OK: write, not a read (and it credits + // p's pointee, so it stays after the + // marked bindings above) + ptr->m = 5; // OK: write, not a read int *q [[ref_to_uninit]] = base; // OK: reads the pointer value, not through it (void)ap; (void)q; (void)r2; } @@ -1228,18 +1252,26 @@ void template_global_read_never_instantiated() { // [[ref_to_uninit]] pointer or reference is diagnosed; the shift forms load // through their LHS promotion instead and must fire exactly once. Reading an // unmarked pointer's pointee is trusted, and ++ on the marked pointer itself -// reads the (initialized) pointer object, not through it. +// reads the (initialized) pointer object, not through it. Each form gets a +// fresh marker: a compound form both reads (the error) and stores, and the +// store credits the pointee for everything after it in parse order (see +// test_pointee_store_credit) -- except through an element access, which +// never sees the credit (p[i] below, after *p's store; paper §5.4). void test_compound_read_through(int *p [[ref_to_uninit]], int *q, int &r [[ref_to_uninit]], - Inner *ptr [[ref_to_uninit]], int i) { + Inner *ptr [[ref_to_uninit]], int i, + int *p2 [[ref_to_uninit]], + int *p3 [[ref_to_uninit]], + int &r2 [[ref_to_uninit]], + int *p4 [[ref_to_uninit]]) { *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} p[i] -= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} r *= 2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} ptr->m |= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} - ++*p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} - (*p)--; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} - ++r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} - *p <<= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ++*p2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (*p3)--; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + ++r2; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p4 <<= 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} *q += 1; // OK: an unmarked pointer is trusted initialized ++p; // OK: reads the pointer object itself, not through it } @@ -1258,6 +1290,124 @@ void template_compound_read_bad(int *p [[ref_to_uninit]]) { } template void template_compound_read_bad(int *); // expected-note {{in instantiation of function template specialization 'template_compound_read_bad' requested here}} +// Parse-order pointee store credit (paper §4.3/§4.5): a whole-`*p` store +// through a [[ref_to_uninit]] pointer is the pointee's initialization, so +// whole-`*p` accesses after it (in parse order) are legal -- and the paper's +// reverse direction applies: the credited pointer now refers to initialized +// memory and REQUIRES an unmarked target. Element accesses never see the +// credit in either direction (§5.4's random-access ban), and reseating the +// pointer clears it. +void test_pointee_store_credit(int *p [[ref_to_uninit]]) { + *p = 5; // OK: the write initializes the pointee (and credits it) + *p = 7; // OK: further whole-entity stores stay legal + int x = *p; // OK: credited (rejected before the store) + int *r2 [[ref_to_uninit]] = p; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)x; (void)r2; +} + +void test_pointee_read_before_store(int *p [[ref_to_uninit]]) { + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; + (void)x; +} + +// The credit is recorded at the tail of the assignment, after the RHS is +// checked: a self-assignment's RHS read must not be silenced by its own +// store (the key recording-order regression test). +void test_pointee_no_self_credit(int *p [[ref_to_uninit]]) { + *p = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} + +// Element stores neither credit nor invalidate (§5.4/§5.5: element-wise +// state is untrackable by design)... +void test_subscript_store_no_credit(int *p [[ref_to_uninit]]) { + p[0] = 1; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +// ...and element reads never see pointee credit: `*p = 5;` must not legalize +// p[1] (the pointee may be an array with only element 0 written). The model +// is purely syntactic, so even p[0] -- the same storage as *p -- stays an +// error: only the whole-`*p` form is credited. +void test_subscript_read_not_credited(int *p [[ref_to_uninit]], int i) { + *p = 5; + int x = p[i]; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + int y = p[0]; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; (void)y; +} + +// Reseating the pointer -- plain assignment, compound arithmetic, or ++ -- +// clears its pointee credit: the credit described the old pointee. +void test_reseat_clears_credit(int *p [[ref_to_uninit]], + int *q [[ref_to_uninit]], int n) { + *p = 5; + p = q; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; + p += n; + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; + p++; + int z = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; (void)y; (void)z; +} + +// A store through a marked *reference* credits its referent; a reference +// cannot be reseated, so the credit is never cleared. +void test_marked_ref_store_credit(int &r [[ref_to_uninit]]) { + r = 5; + int x = r; // OK: the store initialized the referent + (void)x; +} + +void test_marked_ref_read_before_store(int &r [[ref_to_uninit]]) { + int x = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + r = 5; + (void)x; +} + +// Store credit is recorded at pattern-parse time too: non-dependent +// store-then-read inside a template is checked at definition time (the +// documented phase-7 trade-off) and must find the pattern-time credit; +// instantiations rebuild every DeclRefExpr against fresh declarations and +// re-record independently. +template +void template_store_then_read(int *p [[ref_to_uninit]]) { + *p = 5; + int x = *p; // OK at definition time and at instantiation + (void)x; +} +template void template_store_then_read(int *); + +// A store in a discarded if-constexpr branch is not instantiated, so the +// rebuilt read finds no credit at that instantiation -- while the pattern's +// store did credit the definition-time check (a dependent condition +// discards nothing at parse). +template +void template_discarded_store(int *p [[ref_to_uninit]]) { + if constexpr (B) + *p = 5; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} +template void template_discarded_store(int *); // OK: store instantiated +template void template_discarded_store(int *); // expected-note {{in instantiation of function template specialization 'template_discarded_store' requested here}} + +// Known residual gap (documented definition-time-purity trade-off): a store +// with a *type-dependent RHS* routes through the overloaded-operator path at +// pattern time and never reaches the built-in assignment funnel, so it earns +// no pattern-time credit and the following non-dependent read +// false-positives at definition time. The instantiation is clean (its +// rebuilt store re-records first) -- exactly one error total. +template +void template_dependent_rhs_store(int *p [[ref_to_uninit]], T t) { + *p = t; + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} +template void template_dependent_rhs_store(int *, int); + // std::init / ref_to_uninit (paper §5): a pointer/reference member given a // *written* constructor member-initializer is checked with the enclosing // constructor as the Decl, so a class-template pattern defers and fires once at diff --git a/clang/test/SemaCXX/safety-profile-init-write.cpp b/clang/test/SemaCXX/safety-profile-init-write.cpp index 5c1bbc0a68e04..0c25d6c93a868 100644 --- a/clang/test/SemaCXX/safety-profile-init-write.cpp +++ b/clang/test/SemaCXX/safety-profile-init-write.cpp @@ -119,19 +119,20 @@ void test_compound_and_incdec_stores() { // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} } -// Writes through a [[ref_to_uninit]] pointer or reference are the deferred -// construct_at slice: for a built-in type the write is the pointee's -// initialization (paper §4.5), so they are neither banned nor endorsed. A -// compound assignment through the marker still *reads* the old value, which -// is a genuine uninit_read violation (full coverage in -// safety-profile-init-ref-to-uninit.cpp). +// Writes through a [[ref_to_uninit]] pointer or reference: for a built-in +// type the write is the pointee's initialization (paper §4.5), so they are +// legal -- and a whole-`*p` store credits the pointee as initialized in +// parse order, so the compound assignment below may read the value it wrote. +// An element store (p[3]) neither credits nor invalidates (§5.4/§5.5). A +// compound assignment through a still-uncredited marker reads uninitialized +// memory (full coverage in safety-profile-init-ref-to-uninit.cpp). [[ref_to_uninit]] int &get_uninit_ref(); void test_write_through_ref_to_uninit(int *p [[ref_to_uninit]], int &r [[ref_to_uninit]], Pair *ptr [[ref_to_uninit]]) { - *p = 5; // OK - p[3] = 0; // OK - *p += 1; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + *p = 5; // OK (and credits p's pointee) + p[3] = 0; // OK (no credit, no invalidation) + *p += 1; // OK: the whole-*p store above credited the pointee r = 5; // OK ptr->x = 5; // OK get_uninit_ref() = 5; // OK @@ -151,6 +152,9 @@ T *construct_at(T *p [[ref_to_uninit]], Args &&...args); void test_construct_at_pattern() { Pair s [[uninit]]; construct_at(&s, 1, 2); // OK: the marked parameter accepts &s + // The address escape earns no store credit -- paper §6.2 reserves + // callee-initialization for now_init(); only whole-entity stores credit + // (stores-only policy) -- so the subobject write below stays an error. s.x = 1; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} } @@ -257,3 +261,98 @@ void template_two_rules_never_instantiated() { s.p = &g_uninit_int; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} \ // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } + +// Parse-order whole-entity store credit (paper §4.2/§4.5): assigning the +// whole [[uninit]] entity is its initialization, so bindings after the store +// (in parse order) treat it as initialized -- and the paper's reverse +// direction applies: the credited entity now REQUIRES an unmarked target +// (§4.2's `p4 = &x` error). Purely parse-order, no flow analysis: a store +// under a condition credits everything after it (§1.2 "consider all branches +// executed" -- the untaken path is a missed diagnostic, never a false +// positive). +void take_int_ptr(int *q); +void test_whole_store_credit() { + int u [[uninit]]; + u = 5; + int *q = &u; // OK: u is initialized (rejected before the store) + take_int_ptr(&u); // OK + int &br = u; // OK + int *r [[ref_to_uninit]] = &u; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)q; (void)br; (void)r; +} + +void test_binding_before_store() { + int u [[uninit]]; + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + u = 5; + (void)q; +} + +void test_conditional_store_credit(bool c) { + int u [[uninit]]; + if (c) + u = 5; + int *q = &u; // OK: parse-order credit (§1.2); the untaken path is an + // accepted missed diagnostic + (void)q; +} + +// Compound assignment and ++/-- store, so they credit the whole entity -- +// while their own old-value read keeps the flow-based read-before-init +// error (recorded after the pre-store checks: no self-crediting). +void test_compound_store_credit() { + int u [[uninit]]; // expected-note {{variable 'u' is declared here}} + u += 1; // expected-error {{variable 'u' is read before initialization under profile 'std::init'}} + int *q = &u; // OK: the compound store credited u + int v [[uninit]]; // expected-note {{variable 'v' is declared here}} + ++v; // expected-error {{variable 'v' is read before initialization under profile 'std::init'}} + int *qv = &v; // OK + int w [[uninit]]; // expected-note {{variable 'w' is declared here}} + w = w + 1; // expected-error {{variable 'w' is read before initialization under profile 'std::init'}} + (void)q; (void)qv; +} + +// Class-typed whole-object assignment never credits: it resolves to a member +// operator= -- already rejected as a call on uninitialized storage -- and +// never reaches the built-in assignment funnel (crediting an erroneous +// statement would misstate the object's state). The class remedy remains +// construct_at through [[ref_to_uninit]] (paper §4.5; unmodeled slice). +void test_class_store_never_credits() { + Pair s [[uninit]]; + s = Pair{1, 2}; // expected-error {{calling member function 'operator=' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} + s.x = 3; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + int y = s.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// A store in an unevaluated or discarded context never executes, so it earns +// no credit. +void test_no_credit_contexts() { + int u [[uninit]]; + (void)sizeof(u = 5); // expected-warning {{expression with side effects has no effect in an unevaluated context}} \ + // no-profiles-warning {{expression with side effects has no effect in an unevaluated context}} + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int v [[uninit]]; + using TT = decltype((v = 5)); // expected-warning {{expression with side effects has no effect in an unevaluated context}} \ + // no-profiles-warning {{expression with side effects has no effect in an unevaluated context}} + int *qv = &v; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int w [[uninit]]; + if constexpr (false) { w = 5; } + int *qw = &w; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; (void)qv; (void)qw; +} + +// The credit keys on the unique VarDecl: a same-named sibling-scope local is +// a distinct declaration, so credit does not leak between them. +void test_sibling_scope_credit(bool c) { + if (c) { + int u [[uninit]]; + u = 5; + int *q = &u; // OK: this u is credited + (void)q; + } else { + int u [[uninit]]; + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; + } +} From 649744154146e3bbc1b0eddaf7ff286a6fd6244b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 13:07:49 -0400 Subject: [PATCH 250/289] Exclude parameters from the std::init null-initializer classification --- clang/lib/Sema/SemaProfiles.cpp | 8 +++-- .../safety-profile-init-ref-to-uninit.cpp | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index b0b3b7737c06c..d15261f0287b4 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1028,10 +1028,12 @@ pointerRefersToUninitStorage(ASTContext &Ctx, const Expr *E, // accepted missed diagnostic (parse-order leniency). Deliberately // excluded: globals/extern (an extern pointer may be initialized // elsewhere, and keeping them Initialized preserves the - // marked-direction diagnostics) and null-NSDMI fields. A *marked* decl - // keeps its marker classification below (respect the explicit marker). + // marked-direction diagnostics), null-NSDMI fields, and parameters -- + // a ParmVarDecl's getInit() is its *default argument*, which is not + // the parameter's value on most calls. A *marked* decl keeps its + // marker classification below (respect the explicit marker). if (const auto *Var = dyn_cast(VD); - Var && Var->hasLocalStorage()) { + Var && Var->hasLocalStorage() && !isa(Var)) { if (const Expr *Init = Var->getInit()) { const Expr *InnerInit = Init->IgnoreParenImpCasts(); const auto *ILE = dyn_cast(InnerInit); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 24c980a190921..c4f8dba97a8c8 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -280,6 +280,37 @@ void test_null_global() { take_uninit_ptr(g_null_ptr); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} } +// A static local is excluded for the same reason as a global (not +// function-local state; hasLocalStorage is the gate). +void test_null_static_local() { + static int *sp = nullptr; + take_uninit_ptr(sp); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A *parameter* is excluded too: a ParmVarDecl's initializer is its default +// argument, which is not the parameter's value on most calls -- a +// defaulted-null parameter may be passed any caller pointer, so it must keep +// drawing the marked-target diagnostic. +void null_default_param(int *cp = nullptr) { + take_uninit_ptr(cp); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// At the *call site* an omitted defaulted argument is the null literal +// itself: fine for a marked parameter. +void null_default_marked(int *p [[ref_to_uninit]] = nullptr); +void test_null_default_marked_param() { + null_default_marked(); // OK: the null default argument + null_default_marked(nullptr); // OK +} + +// A *marked* pointer initialized to null keeps its marker classification as +// a source: the explicit marker is respected over the null initializer. +void test_null_init_marked_decl() { + int *m [[ref_to_uninit]] = nullptr; + int *m2 = m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)m2; +} + // A defaulted pointer or reference argument is checked against the parameter's // [[ref_to_uninit]] marking at the call site, like an explicit argument. The // declarations themselves stay clean; the diagnostic fires at the call. From ac849125eba6d979debc3d1c4713a807d6a7f9e1 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 13:10:09 -0400 Subject: [PATCH 251/289] Pin std::init field-marker, store-credit, and null-source boundaries --- clang/docs/ProfilesFramework.rst | 5 ++- .../safety-profile-init-field-marker.cpp | 30 +++++++++++++ .../safety-profile-init-ref-to-uninit.cpp | 45 +++++++++++++++++++ .../SemaCXX/safety-profile-init-write.cpp | 22 +++++++++ 4 files changed, 100 insertions(+), 2 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 27f45168a82b9..c4f35619c0b78 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -448,9 +448,10 @@ Positions that cannot carry the marker -- a variadic argument, a parameter of a function called through a function pointer, the implicit object parameter -- are checked as unmarked targets; suppress at the call site if the flow is intended. A null pointer source -- ``nullptr``, ``0``, ``{}``, or a local -pointer initialized to null -- refers to no object, so it is accepted for +variable initialized to null -- refers to no object, so it is accepted for marked and unmarked targets alike (§4.3, §8: the marker means "zero or more -uninitialized objects"). A source whose form the recognizer cannot classify +uninitialized objects"); a *parameter* with a null default argument is not a +null source (callers may pass any pointer). A source whose form the recognizer cannot classify (pointer arithmetic, an integer-to-pointer cast) is likewise accepted for either target. diff --git a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp index cd5afd7930247..2950303f009ab 100644 --- a/clang/test/SemaCXX/safety-profile-init-field-marker.cpp +++ b/clang/test/SemaCXX/safety-profile-init-field-marker.cpp @@ -221,6 +221,36 @@ template struct DependentVacuity; // expected-note {{in instantiation // expected-error@#dependent-vacuity-member {{member 'm' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} // expected-note@#dependent-vacuity-member {{default-initialization of 'RunsCtor' runs a constructor}} +// A marked field of an *anonymous* struct is checked when the anonymous +// record itself is finalized: the enclosing class's walk sees only the +// unnamed anonymous-record member, which cannot carry the attribute. +struct WithAnonStruct { + struct { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + }; +}; + +// An anonymous struct inside a union is itself not a union, so its +// non-vacuous marked member draws the member diagnostic (exactly one): its +// members are variant members, where the marker is just as unsatisfiable. +union UnionWithAnonStruct { + struct { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + }; + int tag; +}; + +// A local class is finalized like any other. +void local_class_field_marker() { + struct Local { + RunsCtor s [[uninit]]; // expected-error {{member 's' cannot be marked '[[uninit]]' under profile 'std::init'; default-initialization of its type 'RunsCtor' does not leave it uninitialized}} \ + // expected-note {{default-initialization of 'RunsCtor' runs a constructor}} + }; + (void)sizeof(Local); +} + // Suppression: rule-targeted on the field, and whole-profile on the class. struct SuppressedFieldMarker { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index c4f8dba97a8c8..556c13203230a 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1384,6 +1384,43 @@ void test_reseat_clears_credit(int *p [[ref_to_uninit]], (void)x; (void)y; (void)z; } +// The credit map keys on local VarDecls, so a [[ref_to_uninit]] *member* +// pointer is never credited: a read through it keeps failing even after a +// store through the exact same lvalue (no per-object tracking). +struct WithMarkedPtrField { + int *p [[ref_to_uninit]] = &g_uninit; +}; +void test_member_pointer_never_credited(WithMarkedPtrField w) { + *w.p = 5; + int x = *w.p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +// A member store through a marked class-typed pointer is a subobject store: +// trusted as a write (paper §4.5) but never crediting, so the member read +// after it still fails. +void test_member_store_never_credits(Inner *ptr [[ref_to_uninit]]) { + ptr->m = 5; + int y = ptr->m; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)y; +} + +// The reverse direction applies through the assignment funnel too: a +// credited marked pointer refers to initialized memory, so assigning it to +// another marked pointer is the requires-uninit error -- while an unmarked +// target now accepts it (paper §4.3: "p no longer refers to uninitialized +// memory"). +void test_assign_credited_to_marked(int *p [[ref_to_uninit]], + int *q [[ref_to_uninit]]) { + *p = 5; + q = p; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} +void test_credited_to_unmarked(int *p [[ref_to_uninit]]) { + *p = 5; + int *s = p; // OK: the credited pointee is initialized + (void)s; +} + // A store through a marked *reference* credits its referent; a reference // cannot be reseated, so the credit is never cleared. void test_marked_ref_store_credit(int &r [[ref_to_uninit]]) { @@ -1392,6 +1429,14 @@ void test_marked_ref_store_credit(int &r [[ref_to_uninit]]) { (void)x; } +// ...and the reference gets the reverse direction too: after the store its +// referent is initialized, so a marked reference can no longer bind to it. +void test_marked_ref_reverse_direction(int &r [[ref_to_uninit]]) { + r = 5; + int &r3 [[ref_to_uninit]] = r; // expected-error {{reference marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)r3; +} + void test_marked_ref_read_before_store(int &r [[ref_to_uninit]]) { int x = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} r = 5; diff --git a/clang/test/SemaCXX/safety-profile-init-write.cpp b/clang/test/SemaCXX/safety-profile-init-write.cpp index 0c25d6c93a868..4743d87b46547 100644 --- a/clang/test/SemaCXX/safety-profile-init-write.cpp +++ b/clang/test/SemaCXX/safety-profile-init-write.cpp @@ -342,6 +342,28 @@ void test_no_credit_contexts() { (void)q; (void)qv; (void)qw; } +// A suppressed store still initializes: the credit is recorded regardless +// of [[profiles::suppress]], so suppression cannot manufacture later false +// positives. +void test_suppressed_store_credits() { + int u [[uninit]]; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] { u = 5; } + int *q = &u; // OK: the suppressed store still credited u + (void)q; +} + +// A nested assignment credits both targets: the inner assignment completes +// (and records) before the outer one. +void test_nested_assignment_credit() { + int u [[uninit]]; + int v [[uninit]]; + u = (v = 5); + int *qu = &u; // OK + int *qv = &v; // OK + (void)qu; (void)qv; +} + // The credit keys on the unique VarDecl: a same-named sibling-scope local is // a distinct declaration, so credit does not leak between them. void test_sibling_scope_credit(bool c) { From 99e5ab8a50863d35fe33d8485af71153fa8c4ea8 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 14:29:17 -0400 Subject: [PATCH 252/289] Check ref_to_uninit on aggregate array-element bindings --- clang/docs/ProfilesFramework.rst | 3 +- clang/lib/Sema/SemaInit.cpp | 37 +++++++--- .../safety-profile-init-ref-to-uninit.cpp | 68 +++++++++++++++++++ 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index c4f35619c0b78..12e7aed3bf79a 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -445,7 +445,8 @@ on an object recognized as uninitialized storage is rejected, and so is copying a class object out of one. For the copy, the escape is the paper's own (§7.2): declare the copy constructor's parameter ``[[ref_to_uninit]]``. Positions that cannot carry the marker -- a variadic argument, a parameter of -a function called through a function pointer, the implicit object parameter +a function called through a function pointer, the implicit object parameter, +a pointer element of an array in aggregate initialization -- are checked as unmarked targets; suppress at the call site if the flow is intended. A null pointer source -- ``nullptr``, ``0``, ``{}``, or a local variable initialized to null -- refers to no object, so it is accepted for diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp index 0f298d3a1eb2f..4dbae6dd27d37 100644 --- a/clang/lib/Sema/SemaInit.cpp +++ b/clang/lib/Sema/SemaInit.cpp @@ -1596,16 +1596,24 @@ void InitListChecker::CheckSubElementType(const InitializedEntity &Entity, // std::init / ref_to_uninit (paper §5): in C++ a pointer field // initialized by an enclosing aggregate's init list is copy- // initialized here (a scalar member never reaches CheckScalarType). - // Scoped to an aggregate subobject (EK_Member) so the enclosing - // variable/argument/return is left to its own site, which already - // handles a braced source via the recognizer. No Decl is passed: the - // init list can appear in a template independently of whether the - // aggregate is one, so checkInitProfileRefToUninit defers on an - // instantiation-dependent source and suppression comes from the - // parse-time stack. (A reference field is routed to - // CheckReferenceType above.) + // Scoped to an aggregate subobject (a field, or an array / vector / + // complex element) so the enclosing variable/argument/return is left + // to its own site, which already handles a braced source via the + // recognizer. An element position has no declaration to carry the + // marker (Entity.getDecl() is null for the element kinds), so it is + // checked as an unmarked target -- like a variadic argument or a + // function-pointer parameter -- and the rules of paper §4.3 apply + // per pointer element. No Decl is passed: the init list can appear + // in a template independently of whether the aggregate is one, so + // checkInitProfileRefToUninit defers on an instantiation-dependent + // source and suppression comes from the parse-time stack. (A + // reference field is routed to CheckReferenceType above; an array + // of references is ill-formed.) if (!Result.isInvalid() && - Entity.getKind() == InitializedEntity::EK_Member) + (Entity.getKind() == InitializedEntity::EK_Member || + Entity.getKind() == InitializedEntity::EK_ArrayElement || + Entity.getKind() == InitializedEntity::EK_VectorElement || + Entity.getKind() == InitializedEntity::EK_ComplexElement)) SemaRef.Profiles().checkInitProfileRefToUninitBinding( expr->getExprLoc(), Entity.getDecl(), ElemType, expr); @@ -6050,6 +6058,17 @@ static void TryOrBuildParenListInitialization( E->getExprLoc(), /*isDirectInit=*/false, E); if (!HandleInitializedEntity(SubEntity, SubKind, E)) return; + + // std::init / ref_to_uninit (paper §5): a pointer element initialized + // from a C++20 parenthesized aggregate list is an element binding, + // checked exactly like the braced-list hook in CheckSubElementType. An + // element cannot carry the marker (null target: unmarked, paper §4.3). + // Only in the build phase, so the verify pass does not double-diagnose; + // the trailing value-initialized filler below has no source expression + // and stays unchecked (value-initialization is null, a non-source). + if (!VerifyOnly) + S.Profiles().checkInitProfileRefToUninitBinding( + E->getExprLoc(), /*Target=*/nullptr, AT->getElementType(), E); } // ...and value-initialized for each k < i <= n; if (ArrayLength > Args.size() || Entity.isVariableLengthArrayNew()) { diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 556c13203230a..caa24093869d0 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1659,6 +1659,65 @@ void test_aggregate_nested() { (void)a; (void)b; } +// A pointer *element* of an array is a binding position with no declaration to +// carry the marker (like a variadic argument), so it is checked as an unmarked +// target and paper §4.3's rules apply per pointer element. The enclosing array +// variable's own site no-ops (an array is not a pointer/reference), so each +// violating element fires exactly once. +void test_array_element_pointer() { + int *a1[2] = {&g_uninit, &g_init}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *a2[2] = {&g_init, &g_init}; // OK + int *base [[ref_to_uninit]] = &g_uninit; + int *a3[1] = {base}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *a4[1] = {allocate(3)}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + // Null sources stay accepted: {} value-initializes the elements to null and + // nullptr is a null source, consistent with an unmarked target (paper §4.3). + int *a5[2] = {}; // OK + int *a6[2] = {nullptr, &g_init}; // OK + (void)a1; (void)a2; (void)base; (void)a3; (void)a4; (void)a5; (void)a6; +} + +// Parse-order store credit applies to element sources like any other binding: +// after the whole-entity store, &u refers to initialized memory. +void test_array_element_store_credit() { + int u [[uninit]]; + u = 5; + int *a[1] = {&u}; // OK: credited by the store above + (void)a; +} + +// Composition with nested aggregates: an array-of-struct element recurses back +// into the member gate (the field's own marker governs), and a struct's array +// member reaches the element gate. +struct WithPtrArray { int *a[2]; }; + +void test_array_element_nested() { + AggPtr arr1[1] = {{&g_uninit}}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + AggPtrMarked arr2[1] = {{&g_uninit}}; // OK: the field carries the marker + AggPtrMarked arr3[1] = {{&g_init}}; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + WithPtrArray w = {{&g_uninit, &g_init}}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)arr1; (void)arr2; (void)arr3; (void)w; +} + +// A designated array element (a C99 extension in C++) is the same element +// binding. +void test_array_element_designated() { + // expected-warning@+2 {{array designators are a C99 extension}} no-profiles-warning@+2 {{array designators are a C99 extension}} + // expected-error@+1 {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *d[3] = {[1] = &g_uninit}; + (void)d; +} + +// A dependent element source defers on the pattern and is rebuilt at +// instantiation, where the Decl-less check fires per specialization (like +// template_throw_bad). +template +void template_array_element_bad() { + T *a[1] = {(T *)&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)a; +} +template void template_array_element_bad(); // expected-note {{in instantiation of function template specialization 'template_array_element_bad' requested here}} + // An aggregate temporary built from an init list is checked the same way // wherever it appears -- as a call argument, a return value, or a new-expression // initializer. The enclosing pointer (the parameter, the return type, the @@ -1750,6 +1809,15 @@ void template_aggregate_paren_bad() { } template void template_aggregate_paren_bad(); // expected-note {{in instantiation of function template specialization 'template_aggregate_paren_bad' requested here}} +// A parenthesized aggregate's array elements are the same unmarked element +// bindings as the braced form; the hook runs only in the build phase, so the +// verify pass adds no second diagnostic. +void test_array_element_paren() { + int *pa[2](&g_uninit, &g_init); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *pb[2](&g_init, nullptr); // OK + (void)pa; (void)pb; +} + // A plain pointer *variable* with a braced initializer is checked once at its // own variable site (EK_Variable); the aggregate field hooks are scoped to a // member subobject, so this fires exactly once with no new duplicate. From 731c3f009e4621c289deff78be2bb607a2818117 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 14:29:18 -0400 Subject: [PATCH 253/289] Check ref_to_uninit on lambda and block returns --- clang/docs/ProfilesFramework.rst | 5 +- clang/lib/Sema/SemaStmt.cpp | 37 +++++- .../safety-profile-init-ref-to-uninit.cpp | 121 +++++++++++++++++- 3 files changed, 158 insertions(+), 5 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 12e7aed3bf79a..d487edd913fe1 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -439,7 +439,10 @@ one parse-order fact, whole-entity stores (below): The check applies wherever a pointer or reference is bound: variable, member, and aggregate initialization, assignment, call arguments (including defaulted -and variadic ones), ``return`` and ``throw`` statements, lambda captures, and +and variadic ones), ``return`` and ``throw`` statements (including returns +inside lambdas and blocks -- a lambda's marker is written on its call +operator, in the C++23 attribute position after the lambda-introducer: +``[] [[ref_to_uninit]] () -> int* { ... }``), lambda captures, and the implicit object argument of a member call -- so calling a member function on an object recognized as uninitialized storage is rejected, and so is copying a class object out of one. For the copy, the escape is the paper's diff --git a/clang/lib/Sema/SemaStmt.cpp b/clang/lib/Sema/SemaStmt.cpp index 51cbef34c4354..61ba9fa5cafaf 100644 --- a/clang/lib/Sema/SemaStmt.cpp +++ b/clang/lib/Sema/SemaStmt.cpp @@ -3709,6 +3709,35 @@ StmtResult Sema::ActOnCapScopeReturnStmt(SourceLocation ReturnLoc, } } + // std::init / ref_to_uninit (paper §5): a return inside a capturing scope + // binds the returned pointer or reference against that scope's *own* + // [[ref_to_uninit]] marking -- never the enclosing function's (paper §8.2: + // the function returning the value must itself be declared appropriately). + // A lambda carries the marker on its call operator (C++23 attribute + // position, after the lambda-introducer); a block cannot carry it, so block + // returns are unmarked targets (null Target), like variadic arguments. The + // owning declaration drives the suppression walk through lexical parents + // and, for a call operator in a template -- a generic lambda's pattern, or + // any lambda in a templated entity -- defers via isTemplated: the body is + // rebuilt under a fresh LambdaScopeInfo at instantiation + // (LambdaScopeForCallOperatorInstantiationRAII / TransformLambdaExpr), so + // this hook re-runs with the concrete declaration. Runs after the + // deduced/implicit return type is resolved above, so the wrapper sees the + // concrete FnRetType (a still-dependent one no-ops there, deferring to the + // same rebuild). A return in a captured region already errored above. + if (RetValExp && getLangOpts().Profiles) { + const ValueDecl *Target = nullptr; + const Decl *ScopeDecl = nullptr; + if (CurLambda) { + Target = CurLambda->CallOperator; + ScopeDecl = CurLambda->CallOperator; + } else if (const auto *BSI = dyn_cast(CurCap)) { + ScopeDecl = BSI->TheDecl; + } + Profiles().checkInitProfileRefToUninitBinding( + RetValExp->getExprLoc(), Target, FnRetType, RetValExp, ScopeDecl); + } + // Otherwise, verify that this result type matches the previous one. We are // pickier with blocks than for normal functions because we don't have GCC // compatibility to worry about here. @@ -4118,9 +4147,13 @@ StmtResult Sema::BuildReturnStmt(SourceLocation ReturnLoc, Expr *RetValExp, } } // std::init / ref_to_uninit (paper §5): the returned pointer or reference - // must match the function's [[ref_to_uninit]] return marking. + // must match the function's [[ref_to_uninit]] return marking. AllowLambda + // keeps a lambda call operator's body -- should it ever reach this + // non-capturing path -- checked against its own marker rather than the + // enclosing function's (returns inside lambdas normally take the capturing + // branch above, at parse and at instantiation alike). if (RetValExp && getLangOpts().Profiles) - if (const FunctionDecl *FD = getCurFunctionDecl()) + if (const FunctionDecl *FD = getCurFunctionDecl(/*AllowLambda=*/true)) Profiles().checkInitProfileRefToUninitBinding(RetValExp->getExprLoc(), FD, FnRetType, RetValExp); diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index caa24093869d0..ac58edf76086d 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1,5 +1,5 @@ -// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fcxx-exceptions -std=c++23 %s -// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -fcxx-exceptions -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -fblocks -fcxx-exceptions -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -fblocks -fcxx-exceptions -std=c++23 %s // std::init / ref_to_uninit (paper §5): a [[ref_to_uninit]] pointer or // reference must be bound to uninitialized memory, and an unmarked pointer or @@ -678,6 +678,123 @@ int *ret_suppressed() { [[profiles::suppress(std::init)]] return &g_uninit; // OK: suppressed } +// A return inside a lambda binds against the *lambda's own* call operator +// (paper §8.2: the function returning the value must itself be declared +// appropriately), whose [[ref_to_uninit]] marker is spelled in the C++23 +// attribute position after the lambda-introducer. A deduced return type is +// resolved before the check runs, so `auto` lambdas are covered too. +// (co_return is not this hook's: it lowers to promise.return_value(e), whose +// argument funnels through the call-argument binding site.) +void test_lambda_return_unmarked() { + auto explicit_ret = [](int *q [[ref_to_uninit]]) -> int * { + return q; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + auto deduced_ret = [](int *q [[ref_to_uninit]]) { + return q; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + auto ref_ret = []() -> int & { + return g_uninit; // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + auto ok = []() -> int * { return &g_init; }; // OK + (void)explicit_ret; (void)deduced_ret; (void)ref_ret; (void)ok; +} + +// The marked call operator is enforced in both directions, enabling the §4.4 +// get_uninit() idiom in lambda form. +void test_lambda_return_marked() { + auto marked = [] [[ref_to_uninit]] (int *q [[ref_to_uninit]]) -> int * { + return q; // OK: marked operator returning uninitialized memory + }; + auto marked_bad = [] [[ref_to_uninit]] () -> int * { + return &g_init; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + }; + (void)marked; (void)marked_bad; +} + +// The enclosing function's marker never leaks into a lambda's returns (and +// vice versa): each return is attributed to its own scope's declaration. +[[ref_to_uninit]] int *marked_fn_lambda_isolated() { + auto inner = []() -> int * { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + (void)inner; + return &g_uninit; // OK: the function's own marker +} +int *unmarked_fn_marked_lambda() { + auto inner = [] [[ref_to_uninit]] () -> int * { + return &g_uninit; // OK: the lambda's own marker + }; + (void)inner; + return &g_init; // OK: the function itself is unmarked +} + +// Nested lambdas: the inner return is checked against the inner operator, the +// outer return against the outer one. +void test_nested_lambda_returns() { + auto outer = [] [[ref_to_uninit]] () -> int * { + auto inner = []() -> int * { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + (void)inner; + return &g_uninit; // OK: the outer operator is marked + }; + (void)outer; +} + +// Suppression covers a lambda return like any other site: the declaration +// statement's parse-time dominion spans the lambda body's tokens. +void test_lambda_return_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] auto l = []() -> int * { + return &g_uninit; // OK: suppressed + }; + auto m = []() -> int * { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] return &g_uninit; // OK: suppressed + }; + (void)l; (void)m; +} + +// Parse-order store credit reaches lambda returns like every binding: the +// body's own store precedes the return in parse order, so the credited entity +// is initialized memory and the unmarked return is accepted (the by-ref +// capture is accepted for the same reason, see test_ref_capture_body_store). +void test_lambda_return_after_store() { + int u [[uninit]]; + auto l = [&]() -> int * { + u = 5; + return &u; // OK: credited by the store above + }; + (void)l; +} + +// A generic lambda's call operator is a template pattern: the Decl-carrying +// return check defers via isTemplated and fires when the call operator is +// instantiated -- mirroring the variable-init site (template_nondependent_bad) +// -- so a never-invoked generic lambda's return stays undiagnosed (deferred, +// never instantiated), and an invoked one fires per instantiation. +void test_generic_lambda_return() { + auto never = [](auto) -> int * { return &g_uninit; }; // OK: never instantiated + auto invoked = [](auto) -> int * { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + invoked(0); // expected-note {{in instantiation of function template specialization}} + (void)never; +} + +// A block's return is the same capturing-scope binding with no declaration to +// carry the marker, so it is checked as an unmarked target (like a variadic +// argument), through the same null-target path. +void test_block_return() { + int *(^b1)() = ^int *() { + return &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + int *(^b2)() = ^int *() { + return &g_init; // OK + }; + (void)b1; (void)b2; +} + template void template_bad() { T *p = &g_uninit; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} From 90b6947e143141ea635b62a554e610c315736488 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 14:38:41 -0400 Subject: [PATCH 254/289] Credit parse-order member stores per tracked base object --- clang/docs/ProfilesFramework.rst | 20 ++- clang/include/clang/Sema/SemaProfiles.h | 30 ++++ clang/lib/Sema/SemaProfiles.cpp | 95 +++++++++++- .../safety-profile-init-ref-to-uninit.cpp | 143 +++++++++++++++++- 4 files changed, 277 insertions(+), 11 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index d487edd913fe1..99bd2fea59248 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -473,10 +473,22 @@ credited in either direction (§5.4's random-access ban applies even to ``&u`` to a ``[[ref_to_uninit]]`` parameter) never count -- the paper reserves callee-initialization for ``now_init()`` (§6.2). Class-typed whole-object assignment never credits either: it is a member ``operator=`` -call on uninitialized storage, rejected as above. The credit is purely -parse-order, with no dominance or flow analysis: a store under a condition -credits everything after it, so a binding on the untaken path is a missed -diagnostic (see `Limitations`_). +call on uninitialized storage, rejected as above. + +Whole-member stores are credited too, keyed per base object: after +``a.m = 5;`` on a directly named local, or ``this->m = 5;`` on the current +object (keyed to the enclosing function body, so no other function -- nor a +lambda body, which is its own function -- shares it), binding ``a.m`` or +``&a.m`` through the *same* base is accepted, and the reverse direction +applies as for locals. The boundaries: one member level only (``x.agg.m = +5;`` is itself the piecemeal-initialization error and earns nothing), only +directly named non-reference locals or the current object (a member reached +through a pointer, reference, or any other object -- including a copy, per +§5.2 -- stays strict), and never member *pointee* stores (``*w.p = 5;``), +whose aliasing is per-value: a copy of the object shares the pointee. The +credit is purely parse-order, with no dominance or flow analysis: a store +under a condition credits everything after it, so a binding on the untaken +path is a missed diagnostic (see `Limitations`_). Constructors diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 2468055306872..65047440bda01 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -393,6 +393,23 @@ class SemaProfiles : public SemaBase { /// references cannot be reseated, so their credit is never cleared). bool hasPointeeStoreCredit(const ValueDecl *VD) const; + /// True if the [[uninit]] member \p F of the base object identified by + /// \p Base (see resolveMemberStoreBase; null returns false) is credited by + /// a recorded whole-member store; the member then classifies as + /// initialized through that same base. + bool hasMemberStoreCredit(const Decl *Base, const FieldDecl *F) const; + + /// Resolve the identity key of a member access's base object for the + /// per-object member store credit: the enclosing function declaration for + /// a current-object access (this->m / m / (*this).m) -- so credit recorded + /// in one function body can never satisfy a binding in another -- or the + /// directly named local-storage, non-reference VarDecl of a dot access + /// (a.m). Any other base -- another member (a.b.m; §5.4 rejects deep + /// delayed-initialization tracking), an arrow through an arbitrary pointer + /// value, a reference (an alias to an object also reachable other ways) -- + /// is untrackable per object: null. + const Decl *resolveMemberStoreBase(const MemberExpr *ME) const; + /// Store-credit bits for recordInitProfileStore. enum InitStoreCreditFlags : unsigned { /// The [[uninit]] entity itself was assigned (u = e, u @= e, ++u). @@ -409,6 +426,19 @@ class SemaProfiles : public SemaBase { /// pattern-time and instantiation-time state stay independent. llvm::DenseMap InitStoreCredit; + /// Parse-order whole-member store credit, keyed per base object: the base + /// is the directly named local-storage VarDecl (a.m = e) or, for the + /// current object (this->m = e / m = e), the enclosing function + /// declaration -- so credit recorded in one function body can never + /// satisfy a binding in another, and two locals of the same type never + /// share credit. Only WholeStored is ever set: member *pointee* stores + /// (*a.p = e) are deliberately never credited -- per-object pointee + /// aliasing (copies share pointees) makes them unsound to approximate. + /// Never cleared, for the same reasons as InitStoreCredit (instantiations + /// key on fresh field and function declarations). + llvm::DenseMap, unsigned> + MemberStoreCredit; + /// std::init / ref_to_uninit (paper §5): a thrown pointer copy-initializes /// the exception object, which cannot carry [[ref_to_uninit]]; a no-op for /// a non-pointer exception object. Hosts the cluster from diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index d15261f0287b4..ff205afd3d556 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -929,6 +929,45 @@ const ValueDecl *SemaProfiles::getDirectlyNamedDecl(const Expr *E) { return nullptr; } +// True if E denotes the current object: `this` (the implicit/explicit pointer +// of an arrow access) or `*this` (the object lvalue of a dot access). A local +// twin of AnalysisBasedWarnings.cpp's isCurrentObjectBase (the CFG passes' +// helper); each file keeps its recognizer vocabulary self-contained. +static bool isCurrentObjectExpr(const Expr *E) { + E = E->IgnoreParenImpCasts(); + if (isa(E)) + return true; + const auto *UO = dyn_cast(E); + return UO && UO->getOpcode() == UO_Deref && + isa(UO->getSubExpr()->IgnoreParenImpCasts()); +} + +const Decl *SemaProfiles::resolveMemberStoreBase(const MemberExpr *ME) const { + const Expr *Base = ME->getBase()->IgnoreParenImpCasts(); + // The current object: this->m, the implicit m, or (*this).m. Keyed on the + // enclosing function declaration (`this` cannot be reseated, so the key is + // stable for the whole body); AllowLambda gives a lambda body inside a + // member function its own key, so its stores and the enclosing function's + // never share credit. No current function (e.g. an NSDMI parse) is + // untrackable. + if (isCurrentObjectExpr(Base)) + return SemaRef.getCurFunctionDecl(/*AllowLambda=*/true); + // A directly named local object: a dot access on a local-storage, + // non-reference VarDecl (a by-value parameter is its own object and + // qualifies). A reference base is an alias to an object also reachable + // under other names, and an arrow base reaches the object through an + // arbitrary (reseatable) pointer value -- both untrackable per object, the + // same aliasing boundary that keeps fields of parameter-reached objects + // uncredited. A deeper base (a.b.m) is §5.4's rejected deep + // delayed-initialization tracking. + if (!ME->isArrow()) + if (const auto *DRE = dyn_cast(Base)) + if (const auto *VD = dyn_cast(DRE->getDecl()); + VD && VD->hasLocalStorage() && !VD->getType()->isReferenceType()) + return VD; + return nullptr; +} + // Pass-through forms shared by the pointer and glvalue recognizers, which are // transparent to their operand: a single-element braced initializer { e } // binds from e (modeling @@ -1139,7 +1178,26 @@ static UninitStorage glvalueDenotesUninitStorage(ASTContext &Ctx, const Expr *E, // initialization of an [[uninit]] object is itself banned (paper §5.4; // only whole-object construct_at re-initializes, which is uniformly // unmodeled) -- so below the top level the marker counts for every access. - if (DeclDenotesUninit(ME->getMemberDecl())) + // + // Parse-order member store credit: after `a.m = 5` / `this->m = 5`, the + // marked member of that *specific* base object counts as initialized + // (paper §4.2: "After initialization, the object is no longer + // [[uninit]]"; §6: assignment initializes a built-in) -- covering both + // `a.m` (a reference binding lands in this arm directly) and `&a.m` (the + // UO_AddrOf arm recurses here). The consult keys on the same base + // identity the recording resolved, so the same member observed through + // any other object -- including a copy (§5.2: a copy does not inherit + // credit) -- stays uncredited. It applies at any chain depth: the map + // only ever holds whole-member stores (which initialize the entire + // member), and in practice only scalar members (see + // recordInitProfileStore), which have no subobjects to chain through. + const ValueDecl *MD = ME->getMemberDecl(); + if (const auto *F = dyn_cast(MD); + F && Opts.Credit && F->hasAttr() && + Opts.Credit->hasMemberStoreCredit( + Opts.Credit->resolveMemberStoreBase(ME), F)) + return UninitStorage::Initialized; + if (DeclDenotesUninit(MD)) return UninitStorage::Uninitialized; return ME->isArrow() ? pointerRefersToUninitStorage( Ctx, ME->getBase(), Opts.withoutTopLevelDrop()) @@ -1498,9 +1556,30 @@ void SemaProfiles::recordInitProfileStore(const Expr *LHS) { InitStoreCredit[VD] |= PointeeStored; return; } - // Only a directly named local-storage variable can be credited: a - // MemberExpr's FieldDecl fails the VarDecl cast (fields are the CFG - // passes' territory) and statics fail hasLocalStorage. + // a.m = e / this->m = e / m = e (also `@=` and `++`, via the shared + // hosts): a whole-member store to an [[uninit]] field of a trackable base + // object is that member's initialization (paper §4.2: "After + // initialization, the object is no longer [[uninit]]"; §6: ordinary + // assignment initializes a built-in), keyed per (base, field) so unrelated + // objects and other function bodies never share credit. Only single-level + // bases earn credit (x.agg.m = e resolves no base -- and is itself an + // uninit_write violation; §5.4 rejects deep delayed-initialization + // tracking), element stores (a.m[i] = e) present a subscript, not a + // MemberExpr, and stay uncredited, and a class-typed x.agg = e is a member + // operator= call (rejected as a call on uninitialized storage) that never + // reaches this built-in-assignment funnel -- so only scalar members are + // ever credited. Member *pointee* stores (*a.p = e) took the deref arm + // above, which keys on local pointers only: the pinned per-object aliasing + // boundary (copies share pointees). + if (const auto *ME = dyn_cast(E)) { + if (const auto *F = dyn_cast(ME->getMemberDecl()); + F && F->hasAttr()) + if (const Decl *Base = resolveMemberStoreBase(ME)) + MemberStoreCredit[{Base, F}] |= WholeStored; + return; + } + // Only a directly named local-storage variable can be credited beyond + // this point: statics fail hasLocalStorage. const auto *VD = dyn_cast_or_null(getDirectlyNamedDecl(E)); if (!VD || !VD->hasLocalStorage()) return; @@ -1541,6 +1620,14 @@ bool SemaProfiles::hasPointeeStoreCredit(const ValueDecl *VD) const { return It != InitStoreCredit.end() && (It->second & PointeeStored); } +bool SemaProfiles::hasMemberStoreCredit(const Decl *Base, + const FieldDecl *F) const { + if (!Base || !F) + return false; + auto It = MemberStoreCredit.find({Base, F}); + return It != MemberStoreCredit.end() && (It->second & WholeStored); +} + void SemaProfiles::checkInitProfileThrowOperand(const Expr *Operand) { // A thrown pointer copy-initializes the exception object, which cannot // carry [[ref_to_uninit]], so throwing a pointer to uninitialized memory is diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index ac58edf76086d..e46a1c8b04be4 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1501,9 +1501,12 @@ void test_reseat_clears_credit(int *p [[ref_to_uninit]], (void)x; (void)y; (void)z; } -// The credit map keys on local VarDecls, so a [[ref_to_uninit]] *member* -// pointer is never credited: a read through it keeps failing even after a -// store through the exact same lvalue (no per-object tracking). +// The *pointee* credit map keys on local VarDecls, so a [[ref_to_uninit]] +// *member* pointer is never credited: a read through it keeps failing even +// after a store through the exact same lvalue. The per-object *whole-member* +// credit below deliberately does not extend here: pointee aliasing is +// per-value, not per-object (a copy of the object shares the pointee), so +// crediting `(w, p)` would be unsound the moment w is copied. struct WithMarkedPtrField { int *p [[ref_to_uninit]] = &g_uninit; }; @@ -1522,6 +1525,140 @@ void test_member_store_never_credits(Inner *ptr [[ref_to_uninit]]) { (void)y; } +// Per-object whole-member store credit (paper §4.2: "After initialization, +// the object is no longer [[uninit]]"; §6: ordinary assignment initializes a +// built-in): `a.m = 5` credits exactly the (base object, member) pair, so a +// later binding of that member through the same base is legal -- the member +// analog of the locals credit above. The base identity is the directly named +// local-storage variable or, for current-object accesses (this->m / m), the +// enclosing function declaration, so unrelated objects and other function +// bodies never share credit. Same parse-order semantics: no dominance or +// flow analysis, missed diagnostics only. +struct MemberCredit { int m [[uninit]]; }; // expected-note {{member 'm' declared here}} +void mc_sink_ptr(int *); +void mc_sink_ref(const int &); +void mc_fill(int *p [[ref_to_uninit]]); + +void test_member_store_credit() { + MemberCredit a; + mc_sink_ref(a.m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + mc_sink_ptr(&a.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + a.m = 5; + mc_sink_ref(a.m); // OK: credited + mc_sink_ptr(&a.m); // OK: credited + int *q = &a.m; // OK: credited + (void)q; +} + +// The reverse direction applies too (mirroring test_assign_credited_to_marked): +// a credited member is initialized memory and now requires an unmarked target. +void test_member_credit_reverse_direction() { + MemberCredit a; + a.m = 5; + mc_fill(&a.m); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// The credit is per base object: a store to one local's member says nothing +// about another local of the same type (and per §5.2, nothing about a copy). +void test_member_credit_per_object() { + MemberCredit a1, a2; + a1.m = 5; + mc_sink_ref(a1.m); // OK: credited + mc_sink_ref(a2.m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Current-object members key on the enclosing function: `m = 5` in one member +// function never credits a binding in another, `this->m` / `m` / `(*this).m` +// share the key within one body, and a this-capturing lambda's body is its +// own function (its stores and the enclosing function's do not mix, in +// either direction). +struct ThisMemberCredit { + int m [[uninit]]; + void store_then_bind() { + this->m = 5; + mc_sink_ref(m); // OK: credited (same key as this->m) + mc_sink_ptr(&(*this).m); // OK: credited + } + void bind_without_store() { + mc_sink_ref(m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + } + void lambda_store_isolated() { + auto l = [this] { m = 5; }; // records under the lambda's own key + mc_sink_ptr(&m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)l; + } + void lambda_bind_isolated() { + m = 5; // records under this function's key + auto l = [this] { + mc_sink_ptr(&m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + }; + (void)l; + } +}; + +// In a constructor, the parse-time binding after a whole-member store is +// credited the same way (this was the escape-adjacent false positive); the +// CFG ctor-body pass independently keeps governing *reads*, with real flow +// analysis (safety-profile-init-ctor-body.cpp). +struct CtorMemberCredit { + int m [[uninit]]; + CtorMemberCredit() { + m = 5; + mc_sink_ptr(&m); // OK: credited by the store above + } + CtorMemberCredit(int) { + mc_sink_ptr(&m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + m = 5; + } +}; + +// Compound assignment and ++/-- record through the same arm: the store side +// credits later bindings (the read side of `a.m += 1` on a local aggregate is +// the CFG local-members pass's, which flags it with flow precision). +void test_member_credit_compound() { + MemberCredit a; + a.m += 1; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + mc_sink_ref(a.m); // OK: the compound store credited (a, m) +} + +// Only a single-level, directly named base earns or consults credit. A store +// below a marked class-type member (x.agg.m) is itself the banned piecemeal +// initialization (uninit_write, §5.4) and resolves no base, so it earns +// nothing -- the later binding still sees agg's marker below top level. A +// whole-member `x.agg = ...` cannot credit either: a class-typed assignment +// is a member operator= call on uninitialized storage, rejected outright. +struct AggMemberCredit { MemberCredit agg [[uninit]]; }; +void test_member_credit_single_level() { + AggMemberCredit x; + x.agg.m = 5; // expected-error {{writing a member of an '[[uninit]]' object does not initialize it under profile 'std::init'; initialize the whole object}} + mc_sink_ptr(&x.agg.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + x.agg = MemberCredit(); // expected-error {{calling member function 'operator=' binds its implicit object parameter to uninitialized memory under profile 'std::init'}} +} + +// A member of an object reached through a reference or pointer parameter is +// the pinned aliasing boundary: the store is trusted as a write, but the +// binding through that alias stays strict. +void test_member_credit_alias_boundary(MemberCredit &r, MemberCredit *p) { + r.m = 5; + mc_sink_ref(r.m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + p->m = 5; + mc_sink_ptr(&p->m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Credit is recorded at pattern-parse time and independently at instantiation +// (fresh field and function declarations), so the store-then-bind sequence +// holds in templates too; the dependent binding defers on the pattern and +// re-runs on the rebuilt (credited-in-order) instantiation. +template +struct TmplMemberCredit { + int m [[uninit]]; + void f() { + this->m = 5; + mc_sink_ref(this->m); // OK at the pattern and at instantiation + } +}; +template struct TmplMemberCredit; + // The reverse direction applies through the assignment funnel too: a // credited marked pointer refers to initialized memory, so assigning it to // another marked pointer is the requires-uninit error -- while an unmarked From 86627565953ba3b3ce5eaf3d4294a92f3e512490 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 15:16:00 -0400 Subject: [PATCH 255/289] Add [[now_init]] and credit marked arguments of its callees --- clang/docs/ProfilesFramework.rst | 60 +++++-- clang/docs/ProfilesFrameworkInternals.rst | 5 +- clang/include/clang/Basic/Attr.td | 6 + clang/include/clang/Basic/AttrDocs.td | 39 +++++ .../clang/Basic/DiagnosticSemaKinds.td | 3 + clang/include/clang/Sema/SemaProfiles.h | 26 +++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 96 +++++++++-- clang/lib/Sema/SemaDeclAttr.cpp | 28 ++++ clang/lib/Sema/SemaProfiles.cpp | 111 ++++++++++++- ...a-attribute-supported-attributes-list.test | 1 + .../SemaCXX/safety-profile-init-ctor-body.cpp | 149 +++++++++++++++++- .../safety-profile-init-ref-to-uninit.cpp | 149 ++++++++++++++++++ .../safety-profile-now-init-marker.cpp | 66 ++++++++ 13 files changed, 692 insertions(+), 47 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-now-init-marker.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 99bd2fea59248..80d8f1eb5d8a5 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -490,6 +490,25 @@ credit is purely parse-order, with no dominance or flow analysis: a store under a condition credits everything after it, so a binding on the untaken path is a missed diagnostic (see `Limitations`_). +One kind of call *does* count as initialization: §6.2's ``[[now_init]]`` +attribute (its placement and spelling track an open committee question) +declares that a function initializes the storage passed to each of its +``[[ref_to_uninit]]`` parameters, and it requires at least one such +parameter. After a call to a ``[[now_init]]`` function, the argument's +storage earns exactly the credit the equivalent direct store would -- +``fill(&u)`` credits ``u`` whole, ``fill(p)`` credits the marked pointer's +pointee (until ``p`` is reseated), ``fill(&a.m)`` credits the ``(a, m)`` +pair -- with the same boundaries and the same reverse-direction consequence +(a second ``fill(&u)`` is rejected: ``u`` no longer refers to uninitialized +memory, which incidentally catches double ``construct_at``). This is R2 +§4.5's requested library annotation: declare ``construct_at`` with +``[[now_init]]`` and a ``[[ref_to_uninit]]`` first parameter and the +lifecycle *start* is checked. The §4.4 ``now_init()`` identity function +needs no compiler support at all -- declared as ``template T* +now_init(T* p [[ref_to_uninit]]);``, its unmarked return is already trusted +as initialized -- but only the attribute legalizes the original *name* after +the call. + Constructors ------------ @@ -560,23 +579,30 @@ is a deliberate strictness -- it rejects code the paper itself rejects -- rather than an omission; each of the others is a missed diagnostic, never a false positive. -- Inside a constructor body, *only* a plain assignment to an ``[[uninit]]`` - member counts as its initialization. Taking the member's address, binding - a reference to it, calling a member function, letting ``this`` escape, or - passing ``&m`` to a ``[[ref_to_uninit]]`` parameter earns no credit, so a - later read of the member is rejected: the paper rejects complex +- Inside a constructor body, only a plain assignment to an ``[[uninit]]`` + member or a call to a ``[[now_init]]`` function (§6.2) counts as its + initialization -- the latter for the current-object storage bound to the + callee's ``[[ref_to_uninit]]`` parameters (``&m``, ``m``, or ``this`` + itself, which credits every tracked member), as a genuine dataflow fact, + so a ``[[now_init]]`` call on one branch still does not satisfy a read at + the join (§1.2). Taking the member's address, binding a reference to it, + calling a member function, letting ``this`` escape, or passing ``&m`` to a + ``[[ref_to_uninit]]`` parameter of an *ordinary* function earns no credit, + so a later read of the member is rejected: the paper rejects complex constructor code (§5.1) and reserves callee-initialization for - ``now_init()`` (§6.2); the remedy for an intended flow is - ``[[profiles::suppress]]``. For *locals*, by contrast, the local-aggregate - pass and the plain-local analysis conservatively treat any escape of the - variable as an assignment -- there the omission is a missed diagnostic, - never a false positive. That escape-crediting is an interim deviation - from the paper, to be replaced by explicit ``now_init()`` recognition when - it is implemented. -- ``construct_at``/``destroy_at`` flow is not modeled: class-type - initialization of uninitialized storage, double initialization, and double - destruction are not checked, and writes through ``[[ref_to_uninit]]`` are - not verified. + ``now_init`` (§6.2); the remedy for an intended flow is ``[[now_init]]`` + on the callee or ``[[profiles::suppress]]``. For *locals*, by contrast, + the local-aggregate pass and the plain-local analysis conservatively treat + any escape of the variable as an assignment -- there the omission is a + missed diagnostic, never a false positive. That escape-crediting is an + interim leniency relative to the paper (which credits only ``now_init``); + tightening it to ``[[now_init]]`` callees alone is future work. +- ``construct_at``/``destroy_at`` flow is only partially modeled: a + ``[[now_init]]``-annotated ``construct_at`` declaration checks the + lifecycle start (including double construction, via the reverse-direction + binding rule), but destruction -- ``destroy_at``, double destruction, + use-after-destroy -- is not checked, and writes through + ``[[ref_to_uninit]]`` are not verified. - A ``new`` expression whose result is not bound to anything (``new int;``) is not checked. - A call through a function pointer cannot see parameter markers on the @@ -591,6 +617,8 @@ false positive. - Whole-entity store credit is parse-order only: a store under a condition (or inside a lambda body) credits every later use in parse order, so a read or binding on a path that skips the store is a missed diagnostic. + ``[[now_init]]`` call credit outside constructor bodies is parse-order in + the same way (inside them it is a real dataflow fact, as above). - In a template, a violation in non-dependent code is diagnosed at definition time and may be repeated at instantiation. diff --git a/clang/docs/ProfilesFrameworkInternals.rst b/clang/docs/ProfilesFrameworkInternals.rst index 052f0a044aad1..a5d5b8e2d0891 100644 --- a/clang/docs/ProfilesFrameworkInternals.rst +++ b/clang/docs/ProfilesFrameworkInternals.rst @@ -251,7 +251,10 @@ patterns. Its rules map to mechanisms as follows: - 2 and 1 - ``CFGUninitProfiles`` row for local variables; ``checkInitProfileCtorBody`` and ``checkInitProfileLocalMembers`` - (definite-assignment dataflow over ``[[uninit]]`` members); + (definite-assignment dataflow over ``[[uninit]]`` members; the + ctor-body pass's ``CallExpr`` arm turns a ``[[now_init]]`` call into a + ``Gen`` bit for the current-object storage bound to the callee's + marked parameters, P4222R2 §6.2); ``checkInitProfileReadThrough`` at the lvalue-to-rvalue chokepoint, plus compound-assignment and increment/decrement hooks * - ``uninit_decl`` diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index cf6df4dddb9a0..65e30f7a40823 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1733,6 +1733,12 @@ def RefToUninit : InheritableAttr { let Documentation = [RefToUninitDocs]; } +def NowInit : InheritableAttr { + let Spellings = [CXX11<"", "now_init", 202602>]; + let Subjects = SubjectList<[Function], ErrorDiag>; + let Documentation = [NowInitDocs]; +} + def NonBlocking : TypeAttr { let Spellings = [Clang<"nonblocking">]; let Args = [ExprArgument<"Cond", /*optional*/1>]; diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 454f993aa187e..7745feba2f1bf 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -957,6 +957,45 @@ without ``-fprofiles``. }]; } +def NowInitDocs : Documentation { + let Category = DocCatFunction; + let Heading = "now_init"; + let Content = [{ +The ``[[now_init]]`` attribute marks a function as *initializing* the +storage passed to each of its ``[[ref_to_uninit]]`` parameters. It is purely +informational: it has no effect on code generation. + +The attribute exists to support the initialization profile (see +:doc:`ProfilesFramework`) and implements the annotation proposed in P4222R2 +§6.2 (its exact placement and spelling track an open committee question). +Passing uninitialized memory to a ``[[ref_to_uninit]]`` parameter never by +itself convinces the profile that the memory was initialized -- the callee +may merely store the pointer. On a ``[[now_init]]`` function the profile +instead treats the storage bound to every ``[[ref_to_uninit]]`` parameter as +initialized after the call returns, so the caller may continue to use the +original name: + +.. code-block:: c++ + + [[now_init]] void fill(int *p [[ref_to_uninit]]); + + void f() { + int u [[uninit]]; + fill(&u); + int v = u; // OK: fill() initialized u + } + +The attribute requires at least one parameter marked ``[[ref_to_uninit]]`` +(it would be vacuous otherwise); this is diagnosed regardless of +``-fprofiles``. It does not cover the implicit object parameter of a member +function, which cannot carry ``[[ref_to_uninit]]``. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + }]; +} + def NoMergeDocs : Documentation { let Category = DocCatStmt; let Content = [{ diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index d8d6e9bb414ae..b999e2fa865cb 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14177,6 +14177,9 @@ def err_uninit_attr_invalid_subject : Error< def err_ref_to_uninit_attr_invalid_type : Error< "'ref_to_uninit' attribute only applies to pointers, references, and " "functions returning them">; +def err_now_init_attr_no_marked_parameter : Error< + "'now_init' attribute requires at least one parameter marked " + "'[[ref_to_uninit]]'">; def err_init_union_marker : ProfileRuleError< "'[[uninit]]' cannot be applied to %select{a variable of union type|" "a union member|a data member of union type}1 under profile '%0'">; diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 65047440bda01..6cdcb956caa63 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -381,6 +381,32 @@ class SemaProfiles : public SemaBase { /// failing to credit it would turn suppression into later false positives. void recordInitProfileStore(const Expr *LHS); + /// std::init / [[now_init]] (P4222R2 §6.2): a [[now_init]] callee + /// initializes the storage bound to each of its [[ref_to_uninit]] + /// parameters, so the binding earns the same parse-order credit the + /// equivalent direct store would. Called from the tail of + /// checkInitProfileRefToUninitBinding when \p Target is a marked parameter + /// of a [[now_init]] function; recognizes the affirmatively creditable + /// source shapes -- &u / u (whole-entity credit on an [[uninit]] local), p + /// / *p / &*p (pointee credit on a marked local/parameter pointer; §6.2's + /// initialize2(p) example), r (pointee credit on a marked reference), and + /// &base.m / base.m (per-object member credit, resolveMemberStoreBase + /// keys) -- through the recognizers' explicit-cast pass-through. Variadic + /// arguments, unmarked parameters, and calls through function pointers + /// never reach here (no marked ParmVarDecl target). Recorded regardless of + /// enforcement, suppression, or diagnosis of the binding itself (the + /// callee still initializes; recordInitProfileStore's rationale), but not + /// in never-executed contexts. + void recordNowInitArgument(const ValueDecl *Target, QualType T, + const Expr *Src); + + /// True if the current expression-evaluation context never executes at + /// runtime (unevaluated or discarded-statement), mirroring + /// shouldEmitProfileViolation's context checks: a store or a callee + /// initialization seen there earns no credit. The shared gate of + /// recordInitProfileStore and recordNowInitArgument. + bool inNeverExecutedContext() const; + /// True if \p VD is a local [[uninit]] variable credited by a recorded /// whole-entity store; the recognizers then classify it as initialized /// (which also enables the paper's reverse-direction rule: a credited diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 84f2f1a474734..99679ecda2244 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -1804,17 +1804,22 @@ static void collectTrackedUninitMembers( // simply never read is left as-is, exactly as the structural ctor_uninit_member // check (R5) excuses a marked member. // -// Crediting is strictly assignment-only: nothing but a plain whole-member -// store (or a written member/base initializer) marks a member assigned. -// Taking the member's address, binding a reference to it, calling a member -// function, letting `this` escape, or passing &m to a [[ref_to_uninit]] -// parameter earns no credit -- the paper rejects complex constructor code -// (§5.1) and reserves callee-initialization for now_init() (§6.2), so such -// code is deliberately rejected here (the remedy is [[profiles::suppress]]). -// This is the deliberate counterpart of checkInitProfileLocalMembers' -// "soundness over completeness" escape crediting below: locals conservatively -// credit any escape (missed diagnostics only), while constructor bodies get -// the paper's strictness. +// Crediting is strict for plain escapes: nothing but a whole-member store +// (or a written member/base initializer) marks a member assigned. Taking the +// member's address, binding a reference to it, calling a member function, +// letting `this` escape, or passing &m to a [[ref_to_uninit]] parameter of +// an ordinary function earns no credit -- the paper rejects complex +// constructor code (§5.1) and reserves callee-initialization for now_init +// (§6.2), so such code is deliberately rejected here (the remedy is +// [[profiles::suppress]]). The one sanctioned exception is exactly §6.2's: a +// call to a [[now_init]] function credits the current-object storage bound +// to its [[ref_to_uninit]] parameters (see the CallExpr arm below), as a +// real Gen bit -- so §1.2's all-branches rule still governs a call under a +// branch. This is the deliberate counterpart of +// checkInitProfileLocalMembers' "soundness over completeness" escape +// crediting below: locals conservatively credit any escape (missed +// diagnostics only, subsuming [[now_init]] callees), while constructor +// bodies get the paper's strictness. static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, AnalysisDeclContext &AC) { // A delegating constructor leaves member initialization to its target (paper @@ -1956,6 +1961,63 @@ static void checkInitProfileCtorBody(Sema &S, const CXXConstructorDecl *Ctor, continue; BlockEvents.push_back({ReadWrite, It->second, UO}); Gen[B->getBlockID()].set(It->second); + } else if (const auto *CE = dyn_cast(St)) { + // A call to a [[now_init]] function initializes the storage bound to + // each of its [[ref_to_uninit]] parameters (P4222R2 §6.2) -- the + // paper's sanctioned exception to the strict assignment-only + // crediting above. A current-object member passed as `&m` / `m` + // becomes assigned at the call element (its argument-subexpression + // events, e.g. a read of another member, precede it in the block); + // passing `this` / `*this` itself to a marked parameter hands the + // callee the whole object to initialize, so every tracked member is + // assigned. This is a real Gen bit in the dataflow, not parse-order + // credit: a [[now_init]] call under a branch still does not satisfy + // a read at the join (§1.2's all-branches rule). A plain (non- + // [[now_init]]) callee continues to earn nothing. + const FunctionDecl *Callee = CE->getDirectCallee(); + if (!Callee || !Callee->hasAttr()) + continue; + // Zip declared parameters with arguments. A member operator called + // through CXXOperatorCallExpr receives the object as argument 0 + // ahead of its declared parameters -- for a C++23 static operator + // too, whose object argument is still evaluated -- so skip it. An + // explicit-object member function instead declares its object as + // parameter 0, so its mapping is already direct. + unsigned ArgOffset = 0; + if (isa(CE)) + if (const auto *MD = dyn_cast(Callee); + MD && !MD->isExplicitObjectMemberFunction()) + ArgOffset = 1; + for (unsigned PI = 0, NP = Callee->getNumParams(); PI != NP; ++PI) { + if (PI + ArgOffset >= CE->getNumArgs()) + break; + if (!Callee->getParamDecl(PI)->hasAttr()) + continue; + const Expr *Arg = CE->getArg(PI + ArgOffset)->IgnoreParenImpCasts(); + // Peel explicit pointer/reference casts, mirroring the parse-time + // recognizers (§4.3: a cast marked pointer is itself marked). + while (const auto *Cast = dyn_cast(Arg)) { + const Expr *Sub = Cast->getSubExpr(); + if (!Sub->getType()->isPointerType() && !Sub->isGLValue()) + break; + Arg = Sub->IgnoreParenImpCasts(); + } + const Expr *G = Arg; + if (const auto *AddrOf = dyn_cast(Arg); + AddrOf && AddrOf->getOpcode() == UO_AddrOf) + G = AddrOf->getSubExpr(); + if (const FieldDecl *F = getCurrentObjectMember(G)) { + auto It = Index.find(F); + if (It == Index.end()) + continue; + BlockEvents.push_back({Write, It->second, CE}); + Gen[B->getBlockID()].set(It->second); + } else if (isCurrentObjectBase(Arg)) { + for (unsigned Idx = 0; Idx != N; ++Idx) + BlockEvents.push_back({Write, Idx, CE}); + Gen[B->getBlockID()].set(); + } + } } else if (const auto *LE = dyn_cast(St)) { // The lambda body is a separate function and never appears in this // CFG, but a this-capturing lambda can read members the moment it is @@ -2155,11 +2217,13 @@ static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { // recognized member read or write -- &a, &a.m, a reference binding, passing a // to any function (construct_at, memcpy), a member call, a lambda capture -- // conservatively marks every member assigned from that point (the address may -// be used to initialize the object). Members of an object with a -// user-provided constructor stay untracked (trusted, §5.1), as do objects -// reached through parameters, references, or other objects. A backward goto -// across the declaration re-default-initializes the object, which the -// gen-only dataflow cannot model -- a possible missed diagnostic, matching +// be used to initialize the object). This subsumes [[now_init]] callees +// (§6.2): passing &a.m to one is an escape like any other, so no dedicated +// call arm is needed here, unlike the strict ctor-body pass above. Members of +// an object with a user-provided constructor stay untracked (trusted, §5.1), as +// do objects reached through parameters, references, or other objects. A +// backward goto across the declaration re-default-initializes the object, which +// the gen-only dataflow cannot model -- a possible missed diagnostic, matching // the ctor-body pass's accepted imprecision level. static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { CFG *cfg = AC.getCFG(); diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 2b14bd19ebd1e..4cb2fa8450f21 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -7005,6 +7005,30 @@ static void handleRefToUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { D->addAttr(::new (S.Context) RefToUninitAttr(S.Context, AL)); } +static void handleNowInitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList restricts D to a function. [[now_init]] asserts that the + // callee initializes the storage bound to each of its [[ref_to_uninit]] + // parameters (P4222R2 §6.2), so a declaration with no marked parameter + // would make it vacuous; reject it. The parameters' attributes are + // attached when the parameter declarations are built, before this + // declaration attribute is processed, so the prefix and suffix attribute + // positions both see them. A dependent parameter's marker is attached to + // the pattern unvalidated (its type check defers to instantiation), so a + // template with a marked dependent parameter passes here; should the + // instantiation drop that marker, the inherited [[now_init]] goes inert + // rather than re-diagnosed, like the dropped marker itself. Like the other + // marker subject checks, this fires regardless of -fprofiles. + if (llvm::none_of( + cast(D)->parameters(), + [](const ParmVarDecl *P) { return P->hasAttr(); })) { + S.Diag(AL.getLoc(), diag::err_now_init_attr_no_marked_parameter); + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) NowInitAttr(S.Context, AL)); +} + static void handleMIGServerRoutineAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // Check that the return type is a `typedef int kern_return_t` or a typedef // around it, because otherwise MIG convention checks make no sense. @@ -8279,6 +8303,10 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, handleRefToUninitAttr(S, D, AL); break; + case ParsedAttr::AT_NowInit: + handleNowInitAttr(S, D, AL); + break; + case ParsedAttr::AT_ObjCExternallyRetained: S.ObjC().handleExternallyRetainedAttr(D, AL); break; diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index ff205afd3d556..5196d48af8f49 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -968,6 +968,20 @@ const Decl *SemaProfiles::resolveMemberStoreBase(const MemberExpr *ME) const { return nullptr; } +// The directly named [[ref_to_uninit]] local/parameter *pointer* of \p E, if +// any: the only pointer entity whose pointee state is tracked (the credit +// map keys on VarDecls; a marked member pointer is the pinned per-object +// aliasing boundary -- copies share pointees). Shared by the store-recording +// deref arm and the [[now_init]] argument shapes. +static const VarDecl *getCreditableMarkedPointer(const Expr *E) { + const auto *VD = + dyn_cast_or_null(SemaProfiles::getDirectlyNamedDecl(E)); + if (VD && VD->hasLocalStorage() && VD->getType()->isPointerType() && + VD->hasAttr()) + return VD; + return nullptr; +} + // Pass-through forms shared by the pointer and glvalue recognizers, which are // transparent to their operand: a single-element braced initializer { e } // binds from e (modeling @@ -1292,6 +1306,89 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, checkInitProfileRefToUninit(Loc, Target && Target->hasAttr(), T->isReferenceType(), Src, D); + // Runs after the check: the binding itself is judged against the + // *pre-call* state, and only then does a [[now_init]] callee's promised + // initialization take effect for what follows in parse order. + recordNowInitArgument(Target, T, Src); +} + +void SemaProfiles::recordNowInitArgument(const ValueDecl *Target, QualType T, + const Expr *Src) { + // Only the binding of a [[ref_to_uninit]] parameter of a [[now_init]] + // function carries the callee's initialization promise (P4222R2 §6.2: the + // attribute "would apply to every [[ref_to_uninit]] argument"). A variadic + // argument, an unmarked parameter, or a call through a function pointer + // presents no marked ParmVarDecl and earns nothing. + const auto *Parm = dyn_cast_or_null(Target); + if (!Parm || !Src || !Parm->hasAttr()) + return; + const auto *FD = dyn_cast_or_null(Parm->getDeclContext()); + if (!FD || !FD->hasAttr()) + return; + // Like recordInitProfileStore: no enforcement, suppression, or in-template + // gate (a suppressed or pattern-parsed call still initializes; rebuilt + // instantiations re-record against fresh declarations), but a call in a + // never-executed context earns no credit. + if (inNeverExecutedContext()) + return; + const Expr *E = Src->IgnoreParenImpCasts(); + // Mirror the recognizers' explicit-cast pass-through (paper §4.3: a cast + // of a marked pointer is itself marked; a reference cast denotes the same + // storage): the callee initializes the same storage either way. + while (const auto *CE = dyn_cast(E)) { + const Expr *Sub = CE->getSubExpr(); + if (!Sub->getType()->isPointerType() && !Sub->isGLValue()) + break; + E = Sub->IgnoreParenImpCasts(); + } + // The glvalue whose storage the callee initializes: the operand of &G for + // a pointer parameter, or the bound glvalue itself for a reference one. + const Expr *Glvalue = nullptr; + if (const auto *UO = dyn_cast(E); + UO && UO->getOpcode() == UO_AddrOf) + Glvalue = UO->getSubExpr()->IgnoreParenImpCasts(); + else if (T->isReferenceType()) + Glvalue = E; + if (Glvalue) { + // &base.m / base.m: per-object whole-member credit, under exactly the + // member-store keys (resolveMemberStoreBase); an untrackable base -- a + // parameter-reached object, a deeper chain -- resolves null and stays + // strict, the same boundary as a direct store. + if (const auto *ME = dyn_cast(Glvalue)) { + if (const auto *F = dyn_cast(ME->getMemberDecl()); + F && F->hasAttr()) + if (const Decl *Base = resolveMemberStoreBase(ME)) + MemberStoreCredit[{Base, F}] |= WholeStored; + return; + } + // &*p / *p: the callee initializes the pointee of a marked pointer. + if (const auto *UO = dyn_cast(Glvalue); + UO && UO->getOpcode() == UO_Deref) { + if (const VarDecl *VD = getCreditableMarkedPointer(UO->getSubExpr())) + InitStoreCredit[VD] |= PointeeStored; + return; + } + if (const auto *VD = + dyn_cast_or_null(getDirectlyNamedDecl(Glvalue)); + VD && VD->hasLocalStorage()) { + // &u / u: the whole [[uninit]] entity is initialized by the callee, + // exactly as `u = e` would credit it. + if (VD->hasAttr()) + InitStoreCredit[VD] |= WholeStored; + // r (a marked reference bound onward): its referent is initialized; a + // reference cannot be reseated, so the credit never lapses. + else if (VD->getType()->isReferenceType() && + VD->hasAttr()) + InitStoreCredit[VD] |= PointeeStored; + } + return; + } + // p as a pointer value: the callee initializes p's pointee -- §6.2's + // initialize2(p) example verbatim. Only a directly named marked + // local/parameter pointer is trackable; reseating p afterwards clears this + // credit like any other pointee credit. + if (const VarDecl *VD = getCreditableMarkedPointer(E)) + InitStoreCredit[VD] |= PointeeStored; } void SemaProfiles::checkInitProfileVariadicArgument(const Expr *Arg) { @@ -1523,6 +1620,11 @@ void SemaProfiles::checkInitProfileIncDec(Expr *Operand, SourceLocation OpLoc) { recordInitProfileStore(Operand); } +bool SemaProfiles::inNeverExecutedContext() const { + return SemaRef.isUnevaluatedContext() || + SemaRef.currentEvaluationContext().isDiscardedStatementContext(); +} + void SemaProfiles::recordInitProfileStore(const Expr *LHS) { if (!getLangOpts().Profiles || !LHS) return; @@ -1534,9 +1636,7 @@ void SemaProfiles::recordInitProfileStore(const Expr *LHS) { // non-dependent code in a template is checked at definition time and must // find pattern-time credit (instantiations rebuild their DeclRefExprs // against fresh decls, so they re-record independently). - if (SemaRef.isUnevaluatedContext()) - return; - if (SemaRef.currentEvaluationContext().isDiscardedStatementContext()) + if (inNeverExecutedContext()) return; const Expr *E = LHS->IgnoreParenImpCasts(); // *p = e: a store through the exact whole-`*p` lvalue of a marked @@ -1549,10 +1649,7 @@ void SemaProfiles::recordInitProfileStore(const Expr *LHS) { // invalidating: the paper bans element-wise tracking (§5.4/§5.5). if (const auto *UO = dyn_cast(E); UO && UO->getOpcode() == UO_Deref) { - if (const auto *VD = - dyn_cast_or_null(getDirectlyNamedDecl(UO->getSubExpr())); - VD && VD->hasLocalStorage() && VD->getType()->isPointerType() && - VD->hasAttr()) + if (const VarDecl *VD = getCreditableMarkedPointer(UO->getSubExpr())) InitStoreCredit[VD] |= PointeeStored; return; } diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test index 049498a8f6b82..ae436d361aba5 100644 --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -146,6 +146,7 @@ // CHECK-NEXT: NoUwtable (SubjectMatchRule_hasType_functionType) // CHECK-NEXT: NonString (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: NotTailCalled (SubjectMatchRule_function) +// CHECK-NEXT: NowInit (SubjectMatchRule_function) // CHECK-NEXT: OMPAssume (SubjectMatchRule_function, SubjectMatchRule_objc_method) // CHECK-NEXT: OSConsumed (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: OSReturnsNotRetained (SubjectMatchRule_function, SubjectMatchRule_objc_method, SubjectMatchRule_objc_property, SubjectMatchRule_variable_is_parameter) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp index 57ab2bc076ee0..dd379afd18665 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor-body.cpp @@ -296,13 +296,15 @@ struct LambdaReadSuppressed { } }; -// Strict assignment-only crediting: inside a constructor body, nothing but a -// plain whole-member assignment counts as initializing an [[uninit]] member. -// Passing &m to a [[ref_to_uninit]] parameter, calling a member function that -// assigns it, or letting `this` escape earns no credit -- the paper rejects -// complex constructor code (§5.1) and reserves callee-initialization for -// now_init() (§6.2); the remedy is [[profiles::suppress]]. Contrast the -// *local*-object passes, which conservatively credit any escape +// Strict crediting for plain escapes: inside a constructor body, nothing but +// a whole-member assignment counts as initializing an [[uninit]] member. +// Passing &m to a [[ref_to_uninit]] parameter of an ordinary function, +// calling a member function that assigns it, or letting `this` escape earns +// no credit -- the paper rejects complex constructor code (§5.1) and +// reserves callee-initialization for now_init (§6.2, the [[now_init]] tests +// below); the remedy for a plain callee is [[profiles::suppress]] or the +// [[now_init]] annotation. Contrast the *local*-object passes, which +// conservatively credit any escape // (safety-profile-init-local-member-read.cpp, test_escape_*). void init_pointee(int *p [[ref_to_uninit]]); struct EscapeMemberAddress { @@ -335,6 +337,139 @@ struct EscapeThis { } }; +// The §6.2 exception: a call to a [[now_init]] function initializes the +// storage bound to each of its [[ref_to_uninit]] parameters, so a +// current-object member passed as `&m` (or `m` to a reference parameter) +// becomes assigned at the call. The credit is a real Gen bit in the +// dataflow -- not parse-order -- so §1.2's all-branches rule still governs: +// a [[now_init]] call under one branch does not satisfy a read at the join. +[[now_init]] void now_init_pointee(int *p [[ref_to_uninit]]); +[[now_init]] void now_init_referent(int &r [[ref_to_uninit]]); +struct NowInitMemberAddress { + int m [[uninit]]; + NowInitMemberAddress() { + now_init_pointee(&m); + int x = m; // OK: the [[now_init]] callee initialized m + (void)x; + } +}; + +struct NowInitMemberReference { + int m [[uninit]]; + NowInitMemberReference() { + now_init_referent(m); + int x = m; // OK + (void)x; + } +}; + +struct NowInitReadBeforeCall { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + NowInitReadBeforeCall() { + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + now_init_pointee(&m); + (void)x; + } +}; + +struct NowInitUnderBranch { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + NowInitUnderBranch(bool b) { + if (b) + now_init_pointee(&m); + int x = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; + } +}; + +struct NowInitBothBranches { + int m [[uninit]]; + NowInitBothBranches(bool b) { + if (b) + now_init_pointee(&m); + else + m = 0; + int x = m; // OK: initialized on every path + (void)x; + } +}; + +// Only the *marked* parameters carry the promise: an unmarked parameter of +// the same [[now_init]] callee earns nothing -- and handing it &m is already +// the parse-time binding violation. +[[now_init]] void now_init_first(int *p [[ref_to_uninit]], int *q); +struct NowInitUnmarkedParam { + int m [[uninit]]; // expected-note {{member 'm' declared here}} + int o [[uninit]]; + NowInitUnmarkedParam() { + now_init_first(&o, &m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int x = o; // OK: bound to the marked parameter + int y = m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} + (void)x; (void)y; + } +}; + +// Passing `this` itself to a marked parameter hands the callee the whole +// object to initialize: every tracked member is assigned at the call. (The +// implicit object parameter of a member call cannot carry the marker, so a +// member call still earns nothing -- see EscapeMemberCall.) +struct NowInitWholeObject; +[[now_init]] void now_init_object(NowInitWholeObject *obj [[ref_to_uninit]]); +struct NowInitWholeObject { + int a [[uninit]]; + int b [[uninit]]; + NowInitWholeObject() { + now_init_object(this); + int x = a + b; // OK: the whole object was handed over for initialization + (void)x; + } +}; + +// Argument mapping through operator calls: a member operator() receives the +// object as operator-call argument 0 ahead of its declared parameters -- +// for a C++23 static operator() too, whose object argument is still +// evaluated -- while an explicit-object operator() declares the object as +// parameter 0 and maps arguments directly. +struct NowInitStaticFunctor { + [[now_init]] static void operator()(int *p [[ref_to_uninit]]); +}; +struct NowInitStaticOperator { + int m [[uninit]]; + NowInitStaticOperator() { + NowInitStaticFunctor f; + f(&m); + int x = m; // OK: &m bound the static operator()'s marked parameter + (void)x; + } +}; + +struct NowInitExplicitObjectFunctor { + [[now_init]] void operator()(this NowInitExplicitObjectFunctor &, + int *p [[ref_to_uninit]]); +}; +struct NowInitExplicitObjectOperator { + int m [[uninit]]; + NowInitExplicitObjectOperator() { + NowInitExplicitObjectFunctor f; + f(&m); + int x = m; // OK: &m bound the explicit-object operator()'s parameter 1 + (void)x; + } +}; + +// A [[now_init]] call inside a written member initializer credits in +// execution order: the call element precedes the initializer's own write, so +// a body read of the passed member is already covered. +[[now_init]] int now_init_count(int *p [[ref_to_uninit]]); +struct NowInitInMemInit { + int m [[uninit]]; + int n; + NowInitInMemInit() : n(now_init_count(&m)) { + int x = m; // OK: credited at the call inside n's initializer + (void)x; + } +}; + // [[uninit]] members inherited from a non-virtual base with no user-provided // constructor are tracked like the class's own: nothing can have assigned // them before the derived body runs. A base with a user-provided constructor diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index e46a1c8b04be4..595f14ae9427c 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1659,6 +1659,155 @@ struct TmplMemberCredit { }; template struct TmplMemberCredit; +// [[now_init]] (P4222R2 §6.2): binding an entity to a [[ref_to_uninit]] +// parameter of a [[now_init]] function earns the same parse-order credit as +// the equivalent direct store -- the callee initializes the storage it was +// handed. A plain (non-[[now_init]]) callee still earns nothing (the strict +// no-escape-credit doctrine; the paper reserves callee-initialization for +// exactly this annotation). Inside constructors the CFG ctor-body pass has +// its own flow-precise [[now_init]] credit (safety-profile-init-ctor-body.cpp). +[[now_init]] void now_init_fill(int *p [[ref_to_uninit]]); +[[now_init]] void now_init_fill_ref(int &r [[ref_to_uninit]]); +[[now_init]] void now_init_variadic(int *p [[ref_to_uninit]], ...); +void ni_sink(int *); +void ni_cref(const int &); + +void test_now_init_whole_local() { + int u [[uninit]]; + ni_sink(&u); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + now_init_fill(&u); // OK: marked target, uninitialized source + ni_sink(&u); // OK: the callee initialized u + ni_cref(u); // OK + now_init_fill(&u); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// A plain callee taking the same marked parameter earns no credit: the +// binding after it stays the strict error. +void plain_fill(int *p [[ref_to_uninit]]); +void test_plain_callee_no_credit() { + int u [[uninit]]; + plain_fill(&u); + ni_sink(&u); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Passing the marked pointer's *value* is §6.2's initialize2(p) example +// verbatim: the callee initializes the pointee, so whole-`*p` accesses after +// the call are legal -- composed with the existing reseat machinery, which +// clears the credit like any other pointee credit. +void test_now_init_pointee(int *p [[ref_to_uninit]]) { + int before = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + now_init_fill(p); // OK: marked-to-marked binding + int v = *p; // OK: the callee initialized the pointee + ni_sink(p); // OK: p now refers to initialized memory + (void)before; (void)v; +} + +void test_now_init_reseat(int *p [[ref_to_uninit]], int *q [[ref_to_uninit]]) { + now_init_fill(p); + p = q; // reseating clears the pointee credit + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +void test_now_init_reference(int &r [[ref_to_uninit]]) { + now_init_fill_ref(r); // OK: marked reference onward to a marked parameter + int v = r; // OK: the referent is initialized (never lapses) + (void)v; +} + +// Members earn the same per-object credit as a direct member store, under +// the same base-identity keys and boundaries: a member of a parameter- +// reached object stays uncredited (the pinned aliasing boundary). +void test_now_init_member() { + MemberCredit a; + now_init_fill(&a.m); // OK + mc_sink_ptr(&a.m); // OK: (a, m) credited by the callee + mc_sink_ref(a.m); // OK +} +void test_now_init_member_boundary(MemberCredit &r) { + now_init_fill(&r.m); // OK: marked target, uninitialized source + mc_sink_ptr(&r.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// A variadic argument reaches no declared parameter, so it earns nothing +// even from a [[now_init]] callee (and is checked as an unmarked target). +void test_now_init_variadic_no_credit() { + int u [[uninit]]; + int w [[uninit]]; + now_init_variadic(&u, &w); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + ni_sink(&u); // OK: the marked parameter credited u + ni_sink(&w); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// [[now_init]] on a function template: a call to a concrete [[now_init]] +// callee credits at pattern-parse time, so definition-time checks find it; a +// dependent callee defers with the rest of the call and credits per +// instantiation, in the rebuilt parse order. +template +[[now_init]] void now_init_tmpl(T *p [[ref_to_uninit]]); + +template +void template_now_init_nondependent() { + int u [[uninit]]; + now_init_fill(&u); + ni_sink(&u); // OK at definition time and at instantiation +} +template void template_now_init_nondependent(); + +template +void template_now_init_dependent() { + T u [[uninit]]; + now_init_tmpl(&u); + T *s = &u; // OK per instantiation: the rebuilt call credits first + (void)s; +} +template void template_now_init_dependent(); + +// R2 §4.4's now_init() library function works today as a *pure declaration* +// with no compiler support at all: its unmarked return classifies as +// initialized (trusted, §4.3), so the returned pointer launders the storage +// -- the paper's own deliberate profile hole (its `return p;` definition is +// written once, under suppression). What the declaration alone cannot do is +// legalize the *original name* after the call; that is exactly what the §6.2 +// [[now_init]] attribute adds when placed on the same declaration. +template T *now_init(T *p [[ref_to_uninit]]); +template [[now_init]] T *now_init_annotated(T *p [[ref_to_uninit]]); + +void test_now_init_library_pattern() { + int m [[uninit]]; + int v = *now_init(&m); // OK today: the unmarked return is trusted + int *s = now_init(&m); // OK: unmarked pointer from an unmarked return + ni_cref(m); // expected-error {{reference to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)v; (void)s; +} +void test_now_init_library_pattern_annotated() { + int m [[uninit]]; + int *s = now_init_annotated(&m); // OK + ni_cref(m); // OK: the attribute legalizes the original name + (void)s; +} + +// R2 §4.5's requested library annotation, verbatim: construct_at takes a +// [[ref_to_uninit]] pointer and returns a pointer to an initialized object. +// As a [[now_init]]-annotated declaration the lifecycle *start* works: the +// argument's whole object is credited and a repeated construct_at is even +// caught by the reverse-direction rule (a credited source no longer refers +// to uninitialized memory). The rest of the §4.5 lifecycle -- destroy_at, +// use-after-destroy -- remains future work. +template +[[now_init]] T *construct_at(T *p [[ref_to_uninit]], A &&...args); + +struct CtorAtPayload { int x; int y; }; +void test_construct_at_bridge() { + CtorAtPayload s [[uninit]]; + construct_at(&s, 1, 2); + int v = s.x; // OK: construct_at initialized the whole object + construct_at(&s, 3, 4); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + CtorAtPayload t [[uninit]]; + int w = t.x; // expected-error {{read of a subobject of an '[[uninit]]' object accesses uninitialized memory under profile 'std::init'}} + (void)v; (void)w; +} + // The reverse direction applies through the assignment funnel too: a // credited marked pointer refers to initialized memory, so assigning it to // another marked pointer is the requires-uninit error -- while an unmarked diff --git a/clang/test/SemaCXX/safety-profile-now-init-marker.cpp b/clang/test/SemaCXX/safety-profile-now-init-marker.cpp new file mode 100644 index 0000000000000..29ab9b9c5477e --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-now-init-marker.cpp @@ -0,0 +1,66 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// The [[now_init]] marker (P4222R2 §6.2; its exact placement and spelling +// track an open committee question) is recognized regardless of -fprofiles. +// It applies to functions with at least one [[ref_to_uninit]] parameter -- +// the parameters whose storage the callee promises to initialize; with no +// profile enforced it has no effect on those valid subjects. Other +// placements, and a function with no marked parameter (a vacuous promise), +// are rejected regardless of -fprofiles. + +int g; + +[[now_init]] void fill(int *p [[ref_to_uninit]]); +[[now_init]] void fill_ref(int &r [[ref_to_uninit]]); +void fill_after_name [[now_init]] (int *p [[ref_to_uninit]]); +[[now_init]] void fill_many(int n, int *p [[ref_to_uninit]], int *q); +[[now_init]] int *fill_and_return(int *p [[ref_to_uninit]]); + +struct S { + [[now_init]] void fill_member(int *p [[ref_to_uninit]]); + [[now_init]] void fill_out_of_line(int *p [[ref_to_uninit]]); +}; +void S::fill_out_of_line(int *p [[ref_to_uninit]]) {} + +template +[[now_init]] void fill_template(T *p [[ref_to_uninit]]); + +// A dependent parameter's marker is attached to the pattern unvalidated, so +// the vacuity check accepts the template; if instantiation drops the marker +// (T deduced such that T p is not a pointer/reference), the inherited +// [[now_init]] is inert rather than re-diagnosed, like the dropped marker. +template +[[now_init]] void fill_dependent(T p [[ref_to_uninit]]) {} +template void fill_dependent(int *); +template void fill_dependent(int); + +int bad_var [[now_init]]; // expected-error {{'now_init' attribute only applies to functions}} \ + // no-profiles-error {{'now_init' attribute only applies to functions}} + +struct BadField { + int m [[now_init]]; // expected-error {{'now_init' attribute only applies to functions}} \ + // no-profiles-error {{'now_init' attribute only applies to functions}} +}; + +[[now_init]] void bad_no_params(); // expected-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} \ + // no-profiles-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} + +[[now_init]] void bad_unmarked_params(int *p, int &r); // expected-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} \ + // no-profiles-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} + +// The [[ref_to_uninit]] on a non-pointer parameter is itself rejected and +// dropped, leaving the function with no marked parameter: the vacuity error +// fires alongside. +[[now_init]] void bad_dropped_marker(int v [[ref_to_uninit]]); // expected-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // expected-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} \ + // no-profiles-error {{'ref_to_uninit' attribute only applies to pointers, references, and functions returning them}} \ + // no-profiles-error {{'now_init' attribute requires at least one parameter marked '[[ref_to_uninit]]'}} + +// Inert without an enforced profile: a valid placement changes nothing about +// how either run type-checks these calls. +void use() { + int x = 0; + fill(&x); + fill_ref(x); +} From 23c5715665b65867e60515bb9467c11b273a4d1c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 16:03:06 -0400 Subject: [PATCH 256/289] Restrict the new-initializer binding check to scalar allocations --- clang/include/clang/Sema/SemaProfiles.h | 4 +++- clang/lib/Sema/SemaExprCXX.cpp | 16 +++++++++++----- clang/lib/Sema/SemaProfiles.cpp | 10 +++++++--- .../safety-profile-init-ref-to-uninit.cpp | 18 ++++++++++++++++++ 4 files changed, 39 insertions(+), 9 deletions(-) diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 6cdcb956caa63..32b4ba84bc5c8 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -475,7 +475,9 @@ class SemaProfiles : public SemaBase { /// allocated pointer binds it like a variable initialization, and a heap /// pointer object cannot carry [[ref_to_uninit]]. \p Init is the single /// written initializer expression, or null when there is none (a no-op). - /// Hosts the cluster from Sema::BuildCXXNew. An instantiation-dependent + /// Hosts the cluster from Sema::BuildCXXNew, which calls it for scalar + /// allocations only: an array new's written elements are each checked by + /// the aggregate element hooks instead. An instantiation-dependent /// allocated type defers to the instantiation rebuild. void checkInitProfileNewInitializer(QualType AllocType, Expr *Init); diff --git a/clang/lib/Sema/SemaExprCXX.cpp b/clang/lib/Sema/SemaExprCXX.cpp index 433c6200c8e4a..bfc828dd81ceb 100644 --- a/clang/lib/Sema/SemaExprCXX.cpp +++ b/clang/lib/Sema/SemaExprCXX.cpp @@ -2616,11 +2616,17 @@ ExprResult Sema::BuildCXXNew(SourceRange Range, bool UseGlobal, Initializer = FullInit.get(); // std::init / ref_to_uninit (paper §5): a written initializer for an - // allocated pointer binds it like a variable initialization. The - // Decl-less wrapper defers only on an instantiation-dependent allocated - // type or initializer, rebuilt at instantiation; a non-dependent - // new-expression is checked at definition time. - if (getLangOpts().Profiles) + // allocated pointer binds it like a variable initialization. Scalar + // allocations only: for an array new AllocType is the *element* type, + // and each written element -- including the lone initializer of + // `new T*[k]{p}` or `new T*[k](p)` -- is already checked by the + // aggregate element hooks (InitListChecker::CheckSubElementType and + // TryOrBuildParenListInitialization), so checking it here too would + // diagnose it twice. The Decl-less wrapper defers only on an + // instantiation-dependent allocated type or initializer, rebuilt at + // instantiation; a non-dependent new-expression is checked at + // definition time. + if (getLangOpts().Profiles && !ArraySize) Profiles().checkInitProfileNewInitializer( AllocType, Exprs.size() == 1 ? Exprs[0] : nullptr); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 5196d48af8f49..ef67375285cba 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1746,9 +1746,13 @@ void SemaProfiles::checkInitProfileNewInitializer(QualType AllocType, // [[ref_to_uninit]], so binding it to uninitialized memory is always the // unmarked-direction violation. A braced `new T*{&x}` presents the // InitListExpr, which the recognizer's single-element pass-through looks - // through. An instantiation-dependent allocated type (note that a - // dependent-pointee `T*` still passes isPointerType) defers to the - // instantiation rebuild, which re-runs this check with the concrete type. + // through -- which is why the caller invokes this for scalar allocations + // only: for an array new, AllocType is the element type and the lone + // initializer of `new T*[k]{&x}` would be peeled to the same binding the + // aggregate element hooks already diagnose. An instantiation-dependent + // allocated type (note that a dependent-pointee `T*` still passes + // isPointerType) defers to the instantiation rebuild, which re-runs this + // check with the concrete type. if (!AllocType->isPointerType() || AllocType->isInstantiationDependentType() || !Init) return; diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 595f14ae9427c..3a3d565d3f184 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -2229,6 +2229,24 @@ void test_variable_braced_once() { (void)p; } +// An array new-expression's pointer elements are the same unmarked element +// bindings, for constant and variable bounds alike. The scalar +// new-initializer check stays out of array news -- a lone initializer (a +// one-element braced list, peeled by the single-element pass-through, or a +// single C++20 paren argument) binds the first *element*, already checked by +// the element hooks -- so each violation fires exactly once. Scalar +// allocations keep their single variable-like check. +void test_array_new_element(int n) { + (void)new int *[2]{&g_uninit, &g_init}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[2]{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[1](&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[n]{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[n](&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *[2]{}; // OK: value-initialized null elements + (void)new int *(&g_uninit); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)new int *{&g_uninit}; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + // std::init / ref_to_uninit (paper §5): a source whose syntactic form the // recognizer does not model -- pointer arithmetic, an integer-to-pointer cast, // or a call through a function pointer -- is unknown, not initialized. A marked From 51e54ed8cfa7a955bb838eb6a5b71f0237192c6c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Wed, 15 Jul 2026 16:17:53 -0400 Subject: [PATCH 257/289] Key member store credit on the parse-time pattern function --- clang/docs/ProfilesFramework.rst | 11 ++- clang/include/clang/Sema/SemaProfiles.h | 32 ++++---- clang/lib/Sema/SemaProfiles.cpp | 74 +++++++++++++++++-- .../safety-profile-init-ref-to-uninit.cpp | 46 ++++++++++++ 4 files changed, 140 insertions(+), 23 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 80d8f1eb5d8a5..f49d6a57df883 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -478,7 +478,9 @@ call on uninitialized storage, rejected as above. Whole-member stores are credited too, keyed per base object: after ``a.m = 5;`` on a directly named local, or ``this->m = 5;`` on the current object (keyed to the enclosing function body, so no other function -- nor a -lambda body, which is its own function -- shares it), binding ``a.m`` or +lambda body, which is its own function -- shares it; instantiations of one +template or generic lambda share their pattern's single body, and its +credit), binding ``a.m`` or ``&a.m`` through the *same* base is accepted, and the reverse direction applies as for locals. The boundaries: one member level only (``x.agg.m = 5;`` is itself the piecemeal-initialization error and earns nothing), only @@ -618,7 +620,12 @@ false positive. (or inside a lambda body) credits every later use in parse order, so a read or binding on a path that skips the store is a missed diagnostic. ``[[now_init]]`` call credit outside constructor bodies is parse-order in - the same way (inside them it is a real dataflow fact, as above). + the same way (inside them it is a real dataflow fact, as above). The + requires-uninitialized direction consults this credit at definition time + only -- an instantiation re-walk does not rewind parse-order state, so + re-checked statements must not trip over credit they themselves recorded + -- which makes a reverse-direction violation established only by credit + in fully dependent code a missed diagnostic as well. - In a template, a violation in non-dependent code is diagnosed at definition time and may be repeated at instantiation. diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 32b4ba84bc5c8..60336d10af671 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -426,14 +426,16 @@ class SemaProfiles : public SemaBase { bool hasMemberStoreCredit(const Decl *Base, const FieldDecl *F) const; /// Resolve the identity key of a member access's base object for the - /// per-object member store credit: the enclosing function declaration for - /// a current-object access (this->m / m / (*this).m) -- so credit recorded - /// in one function body can never satisfy a binding in another -- or the - /// directly named local-storage, non-reference VarDecl of a dot access - /// (a.m). Any other base -- another member (a.b.m; §5.4 rejects deep - /// delayed-initialization tracking), an arrow through an arbitrary pointer - /// value, a reference (an alias to an object also reachable other ways) -- - /// is untrackable per object: null. + /// per-object member store credit: the parse-time pattern of the enclosing + /// function declaration for a current-object access (this->m / m / + /// (*this).m) -- so credit recorded in one function body can never satisfy + /// a binding in another, while a statement an instantiation reuses + /// (unrebuilt) from its template or generic-lambda pattern agrees with a + /// rebuilt one on the key -- or the directly named local-storage, + /// non-reference VarDecl of a dot access (a.m). Any other base -- another + /// member (a.b.m; §5.4 rejects deep delayed-initialization tracking), an + /// arrow through an arbitrary pointer value, a reference (an alias to an + /// object also reachable other ways) -- is untrackable per object: null. const Decl *resolveMemberStoreBase(const MemberExpr *ME) const; /// Store-credit bits for recordInitProfileStore. @@ -454,12 +456,14 @@ class SemaProfiles : public SemaBase { /// Parse-order whole-member store credit, keyed per base object: the base /// is the directly named local-storage VarDecl (a.m = e) or, for the - /// current object (this->m = e / m = e), the enclosing function - /// declaration -- so credit recorded in one function body can never - /// satisfy a binding in another, and two locals of the same type never - /// share credit. Only WholeStored is ever set: member *pointee* stores - /// (*a.p = e) are deliberately never credited -- per-object pointee - /// aliasing (copies share pointees) makes them unsound to approximate. + /// current object (this->m = e / m = e), the parse-time pattern of the + /// enclosing function declaration -- so credit recorded in one function + /// body can never satisfy a binding in another, two locals of the same + /// type never share credit, and instantiations agree with their pattern + /// on statements they reuse from it (see resolveMemberStoreBase). Only + /// WholeStored is ever set: member *pointee* stores (*a.p = e) are + /// deliberately never credited -- per-object pointee aliasing (copies + /// share pointees) makes them unsound to approximate. /// Never cleared, for the same reasons as InitStoreCredit (instantiations /// key on fresh field and function declarations). llvm::DenseMap, unsigned> diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index ef67375285cba..9122f51dfa334 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -942,16 +942,59 @@ static bool isCurrentObjectExpr(const Expr *E) { isa(UO->getSubExpr()->IgnoreParenImpCasts()); } +// The parse-time pattern of \p FD -- the declaration whose body statements +// the current function can share. TreeTransform hands a statement back +// *unchanged* when nothing in it needs rebuilding, so a fully non-dependent +// `this->m = 5` inside a generic lambda (or a lambda in a member function +// template) runs Sema -- and earns its parse-order credit -- only once, +// while the pattern is parsed; the instantiated call operator reuses the +// statement wholesale. Keying current-object member credit on the pattern +// makes record and consult agree whether a statement was reused +// (pattern-time credit) or rebuilt (the instantiation-time key normalizes +// to the same pattern). Iterated because each transform hop adds one link: +// a generic lambda in a member function template reaches its parsed pattern +// via a member-specialization link and then a primary-template link (a +// local twin of SemaLambda.cpp's getPatternFunctionDecl). Instantiations of +// one pattern share its key -- and its statements, so the parse order the +// credit approximates is the same for all of them; a store only one +// sibling instantiation rebuilds (e.g. under a dependent `if constexpr`) +// then credits the others too, a parse-order-style missed diagnostic, never +// a false positive. +static const FunctionDecl *getParseTimePattern(const FunctionDecl *FD) { + while (FD) { + // A local function without template machinery of its own, instantiated + // while transforming an enclosing templated body. (A transformed lambda + // call operator instead carries a member-specialization link and a + // generic lambda's specialization a primary-template link -- both + // resolved by the pattern walk below.) + if (FD->getTemplatedKind() == FunctionDecl::TK_DependentNonTemplate) { + const FunctionDecl *P = FD->getInstantiatedFromDecl(); + if (!P) + return FD; + FD = P; + continue; + } + const FunctionDecl *P = FD->getTemplateInstantiationPattern(); + if (!P || P == FD) + return FD; + FD = P; + } + return FD; +} + const Decl *SemaProfiles::resolveMemberStoreBase(const MemberExpr *ME) const { const Expr *Base = ME->getBase()->IgnoreParenImpCasts(); // The current object: this->m, the implicit m, or (*this).m. Keyed on the - // enclosing function declaration (`this` cannot be reseated, so the key is - // stable for the whole body); AllowLambda gives a lambda body inside a - // member function its own key, so its stores and the enclosing function's - // never share credit. No current function (e.g. an NSDMI parse) is - // untrackable. + // enclosing function declaration's parse-time pattern (`this` cannot be + // reseated, so the key is stable for the whole body; the *pattern*, so + // that a statement an instantiation reuses from its pattern and a rebuilt + // one agree on the key -- see getParseTimePattern); AllowLambda gives a + // lambda body inside a member function its own key, so its stores and the + // enclosing function's never share credit. No current function (e.g. an + // NSDMI parse) is untrackable. if (isCurrentObjectExpr(Base)) - return SemaRef.getCurFunctionDecl(/*AllowLambda=*/true); + return getParseTimePattern( + SemaRef.getCurFunctionDecl(/*AllowLambda=*/true)); // A directly named local object: a dot access on a local-storage, // non-reference VarDecl (a by-value parameter is its own object and // qualifies). A reference base is an alias to an object also reachable @@ -1281,8 +1324,25 @@ void SemaProfiles::checkInitProfileRefToUninit(SourceLocation Loc, static constexpr StringRef Rule = "ref_to_uninit"; if (!shouldEmitProfileViolation(Profile, Rule, Loc, D)) return; + // Parse-order credit is recorded once and never rewound, so when an + // instantiation re-walks a statement the pattern already checked, the + // re-check runs against post-pattern state -- including credit this very + // statement recorded (a reused [[now_init]] argument, a this-member store + // keyed to the pattern). For the requires-uninit direction that would turn + // the pattern's pass into a false "must refer to uninitialized memory" at + // instantiation, so a marked target classifies without credit while + // instantiating: the reverse direction diagnoses at definition time, and a + // violation established only by credit in fully dependent code is a missed + // diagnostic -- the usual parse-order trade, never a false positive. + // Direct classification (an initialized global, a marked entity) is + // unaffected, so credit-free reverse violations still repeat per + // instantiation, and the accepting direction keeps credit everywhere (a + // deferred binding needs the instantiation-time record to pass). UninitStorage SrcState = classifyUninitSource( - getASTContext(), Src, IsReference, UninitBindAccess.withCredit(this)); + getASTContext(), Src, IsReference, + TargetIsRefToUninit && SemaRef.inTemplateInstantiation() + ? UninitBindAccess + : UninitBindAccess.withCredit(this)); unsigned IsRef = IsReference ? 1 : 0; // A marked target is a violation only against an affirmatively Initialized // source: an Unknown one (pointer arithmetic, an integer-to-pointer cast, a diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 3a3d565d3f184..0840fe360b65b 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1659,6 +1659,38 @@ struct TmplMemberCredit { }; template struct TmplMemberCredit; +// TreeTransform hands a fully non-dependent statement back *unchanged* when +// instantiating a body, so `m = 5` inside a generic lambda runs Sema (and +// records its credit) only at the pattern parse. The current-object key is +// the function's parse-time pattern on record and consult alike, so the +// instantiated call operators' checks still find that credit -- while the +// per-function isolation above is unchanged (different functions have +// different patterns). +struct GenericLambdaMemberCredit { + int m [[uninit]]; + void go() { + auto l = [this](auto) { + m = 5; + mc_sink_ref(m); // OK: credited at the pattern parse and per instantiation + mc_sink_ptr(&m); // OK + }; + l(1); + l(2L); + } +}; + +// The same statement reuse through a non-generic lambda in a member function +// template: the transformed call operator reaches its parsed pattern through +// a member-specialization link. +struct LambdaInMemberTemplateCredit { + int m [[uninit]]; + template void go() { + auto l = [this] { m = 5; mc_sink_ref(m); }; // OK + l(); + } +}; +template void LambdaInMemberTemplateCredit::go(); + // [[now_init]] (P4222R2 §6.2): binding an entity to a [[ref_to_uninit]] // parameter of a [[now_init]] function earns the same parse-order credit as // the equivalent direct store -- the callee initializes the storage it was @@ -1729,6 +1761,20 @@ void test_now_init_member_boundary(MemberCredit &r) { mc_sink_ptr(&r.m); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} } +// The callee credit survives statement reuse in instantiated lambda bodies +// exactly like a direct store (the current-object key is the parse-time +// pattern; see GenericLambdaMemberCredit above). +struct GenericLambdaNowInitMemberCredit { + int m [[uninit]]; + void go() { + auto l = [this](auto) { + now_init_fill(&m); + mc_sink_ptr(&m); // OK: credited at the pattern parse and per instantiation + }; + l(1); + } +}; + // A variadic argument reaches no declared parameter, so it earns nothing // even from a [[now_init]] callee (and is checked as an unmarked target). void test_now_init_variadic_no_credit() { From eebb4a6ed623c42b0bf3fadc7b3d070d666343b9 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 21:57:22 -0400 Subject: [PATCH 258/289] Make -fprofiles a Compatible language option --- clang/docs/ProfilesFramework.rst | 8 +++++ clang/include/clang/Basic/LangOptions.def | 2 +- .../safety-profile-framework-modules.cppm | 36 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index f49d6a57df883..6b6621e6233de 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -158,6 +158,14 @@ same way: an ``[[profiles::enforce(...)]];`` empty-declaration in the header is exported by the corresponding header unit and validated by ``[[profiles::require]]`` on its import. +``-fprofiles`` does not have to be uniform across a build: a module built +without the flag imports fine into a profiles-enabled compile -- it simply +advertises no profiles, so a ``[[profiles::require]]`` on the import reports +the profile as not enforced -- and an enforcing module loads fine into a +compile with the feature off. A PCH is stricter (like other compatible +language options, it must be built with the same ``-fprofiles`` setting as +its consumer). + Enforcement on a module interface extends to the module's implementation units: diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def index 1b991275d6f7c..5f797e1c6e5c8 100644 --- a/clang/include/clang/Basic/LangOptions.def +++ b/clang/include/clang/Basic/LangOptions.def @@ -154,7 +154,7 @@ LANGOPT(EmitAllDecls , 1, 0, Benign, "emitting all declarations") LANGOPT(MathErrno , 1, 1, NotCompatible, "errno in math functions") LANGOPT(Modules , 1, 0, NotCompatible, "modules semantics") LANGOPT(CPlusPlusModules , 1, 0, Compatible, "C++ modules syntax") -LANGOPT(Profiles , 1, 0, NotCompatible, "C++ profiles framework") +LANGOPT(Profiles , 1, 0, Compatible, "C++ profiles framework") LANGOPT(ProfilesTestProfiles, 1, 0, Benign, "C++ profiles framework built-in test:: profiles") LANGOPT(SkipODRCheckInGMF , 1, 0, NotCompatible, "Skip ODR checks for decls in the global module fragment") LANGOPT(BuiltinHeadersInSystemModules, 1, 0, NotCompatible, "builtin headers belong to system modules, and _Builtin_ modules are ignored for cstdlib headers") diff --git a/clang/test/SemaCXX/safety-profile-framework-modules.cppm b/clang/test/SemaCXX/safety-profile-framework-modules.cppm index 9d0b263c15c4a..7e502d40db1e2 100644 --- a/clang/test/SemaCXX/safety-profile-framework-modules.cppm +++ b/clang/test/SemaCXX/safety-profile-framework-modules.cppm @@ -26,6 +26,9 @@ // RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/mod_noflag_enforce.cppm -verify // RUN: %clang_cc1 -std=c++20 -emit-module-interface %t/mod_bare.cppm -o %t/mod_bare.pcm -verify // RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_noflag_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_mixed_require.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fprofiles -fprofiles-test-profiles -fsyntax-only %t/import_mixed_local_enforce.cpp -fmodule-file=BareMod=%t/mod_bare.pcm -verify +// RUN: %clang_cc1 -std=c++20 -fsyntax-only %t/import_mixed_noflag.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/mod_enforce_no_args.cppm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/import_require_no_args.cpp -fmodule-file=TestMod=%t/mod_enforced.pcm -verify // RUN: %clang_cc1 -std=c++20 -fprofiles -fsyntax-only %t/unknown_attr_mod.cppm -verify @@ -270,6 +273,39 @@ export void bare_fn(); //--- import_noflag_require.cpp import BareMod [[profiles::require(test::type_cast)]]; // expected-warning {{'profiles::require' attribute ignored}} +// =================================================================== +// -fprofiles is a Compatible language option: a BMI built without it +// loads into a -fprofiles compile instead of being rejected as a +// configuration mismatch (P3589R2's gradual-adoption premise). The +// no-flag BMI advertises no profiles, so require gets its ordinary +// not-enforced answer rather than a load error. +// =================================================================== +//--- import_mixed_require.cpp +import BareMod [[profiles::require(test::type_cast)]]; // expected-error {{required profile 'test::type_cast' is not enforced by imported module}} + +// =================================================================== +// Local enforcement still works across a mixed-mode import. +// =================================================================== +//--- import_mixed_local_enforce.cpp +[[profiles::enforce(test::type_cast)]]; +import BareMod; + +void mixed_local_func() { + int *p = reinterpret_cast(0); // expected-error {{'reinterpret_cast' is unsafe under profile 'test::type_cast'}} +} + +// =================================================================== +// The reverse mix: a BMI built WITH -fprofiles loads into a compile +// without the flag; require is ignored (flag off) and the module's +// enforcement does not leak into the importer. +// =================================================================== +//--- import_mixed_noflag.cpp +import TestMod [[profiles::require(test::type_cast)]]; // expected-warning {{'profiles::require' attribute ignored}} + +void mixed_noflag_func() { + int *p = reinterpret_cast(0); +} + // =================================================================== // [[profiles::enforce]] on a module-declaration with no argument // clause must be diagnosed, not crash. From 5d982b29b514c81513ffb825fe2bc996b3922132 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:03:06 -0400 Subject: [PATCH 259/289] Classify known C allocator callees in the uninit recognizers --- clang/docs/ProfilesFramework.rst | 10 +- clang/include/clang/Sema/SemaProfiles.h | 4 +- clang/lib/Sema/SemaProfiles.cpp | 49 ++++++++-- .../safety-profile-init-allocators.cpp | 94 +++++++++++++++++++ 4 files changed, 144 insertions(+), 13 deletions(-) create mode 100644 clang/test/SemaCXX/safety-profile-init-allocators.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 6b6621e6233de..c4a858929f430 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -422,9 +422,13 @@ memory, and an unmarked one only to initialized memory. Whether a source refers to uninitialized memory is recognized from its form -- the address or a subobject of an ``[[uninit]]`` entity, the value or dereference of a marked pointer or reference, pointer and reference casts of those, a call to -a marked function, and a ``new`` expression that default-initializes a type -with indeterminate scalars (``new int``, ``new int[n]``; §1.2) -- refined by -one parse-order fact, whole-entity stores (below): +a marked function, a call to a known allocator (``malloc``, +``aligned_alloc``, and ``alloca`` return uninitialized memory, ``calloc`` +zero-initialized memory, and ``realloc`` is unclassified; §4.3 -- keyed on +Clang's builtin recognition, which ``-fno-builtin`` disables), and a ``new`` +expression that default-initializes a type with indeterminate scalars +(``new int``, ``new int[n]``; §1.2) -- refined by one parse-order fact, +whole-entity stores (below): .. code-block:: c++ diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 60336d10af671..fe5ce8e87d0fc 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -229,7 +229,9 @@ class SemaProfiles : public SemaBase { /// [[ref_to_uninit]] pointer/reference or array; a dereference of such a /// pointer; a cast of such a pointer to another pointer type, or of such a /// glvalue to another reference; a call to a [[ref_to_uninit]]-returning - /// function; or a new-expression whose default-initialization leaves the + /// function or to a known uninitialized-returning allocator (the malloc + /// and alloca builtin families; calloc's result is initialized, realloc's + /// unknown); or a new-expression whose default-initialization leaves the /// allocated object indeterminate (e.g. new int). A trusted-initialized /// source and an unrecognized (unknown) source both return false (no flow /// analysis). diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 9122f51dfa334..f38d9a04ced1a 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -20,6 +20,7 @@ #include "clang/AST/ExprCXX.h" #include "clang/AST/ParentMap.h" #include "clang/Analysis/AnalysisDeclContext.h" +#include "clang/Basic/Builtins.h" #include "clang/Basic/Module.h" #include "clang/Basic/SourceManager.h" #include "clang/Sema/Attr.h" @@ -1054,18 +1055,48 @@ classifyUninitPassThrough(const Expr *E, UninitStorage EmptyListState, // A call to a [[ref_to_uninit]]-returning function yields uninitialized // storage (the pointed-to memory, or the returned referent) -- deferred to -// Unknown when the marker is trusted (a store). An unmarked direct callee is -// trusted Initialized (paper §4.3); a call with no direct callee (through a -// function pointer) is Unknown. Shared by both recognizers. +// Unknown when the marker is trusted (a store). Known allocator callees are +// classified the same way without a marker (paper §4.3: functions like +// malloc "must be known to an analyzer enforcing the initialization +// profile"): the malloc and alloca families return uninitialized memory, +// calloc returns zero-initialized memory, and realloc returns a preserved +// prefix plus an indeterminate tail -- affirmatively neither, so Unknown. +// The builtin ID is absent under -fno-builtin / -ffreestanding (or a +// non-matching declaration), where an allocator falls back to the trusted +// default below -- a missed diagnostic, never a false positive. Any other +// unmarked direct callee is trusted Initialized (paper §4.3); a call with +// no direct callee (through a function pointer) is Unknown. Shared by both +// recognizers. static UninitStorage classifyRefToUninitCallee(const CallExpr *CE, UninitAccessOpts Opts) { - if (const FunctionDecl *FD = CE->getDirectCallee()) { - if (!FD->hasAttr()) - return UninitStorage::Initialized; - return Opts.TrustRefToUninit ? UninitStorage::Unknown - : UninitStorage::Uninitialized; + const FunctionDecl *FD = CE->getDirectCallee(); + if (!FD) + return UninitStorage::Unknown; + bool RefersToUninit = FD->hasAttr(); + switch (FD->getBuiltinID()) { + case Builtin::BImalloc: + case Builtin::BI__builtin_malloc: + case Builtin::BIaligned_alloc: + case Builtin::BIalloca: + case Builtin::BI__builtin_alloca: + case Builtin::BI__builtin_alloca_uninitialized: + case Builtin::BI__builtin_alloca_with_align: + case Builtin::BI__builtin_alloca_with_align_uninitialized: + RefersToUninit = true; + break; + case Builtin::BIcalloc: + case Builtin::BI__builtin_calloc: + return UninitStorage::Initialized; + case Builtin::BIrealloc: + case Builtin::BI__builtin_realloc: + return UninitStorage::Unknown; + default: + break; } - return UninitStorage::Unknown; + if (!RefersToUninit) + return UninitStorage::Initialized; + return Opts.TrustRefToUninit ? UninitStorage::Unknown + : UninitStorage::Uninitialized; } // \p E is a pointer prvalue. Classifies whether it points to uninitialized diff --git a/clang/test/SemaCXX/safety-profile-init-allocators.cpp b/clang/test/SemaCXX/safety-profile-init-allocators.cpp new file mode 100644 index 0000000000000..3fe4e48f352d6 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-allocators.cpp @@ -0,0 +1,94 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// std::init: known allocator callees are classified without annotation +// (paper §4.3: malloc returns a pointer to uninitialized memory, calloc to +// zero-initialized memory, and such functions "must be known to an analyzer +// enforcing the initialization profile"). The knowledge keys on Clang's +// builtin recognition, so the library functions are declared with matching +// signatures here; -fno-builtin would fall back to the trusted-initialized +// default (a missed diagnostic, never a false positive). + +// no-profiles-warning@+1 {{'profiles::enforce' attribute ignored}} +[[profiles::enforce(std::init)]]; + +typedef __SIZE_TYPE__ size_t; +extern "C" void *malloc(size_t); +extern "C" void *calloc(size_t, size_t); +extern "C" void *realloc(void *, size_t); +extern "C" void *aligned_alloc(size_t, size_t); + +void take_uninit_ptr(int *p [[ref_to_uninit]]); +void take_ptr(int *p); + +// malloc returns uninitialized memory: a marked target accepts it, and an +// unmarked one must not bind it (paper §4.3's `void* p2 = &x2` error). +void test_malloc() { + int *p [[ref_to_uninit]] = (int *)malloc(4); // OK: the paper's canonical use + void *v [[ref_to_uninit]] = malloc(4); // OK: no cast needed for void* + int *q = (int *)malloc(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + take_uninit_ptr((int *)malloc(4)); // OK + take_ptr((int *)malloc(4)); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// The __builtin_ spellings need no declaration and classify identically. +void test_builtin_spellings() { + int *p [[ref_to_uninit]] = (int *)__builtin_malloc(4); // OK + int *q = (int *)__builtin_malloc(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r = (int *)__builtin_calloc(1, 4); // OK: zero-initialized +} + +// calloc zero-initializes (paper §4.3), so its result is initialized memory +// and the marked direction flips. +void test_calloc() { + int *p = (int *)calloc(1, 4); // OK + int *q [[ref_to_uninit]] = (int *)calloc(1, 4); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} +} + +// realloc preserves a prefix and leaves the tail indeterminate: +// affirmatively neither initialized nor uninitialized, so neither binding +// direction diagnoses. +void test_realloc(void *v) { + int *p = (int *)realloc(v, 8); // OK: unknown + int *q [[ref_to_uninit]] = (int *)realloc(v, 8); // OK: unknown +} + +void test_aligned_alloc() { + int *p [[ref_to_uninit]] = (int *)aligned_alloc(16, 16); // OK + int *q = (int *)aligned_alloc(16, 16); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// alloca's stack memory is as uninitialized as malloc's heap memory. +void test_alloca() { + int *p [[ref_to_uninit]] = (int *)__builtin_alloca(4); // OK + int *q = (int *)__builtin_alloca(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +// Allocator sources compose with the existing machinery: a write through the +// marked pointer is the pointee's initialization (with parse-order credit), +// and a read before it is the read-through violation. +void test_write_then_read() { + int *p [[ref_to_uninit]] = (int *)malloc(4); + *p = 5; // OK: initializes the pointee + int x = *p; // OK: credited +} + +void test_read_through() { + int *p [[ref_to_uninit]] = (int *)malloc(4); + int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} +} + +// Suppression covers the binding like any other ref_to_uninit site. +void test_suppress() { + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ref_to_uninit")]] + int *q = (int *)malloc(4); // OK: suppressed +} + +// A Decl-less binding of a non-dependent allocator call (a call argument) is +// checked at definition time even in a never-instantiated template, like the +// other all-non-dependent shapes (TreeTransform may reuse the node). +template +void template_malloc_arg() { + take_ptr((int *)malloc(4)); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} From 10c1b232451a91b0ef5cdff3c3951b69ea782bdb Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:06:11 -0400 Subject: [PATCH 260/289] Classify raw operator new callees as uninitialized sources --- clang/docs/ProfilesFramework.rst | 7 ++++--- clang/include/clang/Sema/SemaProfiles.h | 3 ++- clang/lib/Sema/SemaProfiles.cpp | 9 +++++++++ .../safety-profile-init-allocators.cpp | 20 +++++++++++++++++++ 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index c4a858929f430..6bc1ac20a265d 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -423,9 +423,10 @@ refers to uninitialized memory is recognized from its form -- the address or a subobject of an ``[[uninit]]`` entity, the value or dereference of a marked pointer or reference, pointer and reference casts of those, a call to a marked function, a call to a known allocator (``malloc``, -``aligned_alloc``, and ``alloca`` return uninitialized memory, ``calloc`` -zero-initialized memory, and ``realloc`` is unclassified; §4.3 -- keyed on -Clang's builtin recognition, which ``-fno-builtin`` disables), and a ``new`` +``aligned_alloc``, ``alloca``, and raw ``::operator new`` calls return +uninitialized memory, ``calloc`` zero-initialized memory, and ``realloc`` is +unclassified; §4.3 -- keyed on Clang's builtin recognition, which +``-fno-builtin`` disables), and a ``new`` expression that default-initializes a type with indeterminate scalars (``new int``, ``new int[n]``; §1.2) -- refined by one parse-order fact, whole-entity stores (below): diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index fe5ce8e87d0fc..8d3a571dbe41e 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -230,7 +230,8 @@ class SemaProfiles : public SemaBase { /// pointer; a cast of such a pointer to another pointer type, or of such a /// glvalue to another reference; a call to a [[ref_to_uninit]]-returning /// function or to a known uninitialized-returning allocator (the malloc - /// and alloca builtin families; calloc's result is initialized, realloc's + /// and alloca builtin families and raw replaceable ::operator new calls; + /// calloc's result is initialized, realloc's /// unknown); or a new-expression whose default-initialization leaves the /// allocated object indeterminate (e.g. new int). A trusted-initialized /// source and an unrecognized (unknown) source both return false (no flow diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index f38d9a04ced1a..8b48cbb9c5317 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1073,6 +1073,14 @@ static UninitStorage classifyRefToUninitCallee(const CallExpr *CE, if (!FD) return UninitStorage::Unknown; bool RefersToUninit = FD->hasAttr(); + // A direct call to a replaceable global allocation function -- raw + // ::operator new / ::operator new[] -- returns uninitialized memory like + // malloc (a new-*expression* is the recognizers' CXXNewExpr arm instead). + // The operator check excludes operator delete, and replaceability excludes + // class-specific overloads, whose semantics belong to their class. + if (FD->getDeclName().getCXXOverloadedOperator() == OO_New || + FD->getDeclName().getCXXOverloadedOperator() == OO_Array_New) + RefersToUninit |= FD->isReplaceableGlobalAllocationFunction(); switch (FD->getBuiltinID()) { case Builtin::BImalloc: case Builtin::BI__builtin_malloc: @@ -1082,6 +1090,7 @@ static UninitStorage classifyRefToUninitCallee(const CallExpr *CE, case Builtin::BI__builtin_alloca_uninitialized: case Builtin::BI__builtin_alloca_with_align: case Builtin::BI__builtin_alloca_with_align_uninitialized: + case Builtin::BI__builtin_operator_new: RefersToUninit = true; break; case Builtin::BIcalloc: diff --git a/clang/test/SemaCXX/safety-profile-init-allocators.cpp b/clang/test/SemaCXX/safety-profile-init-allocators.cpp index 3fe4e48f352d6..d57297d724f79 100644 --- a/clang/test/SemaCXX/safety-profile-init-allocators.cpp +++ b/clang/test/SemaCXX/safety-profile-init-allocators.cpp @@ -78,6 +78,26 @@ void test_read_through() { int x = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} } +// A direct call to a replaceable global allocation function returns +// uninitialized memory exactly like malloc (a new-*expression* is recognized +// separately, from its initialization style). +void test_operator_new() { + int *p [[ref_to_uninit]] = (int *)::operator new(4); // OK + int *q = (int *)::operator new(4); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r = (int *)::operator new[](8); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *s [[ref_to_uninit]] = (int *)__builtin_operator_new(4); // OK +} + +// A class-specific operator new is not replaceable; its semantics belong to +// its class, so it stays a trusted unmarked callee. +struct PoolAllocated { + static void *operator new(size_t); +}; +void test_class_specific_operator_new() { + int *p [[ref_to_uninit]] = (int *)PoolAllocated::operator new(4); // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + int *q = (int *)PoolAllocated::operator new(4); // OK: trusted +} + // Suppression covers the binding like any other ref_to_uninit site. void test_suppress() { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} From 33e0ba0ecfaf312f92695069fcac43456ce9cf0c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:09:44 -0400 Subject: [PATCH 261/289] Check anonymous-struct members in ctor_uninit_member --- clang/docs/ProfilesFramework.rst | 4 +- clang/lib/Sema/SemaProfiles.cpp | 68 +++++++++++++------ .../test/SemaCXX/safety-profile-init-ctor.cpp | 60 ++++++++++++++++ 3 files changed, 110 insertions(+), 22 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 6bc1ac20a265d..0b547c4903b07 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -554,7 +554,9 @@ initializer or by the base's own user-provided default constructor: }; A member whose type's default-initialization leaves unacknowledged scalars -indeterminate (a nested aggregate) is flagged the same way. A delegating +indeterminate (a nested aggregate) is flagged the same way, and the members +of an anonymous struct are checked exactly like direct members (a written +initializer for one is an indirect member-initializer). A delegating constructor is exempt -- its target initializes the members -- and so is a union's own constructor, whose members are mutually exclusive (§5.6). diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 8b48cbb9c5317..d8d401eb88068 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1890,6 +1890,52 @@ void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { << "test::ctor_final" << Ctor->getParent(); } +// The per-field walk of the ctor_uninit_member callback: check every +// checkable field of \p RD against \p Written, recursing into anonymous +// struct members -- their leaves initialize exactly like direct members of +// the constructor's class (a written initializer for one is an *indirect* +// member-initializer, which the Written collection already resolved to the +// leaf FieldDecl via getAnyMember, and NSDMIs and [[uninit]] markers sit on +// the leaves), so the same per-field logic and diagnostic apply to them. +static void diagnoseCtorUninitFields( + Sema &S, const CXXConstructorDecl *Ctor, const CXXRecordDecl *RD, + const llvm::SmallPtrSetImpl &Written) { + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField()) + continue; + if (F->isAnonymousStructOrUnion()) { + const CXXRecordDecl *AnonRD = F->getType()->getAsCXXRecordDecl(); + if (!AnonRD || !AnonRD->hasDefinition() || AnonRD->isInvalidDecl()) + continue; + // An anonymous union's members are mutually exclusive -- one active + // member satisfies it -- so the every-member walk does not apply. + if (AnonRD->isUnion()) + continue; + diagnoseCtorUninitFields(S, Ctor, AnonRD->getDefinition(), Written); + continue; + } + // Other unnamed fields are skipped; a named bit-field is checked like + // any other member. Reference and const members already have dedicated + // diagnostics when left uninitialized. + if (!F->getDeclName() || F->getType()->isReferenceType() || + F->getType().isConstQualified()) + continue; + if (F->hasAttr() || F->hasInClassInitializer() || + Written.count(F)) + continue; + if (!S.Profiles().defaultInitLeavesScalarIndeterminate( + F->getType(), /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) + << "std::init" << F->getDeclName(); + S.Diag(F->getLocation(), diag::note_init_uninit_member_here) + << F->getDeclName(); + } +} + void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { // Paper §6.1: a user-provided constructor must initialize every member via // its member-initializer list or an NSDMI, unless the member is marked @@ -1920,27 +1966,7 @@ void runStdInitCtorUninitMemberCallback(Sema &S, CXXConstructorDecl *Ctor) { } } - for (const FieldDecl *F : Ctor->getParent()->fields()) { - // Anonymous aggregate members and unnamed bit-fields are skipped; a named - // bit-field is checked like any other member. Reference and const members - // already have dedicated diagnostics when left uninitialized. - if (F->isUnnamedBitField() || !F->getDeclName() || - F->getType()->isReferenceType() || F->getType().isConstQualified()) - continue; - if (F->hasAttr() || F->hasInClassInitializer() || - Written.count(F)) - continue; - if (!S.Profiles().defaultInitLeavesScalarIndeterminate(F->getType(), - /*HonorUninitMarkers=*/true)) - continue; - if (!S.Profiles().shouldEmitProfileViolation( - "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) - continue; - S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_member) - << "std::init" << F->getDeclName(); - S.Diag(F->getLocation(), diag::note_init_uninit_member_here) - << F->getDeclName(); - } + diagnoseCtorUninitFields(S, Ctor, Ctor->getParent(), Written); // The guarantee is over the complete object (paper §5.1, §7.1), so a // direct base-class subobject left indeterminate is as much a violation as a diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 8fd97beccac28..ca108050420c5 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -175,3 +175,63 @@ struct SuppressedBaseByRule : Base { struct VirtualBase : virtual Base { VirtualBase() {} }; + +// ============================================================ +// Anonymous aggregate members +// ============================================================ + +// The leaves of an anonymous struct member initialize exactly like direct +// members (a written initializer for one is an indirect member-initializer), +// so a constructor must cover them the same way. +struct AnonStructMissing { + struct { + int x; // expected-note {{member 'x' declared here}} + }; + AnonStructMissing() {} // expected-error {{constructor does not initialize member 'x' under profile 'std::init'}} +}; + +struct AnonStructMemInit { + struct { + int x; + }; + AnonStructMemInit() : x(1) {} // OK: indirect member-initializer covers the leaf +}; + +struct AnonStructNSDMI { + struct { + int x = 0; + }; + AnonStructNSDMI() {} // OK: the leaf's default member initializer covers it +}; + +struct AnonStructMarked { + struct { + int x [[uninit]]; + }; + AnonStructMarked() {} // OK: the leaf's marker acknowledges it +}; + +struct AnonStructPartial { + struct { + int x; + int y; // expected-note {{member 'y' declared here}} + }; + AnonStructPartial() : x(1) {} // expected-error {{constructor does not initialize member 'y' under profile 'std::init'}} +}; + +struct AnonStructNested { + struct { + struct { + int deep; // expected-note {{member 'deep' declared here}} + }; + }; + AnonStructNested() {} // expected-error {{constructor does not initialize member 'deep' under profile 'std::init'}} +}; + +struct AnonStructSuppressed { + struct { + int x; + }; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] AnonStructSuppressed() {} +}; From ed76cab4522ed8db6f52c6808e98306e06361ec2 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:16:25 -0400 Subject: [PATCH 262/289] Check anonymous-union members in ctor_uninit_member --- clang/docs/ProfilesFramework.rst | 4 +- .../clang/Basic/DiagnosticSemaKinds.td | 5 ++ clang/lib/Sema/SemaProfiles.cpp | 40 +++++++++++++-- .../test/SemaCXX/safety-profile-init-ctor.cpp | 49 +++++++++++++++++++ 4 files changed, 94 insertions(+), 4 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 0b547c4903b07..4aa74870bffed 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -556,7 +556,9 @@ initializer or by the base's own user-provided default constructor: A member whose type's default-initialization leaves unacknowledged scalars indeterminate (a nested aggregate) is flagged the same way, and the members of an anonymous struct are checked exactly like direct members (a written -initializer for one is an indirect member-initializer). A delegating +initializer for one is an indirect member-initializer). An anonymous +*union* member instead needs one active member: a written leaf initializer +or a leaf default member initializer satisfies it. A delegating constructor is exempt -- its target initializes the members -- and so is a union's own constructor, whose members are mutually exclusive (§5.6). diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index b999e2fa865cb..d469695692f34 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14191,8 +14191,13 @@ def err_init_uninit_static_marker : ProfileRuleError< "storage duration under profile '%0'; it is zero-initialized">; def err_init_ctor_uninit_member : ProfileRuleError< "constructor does not initialize member %1 under profile '%0'">; +def err_init_ctor_uninit_anon_union : ProfileRuleError< + "constructor does not initialize any member of the anonymous union under " + "profile '%0'">; def note_init_uninit_member_here : Note< "member %0 declared here">; +def note_init_uninit_anon_union_here : Note< + "anonymous union declared here">; def err_init_ctor_uninit_base : ProfileRuleError< "constructor does not initialize base class %1 under profile '%0'">; def note_init_uninit_base_here : Note< diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index d8d401eb88068..985659036dd47 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1890,6 +1890,24 @@ void runTestCtorFinalCallback(Sema &S, CXXConstructorDecl *Ctor) { << "test::ctor_final" << Ctor->getParent(); } +// True if any leaf field of \p RD -- recursing through nested anonymous +// records -- has a written member-initializer in \p Written. One written +// leaf gives an anonymous union its active member. +static bool anyLeafFieldWritten( + const CXXRecordDecl *RD, + const llvm::SmallPtrSetImpl &Written) { + for (const FieldDecl *F : RD->fields()) { + if (Written.count(F)) + return true; + if (F->isAnonymousStructOrUnion()) + if (const CXXRecordDecl *AnonRD = F->getType()->getAsCXXRecordDecl(); + AnonRD && AnonRD->hasDefinition() && + anyLeafFieldWritten(AnonRD->getDefinition(), Written)) + return true; + } + return false; +} + // The per-field walk of the ctor_uninit_member callback: check every // checkable field of \p RD against \p Written, recursing into anonymous // struct members -- their leaves initialize exactly like direct members of @@ -1907,10 +1925,26 @@ static void diagnoseCtorUninitFields( const CXXRecordDecl *AnonRD = F->getType()->getAsCXXRecordDecl(); if (!AnonRD || !AnonRD->hasDefinition() || AnonRD->isInvalidDecl()) continue; - // An anonymous union's members are mutually exclusive -- one active - // member satisfies it -- so the every-member walk does not apply. - if (AnonRD->isUnion()) + // An anonymous union's members are mutually exclusive: one written + // leaf gives it its active member -- deliberately lenient for a + // struct variant only partially covered by its written leaves (a + // missed diagnostic, never a false positive) -- and a vacuous + // default-initialization (an empty union, or a leaf NSDMI, which is + // the active member) needs nothing. A leaf [[uninit]] marker is not + // consulted: union_marker already rejects markers on union members. + if (AnonRD->isUnion()) { + if (anyLeafFieldWritten(AnonRD->getDefinition(), Written) || + !S.Profiles().defaultInitLeavesScalarIndeterminate( + F->getType(), /*HonorUninitMarkers=*/true)) + continue; + if (!S.Profiles().shouldEmitProfileViolation( + "std::init", "ctor_uninit_member", Ctor->getLocation(), Ctor)) + continue; + S.Diag(Ctor->getLocation(), diag::err_init_ctor_uninit_anon_union) + << "std::init"; + S.Diag(F->getLocation(), diag::note_init_uninit_anon_union_here); continue; + } diagnoseCtorUninitFields(S, Ctor, AnonRD->getDefinition(), Written); continue; } diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index ca108050420c5..6ff0748eedd0d 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -235,3 +235,52 @@ struct AnonStructSuppressed { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] AnonStructSuppressed() {} }; + +// An anonymous union needs one active member, not every member: a written +// leaf initializer or a leaf default member initializer satisfies it. +struct AnonUnionMissing { + union { // expected-note {{anonymous union declared here}} + int i; + float f; + }; + AnonUnionMissing() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} +}; + +struct AnonUnionMemInit { + union { + int i; + float f; + }; + AnonUnionMemInit() : i(1) {} // OK: the written leaf is the active member +}; + +struct AnonUnionNSDMI { + union { + int i = 0; + float f; + }; + AnonUnionNSDMI() {} // OK: the leaf's default member initializer is the active member +}; + +// One written leaf of a struct variant activates the union; whether that +// variant is fully initialized is deliberately lenient (a missed diagnostic, +// never a false positive). +struct AnonUnionStructVariant { + union { + struct { + int a; + int b; + }; + float f; + }; + AnonUnionStructVariant() : a(1) {} // OK: lenient +}; + +struct AnonUnionSuppressed { + union { + int i; + float f; + }; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] AnonUnionSuppressed() {} +}; From d42e42f9c0cb6be0cd03fcfb830adb3460c540cf Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:30:59 -0400 Subject: [PATCH 263/289] Add the [[now_uninit]] function attribute --- clang/include/clang/Basic/Attr.td | 6 ++ clang/include/clang/Basic/AttrDocs.td | 51 ++++++++++++++ .../clang/Basic/DiagnosticSemaKinds.td | 3 + clang/lib/Sema/SemaDeclAttr.cpp | 36 ++++++++++ ...a-attribute-supported-attributes-list.test | 1 + .../safety-profile-now-uninit-marker.cpp | 67 +++++++++++++++++++ 6 files changed, 164 insertions(+) create mode 100644 clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 65e30f7a40823..6b85d48f03529 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -1739,6 +1739,12 @@ def NowInit : InheritableAttr { let Documentation = [NowInitDocs]; } +def NowUninit : InheritableAttr { + let Spellings = [CXX11<"", "now_uninit", 202602>]; + let Subjects = SubjectList<[Function], ErrorDiag>; + let Documentation = [NowUninitDocs]; +} + def NonBlocking : TypeAttr { let Spellings = [Clang<"nonblocking">]; let Args = [ExprArgument<"Cond", /*optional*/1>]; diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 7745feba2f1bf..7b3d98dce5dbc 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -996,6 +996,57 @@ without ``-fprofiles``. }]; } +def NowUninitDocs : Documentation { + let Category = DocCatFunction; + let Heading = "now_uninit"; + let Content = [{ +The ``[[now_uninit]]`` attribute marks a function as *ending the lifetime* +of the storage passed to each of its pointer or reference parameters, +leaving that storage uninitialized. It is purely informational: it has no +effect on code generation. + +The attribute is the mirror of ``[[now_init]]`` and exists to support the +initialization profile (see :doc:`ProfilesFramework`). It supplies the +recording P4222R2 §4.4 notes is missing ("the object subjected to +``destroy_at()`` should be considered uninitialized, but there is no way of +recording that in the code"; the exact placement and spelling of the marker +family track an open committee question). After a call to a +``[[now_uninit]]`` function, the profile treats the storage bound to every +pointer or reference parameter as uninitialized again, so the object can be +re-initialized -- and a second destruction, or a use of the storage through +an ordinary pointer or reference, is diagnosed: + +.. code-block:: c++ + + [[now_init]] void construct_at(int *p [[ref_to_uninit]]); + [[now_uninit]] void destroy_at(int *p); + + void f() { + int u [[uninit]]; + construct_at(&u); + destroy_at(&u); + construct_at(&u); // OK: u is uninitialized again after the destroy + } + + void g() { + int u [[uninit]]; + construct_at(&u); + destroy_at(&u); + destroy_at(&u); // error: double destruction + } + +Because the attribute covers *every* pointer or reference parameter, apply +it only to functions that end the lifetime of every such argument's +storage. It requires at least one parameter of pointer or reference type +(it would be vacuous otherwise); this is diagnosed regardless of +``-fprofiles``. + +Outside the initialization profile the attribute is otherwise silently +accepted and has no effect, so code that uses it remains valid when compiled +without ``-fprofiles``. + }]; +} + def NoMergeDocs : Documentation { let Category = DocCatStmt; let Content = [{ diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index d469695692f34..de21e112f796d 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -14180,6 +14180,9 @@ def err_ref_to_uninit_attr_invalid_type : Error< def err_now_init_attr_no_marked_parameter : Error< "'now_init' attribute requires at least one parameter marked " "'[[ref_to_uninit]]'">; +def err_now_uninit_attr_no_pointer_parameter : Error< + "'now_uninit' attribute requires at least one parameter of pointer or " + "reference type">; def err_init_union_marker : ProfileRuleError< "'[[uninit]]' cannot be applied to %select{a variable of union type|" "a union member|a data member of union type}1 under profile '%0'">; diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp index 4cb2fa8450f21..7e867ee71f3cc 100644 --- a/clang/lib/Sema/SemaDeclAttr.cpp +++ b/clang/lib/Sema/SemaDeclAttr.cpp @@ -7029,6 +7029,38 @@ static void handleNowInitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { D->addAttr(::new (S.Context) NowInitAttr(S.Context, AL)); } +static void handleNowUninitAttr(Sema &S, Decl *D, const ParsedAttr &AL) { + // The SubjectList restricts D to a function. [[now_uninit]] asserts that + // the callee ends the lifetime of the storage bound to each of its + // pointer/reference parameters -- the recording P4222R2 §4.4 wishes for + // ("the object subjected to destroy_at() should be considered + // uninitialized, but there is no way of recording that in the code") -- + // so a declaration with no such parameter would make it vacuous; reject + // it. Unlike [[now_init]], the parameters are unmarked (they receive + // initialized memory), so the vacuity check keys on their types: a + // pointer to an object or a reference. A function pointer or reference + // denotes a function, never destroyable storage (mirroring + // [[ref_to_uninit]]'s subject rule), and a dependent type may + // instantiate to anything, so it passes -- if it does not become a + // pointer or reference, the inherited attribute goes inert (the + // withdrawal arms only ever key on pointer/reference bindings). Like the + // other marker subject checks, this fires regardless of -fprofiles. + if (llvm::none_of(cast(D)->parameters(), + [](const ParmVarDecl *P) { + QualType T = P->getType(); + return T->isDependentType() || + ((T->isPointerType() || T->isReferenceType()) && + !T->isFunctionPointerType() && + !T->isFunctionReferenceType()); + })) { + S.Diag(AL.getLoc(), diag::err_now_uninit_attr_no_pointer_parameter); + AL.setInvalid(); + return; + } + + D->addAttr(::new (S.Context) NowUninitAttr(S.Context, AL)); +} + static void handleMIGServerRoutineAttr(Sema &S, Decl *D, const ParsedAttr &AL) { // Check that the return type is a `typedef int kern_return_t` or a typedef // around it, because otherwise MIG convention checks make no sense. @@ -8307,6 +8339,10 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL, handleNowInitAttr(S, D, AL); break; + case ParsedAttr::AT_NowUninit: + handleNowUninitAttr(S, D, AL); + break; + case ParsedAttr::AT_ObjCExternallyRetained: S.ObjC().handleExternallyRetainedAttr(D, AL); break; diff --git a/clang/test/Misc/pragma-attribute-supported-attributes-list.test b/clang/test/Misc/pragma-attribute-supported-attributes-list.test index ae436d361aba5..4752ab7fd948c 100644 --- a/clang/test/Misc/pragma-attribute-supported-attributes-list.test +++ b/clang/test/Misc/pragma-attribute-supported-attributes-list.test @@ -147,6 +147,7 @@ // CHECK-NEXT: NonString (SubjectMatchRule_variable, SubjectMatchRule_field) // CHECK-NEXT: NotTailCalled (SubjectMatchRule_function) // CHECK-NEXT: NowInit (SubjectMatchRule_function) +// CHECK-NEXT: NowUninit (SubjectMatchRule_function) // CHECK-NEXT: OMPAssume (SubjectMatchRule_function, SubjectMatchRule_objc_method) // CHECK-NEXT: OSConsumed (SubjectMatchRule_variable_is_parameter) // CHECK-NEXT: OSReturnsNotRetained (SubjectMatchRule_function, SubjectMatchRule_objc_method, SubjectMatchRule_objc_property, SubjectMatchRule_variable_is_parameter) diff --git a/clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp b/clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp new file mode 100644 index 0000000000000..50597a732e981 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-now-uninit-marker.cpp @@ -0,0 +1,67 @@ +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 %s +// RUN: %clang_cc1 -fsyntax-only -verify=no-profiles -std=c++23 %s + +// The [[now_uninit]] marker -- the mirror of [[now_init]], supplying the +// recording P4222R2 §4.4 notes is missing for destroy_at -- is recognized +// regardless of -fprofiles. It applies to functions with at least one +// parameter of pointer or reference type -- the parameters whose storage the +// callee destroys; with no profile enforced it has no effect on those valid +// subjects. Other placements, and a function with no pointer or reference +// parameter (a vacuous claim), are rejected regardless of -fprofiles. + +[[now_uninit]] void wipe(int *p); +[[now_uninit]] void wipe_ref(int &r); +[[now_uninit]] void wipe_void(void *p); +void wipe_after_name [[now_uninit]] (int *p); +[[now_uninit]] void wipe_many(int n, int *p, bool b); + +// Carrying both attributes is allowed: a reinitializer both ends and starts +// a lifetime for its argument's storage. +[[now_init]] [[now_uninit]] void reinit(int *p [[ref_to_uninit]]); + +struct S { + [[now_uninit]] void wipe_member(int *p); + [[now_uninit]] void wipe_out_of_line(int *p); +}; +void S::wipe_out_of_line(int *p) {} + +template +[[now_uninit]] void wipe_template(T *p) {} + +// A dependent parameter type may instantiate to a pointer or reference, so +// the vacuity check accepts the template; if it does not, the inherited +// attribute goes inert rather than re-diagnosed (the withdrawal arms only +// key on pointer/reference bindings). +template +[[now_uninit]] void wipe_dependent(T v) {} +template void wipe_dependent(int *); +template void wipe_dependent(int); + +int bad_var [[now_uninit]]; // expected-error {{'now_uninit' attribute only applies to functions}} \ + // no-profiles-error {{'now_uninit' attribute only applies to functions}} + +struct BadField { + int m [[now_uninit]]; // expected-error {{'now_uninit' attribute only applies to functions}} \ + // no-profiles-error {{'now_uninit' attribute only applies to functions}} +}; + +[[now_uninit]] void bad_no_params(); // expected-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} \ + // no-profiles-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} + +[[now_uninit]] void bad_value_params(int v, bool b); // expected-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} \ + // no-profiles-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} + +// A function pointer denotes a function, never destroyable storage +// (mirroring [[ref_to_uninit]]'s subject rule), so it does not satisfy the +// vacuity check. +[[now_uninit]] void bad_fn_ptr_param(void (*fp)()); // expected-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} \ + // no-profiles-error {{'now_uninit' attribute requires at least one parameter of pointer or reference type}} + +// Inert without an enforced profile: a valid placement changes nothing about +// how either run type-checks these calls. +void use() { + int x = 0; + wipe(&x); + wipe_ref(x); + wipe_template(&x); +} From 0f7640567736d88c2adb254f0a8deb43d52e84c3 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:37:09 -0400 Subject: [PATCH 264/289] Withdraw store credit at calls to [[now_uninit]] functions --- clang/docs/ProfilesFramework.rst | 28 +++- clang/docs/ProfilesFrameworkInternals.rst | 7 +- clang/include/clang/Sema/SemaProfiles.h | 31 ++++- clang/lib/Sema/SemaProfiles.cpp | 74 ++++++++-- .../safety-profile-init-ref-to-uninit.cpp | 128 ++++++++++++++++++ 5 files changed, 246 insertions(+), 22 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 4aa74870bffed..9253a212e8fbc 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -524,6 +524,21 @@ now_init(T* p [[ref_to_uninit]]);``, its unmarked return is already trusted as initialized -- but only the attribute legalizes the original *name* after the call. +The lifecycle *end* is the mirror attribute ``[[now_uninit]]`` -- the +recording §4.4 notes is missing for ``destroy_at`` ("the object subjected to +``destroy_at()`` should be considered uninitialized, but there is no way of +recording that in the code"). It declares that a function ends the lifetime +of the storage passed to each of its pointer or reference parameters (which +are unmarked -- destruction takes initialized memory; apply the attribute +only to functions that destroy *every* such argument's storage), and a call +to one *withdraws* exactly the credit the equivalent ``[[now_init]]`` call +would have recorded. Declare ``destroy_at`` as ``template void +destroy_at(T* p) [[now_uninit]];`` and the construct/destroy/construct cycle +is legal, a second destruction is rejected (the storage no longer refers to +initialized memory, so binding it to the unmarked parameter is the +unmarked-direction violation), and binding the destroyed storage to an +ordinary pointer or reference is rejected the same way. + Constructors ------------ @@ -616,12 +631,15 @@ false positive. missed diagnostic, never a false positive. That escape-crediting is an interim leniency relative to the paper (which credits only ``now_init``); tightening it to ``[[now_init]]`` callees alone is future work. -- ``construct_at``/``destroy_at`` flow is only partially modeled: a - ``[[now_init]]``-annotated ``construct_at`` declaration checks the +- ``construct_at``/``destroy_at`` flow is modeled through the annotations: + a ``[[now_init]]``-annotated ``construct_at`` declaration checks the lifecycle start (including double construction, via the reverse-direction - binding rule), but destruction -- ``destroy_at``, double destruction, - use-after-destroy -- is not checked, and writes through - ``[[ref_to_uninit]]`` are not verified. + binding rule) and a ``[[now_uninit]]``-annotated ``destroy_at`` the end + (double destruction, and use-after-destroy where the destroyed storage is + *bound* -- a direct named read after a destroy stays with the flow passes, + which treat the call as an escape, so it is a missed diagnostic; a destroy + inside a constructor body earns no kill bit in the ctor-body dataflow + either). Writes through ``[[ref_to_uninit]]`` are still not verified. - A ``new`` expression whose result is not bound to anything (``new int;``) is not checked. - A call through a function pointer cannot see parameter markers on the diff --git a/clang/docs/ProfilesFrameworkInternals.rst b/clang/docs/ProfilesFrameworkInternals.rst index a5d5b8e2d0891..1337001b4cd0b 100644 --- a/clang/docs/ProfilesFrameworkInternals.rst +++ b/clang/docs/ProfilesFrameworkInternals.rst @@ -286,7 +286,12 @@ patterns. Its rules map to mechanisms as follows: Two helpers are shared across the rules. ``refersToUninitializedMemory`` classifies an expression as referring to initialized, uninitialized, or -unknown storage purely from its syntactic form; its ``UninitAccessOpts`` +unknown storage purely from its syntactic form (parse-order store credit +refines it, recorded by ``recordInitProfileStore`` and by the +``recordNowInitArgument`` / ``recordNowUninitArgument`` pair, which share +one argument-shape walk to add or withdraw the credit of storage a +``[[now_init]]`` callee initializes or a ``[[now_uninit]]`` callee +destroys); its ``UninitAccessOpts`` presets distinguish a *binding* source (markers count everywhere), a value *read*, and a scalar *store* (which differ in whether the top-level ``[[uninit]]`` marker counts and whether ``[[ref_to_uninit]]`` storage is diff --git a/clang/include/clang/Sema/SemaProfiles.h b/clang/include/clang/Sema/SemaProfiles.h index 8d3a571dbe41e..cf725a8fbf0f2 100644 --- a/clang/include/clang/Sema/SemaProfiles.h +++ b/clang/include/clang/Sema/SemaProfiles.h @@ -403,6 +403,34 @@ class SemaProfiles : public SemaBase { void recordNowInitArgument(const ValueDecl *Target, QualType T, const Expr *Src); + /// std::init / [[now_uninit]]: a [[now_uninit]] callee ends the lifetime + /// of the storage bound to each of its pointer/reference parameters + /// (P4222R2 §4.4's missing destroy_at recording), so the binding + /// *withdraws* the parse-order credit the equivalent [[now_init]] call + /// would have recorded: the storage classifies as uninitialized again, + /// re-construction becomes legal, and a second destruction or an + /// unmarked-target binding of the storage is the ordinary + /// unmarked-direction violation. Called from the tail of + /// checkInitProfileRefToUninitBinding when \p Target is a + /// pointer/reference parameter of a [[now_uninit]] function -- the + /// parameters are unmarked (they receive initialized memory), so unlike + /// recordNowInitArgument no parameter marker is required. Recognizes the + /// same source shapes, which are marker-keyed on the source side, so an + /// ordinary initialized argument withdraws nothing. Same gates as its + /// sibling: not enforcement- or suppression-gated (a suppressed destroy + /// still destroys), but never-executed contexts withdraw nothing. + void recordNowUninitArgument(const ValueDecl *Target, QualType T, + const Expr *Src); + + /// The shared shape walk of recordNowInitArgument and + /// recordNowUninitArgument: resolve \p Src (bound as \p T) to the storage + /// the annotated callee initializes or destroys -- &u / u (whole-entity), + /// p / *p / &*p (marked-pointer pointee), r (marked-reference referent), + /// &base.m / base.m (per-object member) -- and add (\p Withdraw false) or + /// remove (true) the corresponding credit bit. + void recordLifetimeAnnotatedArgument(QualType T, const Expr *Src, + bool Withdraw); + /// True if the current expression-evaluation context never executes at /// runtime (unevaluated or discarded-statement), mirroring /// shouldEmitProfileViolation's context checks: a store or a callee @@ -419,7 +447,8 @@ class SemaProfiles : public SemaBase { /// True if \p VD is a [[ref_to_uninit]] local/parameter pointer or /// reference credited by a recorded store through it; the storage behind /// it then classifies as initialized (until a pointer is reseated -- - /// references cannot be reseated, so their credit is never cleared). + /// references cannot be reseated, so no *store* ever clears their credit; + /// a [[now_uninit]] callee withdraws either kind). bool hasPointeeStoreCredit(const ValueDecl *VD) const; /// True if the [[uninit]] member \p F of the base object identified by diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 985659036dd47..a23a11e05f452 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1406,10 +1406,12 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, checkInitProfileRefToUninit(Loc, Target && Target->hasAttr(), T->isReferenceType(), Src, D); - // Runs after the check: the binding itself is judged against the + // Run after the check: the binding itself is judged against the // *pre-call* state, and only then does a [[now_init]] callee's promised - // initialization take effect for what follows in parse order. + // initialization -- or a [[now_uninit]] callee's promised destruction -- + // take effect for what follows in parse order. recordNowInitArgument(Target, T, Src); + recordNowUninitArgument(Target, T, Src); } void SemaProfiles::recordNowInitArgument(const ValueDecl *Target, QualType T, @@ -1431,6 +1433,46 @@ void SemaProfiles::recordNowInitArgument(const ValueDecl *Target, QualType T, // never-executed context earns no credit. if (inNeverExecutedContext()) return; + recordLifetimeAnnotatedArgument(T, Src, /*Withdraw=*/false); +} + +void SemaProfiles::recordNowUninitArgument(const ValueDecl *Target, QualType T, + const Expr *Src) { + // The mirror of recordNowInitArgument: a [[now_uninit]] callee ends the + // lifetime of the storage bound to each of its pointer/reference + // parameters (P4222R2 §4.4's missing destroy_at recording). The + // parameters are *unmarked* -- destruction takes initialized memory -- so + // the gate keys on the parameter's type, not a marker; the shape walk is + // marker-keyed on the source side, so an ordinary initialized argument + // withdraws nothing. A variadic argument or a call through a function + // pointer presents no ParmVarDecl and withdraws nothing (stale credit is + // a missed diagnostic, never a false positive -- the same boundary as + // [[now_init]]). + const auto *Parm = dyn_cast_or_null(Target); + if (!Parm || !Src) + return; + const auto *FD = dyn_cast_or_null(Parm->getDeclContext()); + if (!FD || !FD->hasAttr()) + return; + // Not enforcement- or suppression-gated (a suppressed destroy still + // destroys), but a call in a never-executed context destroys nothing. + if (inNeverExecutedContext()) + return; + recordLifetimeAnnotatedArgument(T, Src, /*Withdraw=*/true); +} + +// Add or remove a credit bit: a [[now_init]] callee's initialization adds +// it, a [[now_uninit]] callee's destruction withdraws it (removal of an +// absent entry leaves a harmless zero entry behind). +static void applyCreditBit(unsigned &Entry, unsigned Bit, bool Withdraw) { + if (Withdraw) + Entry &= ~Bit; + else + Entry |= Bit; +} + +void SemaProfiles::recordLifetimeAnnotatedArgument(QualType T, const Expr *Src, + bool Withdraw) { const Expr *E = Src->IgnoreParenImpCasts(); // Mirror the recognizers' explicit-cast pass-through (paper §4.3: a cast // of a marked pointer is itself marked; a reference cast denotes the same @@ -1458,37 +1500,39 @@ void SemaProfiles::recordNowInitArgument(const ValueDecl *Target, QualType T, if (const auto *F = dyn_cast(ME->getMemberDecl()); F && F->hasAttr()) if (const Decl *Base = resolveMemberStoreBase(ME)) - MemberStoreCredit[{Base, F}] |= WholeStored; + applyCreditBit(MemberStoreCredit[{Base, F}], WholeStored, Withdraw); return; } - // &*p / *p: the callee initializes the pointee of a marked pointer. + // &*p / *p: the callee initializes (or destroys) the pointee of a + // marked pointer. if (const auto *UO = dyn_cast(Glvalue); UO && UO->getOpcode() == UO_Deref) { if (const VarDecl *VD = getCreditableMarkedPointer(UO->getSubExpr())) - InitStoreCredit[VD] |= PointeeStored; + applyCreditBit(InitStoreCredit[VD], PointeeStored, Withdraw); return; } if (const auto *VD = dyn_cast_or_null(getDirectlyNamedDecl(Glvalue)); VD && VD->hasLocalStorage()) { - // &u / u: the whole [[uninit]] entity is initialized by the callee, - // exactly as `u = e` would credit it. + // &u / u: the whole [[uninit]] entity is initialized (or destroyed) + // by the callee, exactly as `u = e` would credit it. if (VD->hasAttr()) - InitStoreCredit[VD] |= WholeStored; + applyCreditBit(InitStoreCredit[VD], WholeStored, Withdraw); // r (a marked reference bound onward): its referent is initialized; a - // reference cannot be reseated, so the credit never lapses. + // reference cannot be reseated, so no store ever clears the credit -- + // only a [[now_uninit]] callee's withdrawal does. else if (VD->getType()->isReferenceType() && VD->hasAttr()) - InitStoreCredit[VD] |= PointeeStored; + applyCreditBit(InitStoreCredit[VD], PointeeStored, Withdraw); } return; } - // p as a pointer value: the callee initializes p's pointee -- §6.2's - // initialize2(p) example verbatim. Only a directly named marked - // local/parameter pointer is trackable; reseating p afterwards clears this - // credit like any other pointee credit. + // p as a pointer value: the callee initializes (or destroys) p's pointee + // -- §6.2's initialize2(p) example verbatim. Only a directly named marked + // local/parameter pointer is trackable; reseating p afterwards clears + // pointee credit like any other. if (const VarDecl *VD = getCreditableMarkedPointer(E)) - InitStoreCredit[VD] |= PointeeStored; + applyCreditBit(InitStoreCredit[VD], PointeeStored, Withdraw); } void SemaProfiles::checkInitProfileVariadicArgument(const Expr *Arg) { diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 0840fe360b65b..1f0a1cedde4af 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1854,6 +1854,134 @@ void test_construct_at_bridge() { (void)v; (void)w; } +// The §4.5 lifecycle *end*, via [[now_uninit]] -- the recording §4.4 wishes +// for ("the object subjected to destroy_at() should be considered +// uninitialized, but there is no way of recording that in the code"): a +// call to a [[now_uninit]] function withdraws the parse-order credit of the +// storage bound to each pointer/reference parameter, so the storage is +// uninitialized again. Re-construction becomes legal, and a second +// destruction, or binding the storage to an unmarked target, is the +// ordinary unmarked-direction violation. +template +[[now_uninit]] void destroy_at(T *p); +[[now_uninit]] void nu_wipe(int *p); +[[now_uninit]] void nu_wipe_ref(int &r); +[[now_uninit]] void nu_wipe2(int *p, int *q); + +void test_destroy_at_lifecycle() { + CtorAtPayload s [[uninit]]; + construct_at(&s, 1, 2); + destroy_at(&s); // OK: destroys initialized storage + construct_at(&s, 3, 4); // OK: uninitialized again (the bridge's error above) + int v = s.x; // OK: re-credited + destroy_at(&s); // OK: re-destroy after re-construction + (void)v; +} + +void test_double_destroy() { + int u [[uninit]]; + now_init_fill(&u); + nu_wipe(&u); // OK + nu_wipe(&u); // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} +} + +void test_use_after_destroy_binding() { + int u [[uninit]]; + now_init_fill(&u); + nu_wipe(&u); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r [[ref_to_uninit]] = &u; // OK: uninitialized again + (void)q; (void)r; +} + +// The pointee shapes withdraw like the store-credit forms: destroying +// through the marked pointer clears its pointee credit, so a later read +// through it is the read-through violation again. +void test_destroy_pointee(int *p [[ref_to_uninit]]) { + *p = 5; + int x = *p; // OK: credited + nu_wipe(p); + int y = *p; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; (void)y; +} + +// A by-reference [[now_uninit]] parameter destroys its referent; a marked +// reference's referent credit -- which no store can clear -- is withdrawn +// the same way. +void test_destroy_by_reference() { + int u [[uninit]]; + u = 5; + nu_wipe_ref(u); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; +} +void test_destroy_marked_reference(int &r [[ref_to_uninit]]) { + r = 5; + nu_wipe(&r); + int x = r; // expected-error {{read through a '[[ref_to_uninit]]' pointer or reference accesses uninitialized memory under profile 'std::init'}} + (void)x; +} + +// Whole-member credit is withdrawn per base object: destroying a1's member +// leaves a2's credit intact. +struct NuMember { int m [[uninit]]; }; +void test_destroy_member_per_object() { + NuMember a1, a2; + a1.m = 5; + a2.m = 5; + nu_wipe(&a1.m); + int *q1 = &a1.m; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *q2 = &a2.m; // OK: a2's credit is untouched + (void)q1; (void)q2; +} + +// A multi-parameter [[now_uninit]] function withdraws every pointer +// argument's credit (the attribute's contract covers all of them). +void test_destroy_multi_param() { + int u [[uninit]], v [[uninit]]; + u = 1; + v = 2; + nu_wipe2(&u, &v); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + int *r = &v; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; (void)r; +} + +// A destroy in a never-executed context destroys nothing (mirroring the +// no-credit contexts of the store recorder). +void test_destroy_never_executed() { + int u [[uninit]]; + u = 5; + using X = decltype(nu_wipe(&u)); + int *q = &u; // OK: the unevaluated destroy withdrew nothing + (void)q; +} + +// A suppressed destroy still destroys, exactly as a suppressed store still +// credits: failing to withdraw would turn suppression into later missed +// double-destroy diagnostics. +void test_suppressed_destroy_withdraws() { + int u [[uninit]]; + u = 5; + // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} + [[profiles::suppress(std::init)]] + nu_wipe(&u); + int *q = &u; // expected-error {{pointer to uninitialized memory must be marked '[[ref_to_uninit]]' under profile 'std::init'}} + (void)q; +} + +// A destroy through a function pointer presents no ParmVarDecl, so it +// withdraws nothing -- stale credit, a known missed diagnostic (the same +// boundary as [[now_init]] call credit). +void test_destroy_through_function_pointer() { + int u [[uninit]]; + u = 5; + void (*fp)(int *) = nu_wipe; + fp(&u); + int *q = &u; // OK: known gap -- the marker is invisible through the pointer + (void)q; +} + // The reverse direction applies through the assignment funnel too: a // credited marked pointer refers to initialized memory, so assigning it to // another marked pointer is the requires-uninit error -- while an unmarked From 33d16ff90e8a0643108cfc831e3861a015cdc407 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:39:41 -0400 Subject: [PATCH 265/289] Correct the untracked-copy rationale and pin the trust boundaries --- clang/docs/ProfilesFramework.rst | 5 +++++ clang/lib/Sema/AnalysisBasedWarnings.cpp | 12 ++++++++---- .../safety-profile-init-local-member-read.cpp | 19 ++++++++++++++----- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 9253a212e8fbc..6c9fa103e58d7 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -647,6 +647,11 @@ false positive. the object-argument check. - Members of anonymous structs and unions, and arrays of aggregates, are not flow-tracked. +- ``[[uninit]]`` members of by-value parameters, and of locals copied from + untracked sources, are not flow-tracked: a copy does not inherit + initialization (§5.2) -- it copies indeterminate bits -- but the source's + per-member state is unknown, so reads of such members are trusted (a + missed diagnostic, never a false positive). - Virtual base subobjects are not checked by ``ctor_uninit_member`` (they are initialized by the most-derived class). - A read of a tracked member inside another member's default initializer is diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 99679ecda2244..de1db57555535 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2186,10 +2186,14 @@ static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { if (RD->isUnion() || RD->isDependentType() || hasUserProvidedCtor(RD)) return nullptr; // Declared without a real initializer: for a record local that is the - // synthesized call to the implicit default constructor (`Agg a;`). Any - // written form -- `Agg a{}` / `= {}` (an InitListExpr), `Agg a = Agg()` - // (a CXXTemporaryObjectExpr), a copy (a non-default constructor) -- gives - // every member a value and leaves nothing to track. + // synthesized call to the implicit default constructor (`Agg a;`). A + // value-initializing form -- `Agg a{}` / `= {}` (an InitListExpr), + // `Agg a = Agg()` (a CXXTemporaryObjectExpr) -- gives every member a + // value and leaves nothing to track. A *copy* does not: it copies + // indeterminate bits, and a copy does not inherit initialization (paper + // §5.2). But the source's per-member state is unknowable here for an + // arbitrary source, so copies stay untracked -- a missed diagnostic, + // never a false positive. if (const Expr *Init = V->getInit()) { const auto *CCE = dyn_cast(Init->IgnoreImplicit()); if (!CCE || isa(CCE) || diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp index a4d05f4d21038..6ba4e71c83b2c 100644 --- a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -177,16 +177,25 @@ int test_trusted_base_member() { return d.tm; // OK: trusted } -// Any written initialization gives every member a value; only the bare -// `Agg a;` form (the implicit no-op default-construction) is tracked. -Agg make_agg(); -int test_initialized_forms(Agg other) { +// A value-initializing written form gives every member a value; only the +// bare `Agg a;` form (the implicit no-op default-construction) is tracked. +int test_value_initialized_forms() { Agg a{}; Agg b = {}; Agg c = Agg(); + return a.m + b.m + c.m; // OK +} + +// A copy does NOT give the [[uninit]] member a value -- it copies +// indeterminate bits (a copy does not inherit initialization, paper §5.2) +// -- but the source's per-member state is unknowable for an untracked +// source, so such copies stay untracked: a known gap, never a false +// positive. +Agg make_agg(); +int test_copy_from_untracked_source(Agg other) { Agg d = other; Agg e = make_agg(); - return a.m + b.m + c.m + d.m + e.m; // OK + return d.m + e.m; // OK: known gap (untracked sources) } // Parameters and references arrive constructed by the caller and are never From 458068e62382b8641e73b12e154c5cc3ae6cf79f Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:42:19 -0400 Subject: [PATCH 266/289] Replay events as the local-member pass's transfer function --- clang/lib/Sema/AnalysisBasedWarnings.cpp | 55 +++++++++++++----------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index de1db57555535..c0014ac1ddb4c 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2334,10 +2334,8 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { }; const unsigned NumBlocks = cfg->getNumBlockIDs(); std::vector> Events(NumBlocks); - std::vector Gen(NumBlocks, llvm::BitVector(N, false)); for (const CFGBlock *B : *cfg) { auto &BlockEvents = Events[B->getBlockID()]; - auto &BlockGen = Gen[B->getBlockID()]; for (const CFGElement &Elem : *B) { auto CS = Elem.getAs(); if (!CS) @@ -2357,7 +2355,6 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { continue; BlockEvents.push_back( {BO->isCompoundAssignmentOp() ? ReadWrite : Write, Idx, BO}); - BlockGen.set(Idx); } else if (const auto *UO = dyn_cast(St)) { if (!UO->isIncrementDecrementOp()) continue; @@ -2365,7 +2362,6 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { if (Idx == ~0u) continue; BlockEvents.push_back({ReadWrite, Idx, UO}); - BlockGen.set(Idx); } else if (const auto *DRE = dyn_cast(St)) { if (Benign.count(DRE)) continue; @@ -2375,20 +2371,44 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { auto It = VarRange.find(V); if (It == VarRange.end()) continue; - for (unsigned Idx = It->second.first; Idx != It->second.second; ++Idx) { + for (unsigned Idx = It->second.first; Idx != It->second.second; ++Idx) BlockEvents.push_back({Write, Idx, DRE}); - BlockGen.set(Idx); - } } } } + // Replay a block's events over State: the block-level transfer function, + // shared by the fixpoint below and the reporting replay after it so the + // two always agree. When Offending is non-null, a read of a member not + // definitely assigned at its program point is collected. + auto ApplyEvents = + [](ArrayRef BlockEvents, llvm::BitVector &State, + std::vector> *Offending) { + for (const Event &Ev : BlockEvents) { + switch (Ev.Kind) { + case Read: + if (Offending && !State.test(Ev.Idx)) + (*Offending)[Ev.Idx].push_back(Ev.E); + break; + case Write: + State.set(Ev.Idx); + break; + case ReadWrite: + if (Offending && !State.test(Ev.Idx)) + (*Offending)[Ev.Idx].push_back(Ev.E); + State.set(Ev.Idx); + break; + } + } + }; + // Forward "definitely assigned" dataflow, identical in shape to the // ctor-body pass's: nothing is assigned at function entry (a tracked local // cannot be referenced before its DeclStmt anyway); a block's entry is the // intersection over its predecessors' exits; unprocessed (unreachable) // predecessors keep the all-assigned top, so unreachable code is never - // flagged. + // flagged. The transfer function is the event replay above (monotone: + // events only set bits), so the worklist still reaches a fixpoint. std::vector EntryState(NumBlocks, llvm::BitVector(N, true)); std::vector ExitState(NumBlocks, llvm::BitVector(N, true)); const CFGBlock &CFGEntry = cfg->getEntry(); @@ -2412,7 +2432,7 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { } } EntryState[B->getBlockID()] = In; - In |= Gen[B->getBlockID()]; + ApplyEvents(Events[B->getBlockID()], In, /*Offending=*/nullptr); if (In != ExitState[B->getBlockID()]) { ExitState[B->getBlockID()] = In; Worklist.enqueueSuccessors(B); @@ -2424,22 +2444,7 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { std::vector> Offending(N); for (const CFGBlock *B : *cfg) { llvm::BitVector Assigned = EntryState[B->getBlockID()]; - for (const Event &Ev : Events[B->getBlockID()]) { - switch (Ev.Kind) { - case Read: - if (!Assigned.test(Ev.Idx)) - Offending[Ev.Idx].push_back(Ev.E); - break; - case Write: - Assigned.set(Ev.Idx); - break; - case ReadWrite: - if (!Assigned.test(Ev.Idx)) - Offending[Ev.Idx].push_back(Ev.E); - Assigned.set(Ev.Idx); - break; - } - } + ApplyEvents(Events[B->getBlockID()], Assigned, &Offending); } // Report at the first offending read (in source order) that is not From fcacd5ec76f6173db6fa8937a0a493b1d997d798 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:48:17 -0400 Subject: [PATCH 267/289] Track copies of tracked locals in the local-member pass --- clang/docs/ProfilesFramework.rst | 9 +- clang/lib/Sema/AnalysisBasedWarnings.cpp | 161 ++++++++++++++++-- .../safety-profile-init-local-member-read.cpp | 99 ++++++++++- 3 files changed, 248 insertions(+), 21 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 6c9fa103e58d7..4495b44c95694 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -372,9 +372,12 @@ such a flow is intended. A ``this``-capturing lambda might run immediately, so member reads in its body count at the point the lambda is created (and writes there earn no credit -- the same strict policy). For a *local* variable the analyses instead treat any escape as an assignment (see -`Limitations`_). Members of an object initialized by a *user-provided* -constructor are trusted (§5.1) and not flow-tracked; only the defining -constructor itself is checked. +`Limitations`_). A local copied or moved from a tracked local is tracked +too: the copy's members inherit the source's per-member state at the copy +point -- a copy does not inherit initialization (§5.2), it inherits +whatever state the source has. Members of an object initialized by a +*user-provided* constructor are trusted (§5.1) and not flow-tracked; only +the defining constructor itself is checked. Writes to Subobjects of Uninitialized Objects diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index c0014ac1ddb4c..c3298bf3917f3 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2162,16 +2162,14 @@ static const DeclRefExpr *getLocalMemberAccess(const Expr *E, return DRE; } -// If V is a local whose [[uninit]] members this pass may soundly flow-track, -// return its class definition; null otherwise. Sound means nothing can have -// assigned the members before V's declaration: the class (and any base -// subtree contributing tracked members) has no user-provided constructor -// (paper §5.1 trusts one -- its body may assign, which local analysis cannot -// see), and the declaration ran nothing but the implicit no-op -// default-construction. A local that is itself [[uninit]]-marked is excluded: -// its subobject accesses are the parse-time read-through / uninit_write -// rules' territory, and tracking it here would double-diagnose. -static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { +// The class-shape half of the tracked-local guards: V is a non-parameter +// local, not itself [[uninit]]-marked (its subobject accesses are the +// parse-time read-through / uninit_write rules' territory, and tracking it +// here would double-diagnose), of a non-union class -- with no +// user-provided constructor anywhere in the contributing subtree (paper +// §5.1 trusts one: its body may assign, which local analysis cannot see). +// How V's declaration *initializes* the members is the callers' half. +static const CXXRecordDecl *getTrackableLocalClass(const VarDecl *V) { if (!V->hasLocalStorage() || isa(V) || isa(V)) return nullptr; if (V->isInvalidDecl() || V->hasAttr()) @@ -2185,15 +2183,28 @@ static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { RD = RD->getDefinition(); if (RD->isUnion() || RD->isDependentType() || hasUserProvidedCtor(RD)) return nullptr; + return RD; +} + +// If V is a local whose [[uninit]] members this pass may soundly flow-track +// from an all-unassigned start, return its class definition; null otherwise. +// Sound means nothing can have assigned the members before V's declaration: +// the class shape qualifies (above) and the declaration ran nothing but the +// implicit no-op default-construction. +static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { + const CXXRecordDecl *RD = getTrackableLocalClass(V); + if (!RD) + return nullptr; // Declared without a real initializer: for a record local that is the // synthesized call to the implicit default constructor (`Agg a;`). A // value-initializing form -- `Agg a{}` / `= {}` (an InitListExpr), // `Agg a = Agg()` (a CXXTemporaryObjectExpr) -- gives every member a // value and leaves nothing to track. A *copy* does not: it copies // indeterminate bits, and a copy does not inherit initialization (paper - // §5.2). But the source's per-member state is unknowable here for an - // arbitrary source, so copies stay untracked -- a missed diagnostic, - // never a false positive. + // §5.2). A copy from a *tracked* local inherits the source's per-member + // state through the copy harvest in checkInitProfileLocalMembers; for an + // arbitrary untracked source that state is unknowable, so those copies + // stay untracked -- a missed diagnostic, never a false positive. if (const Expr *Init = V->getInit()) { const auto *CCE = dyn_cast(Init->IgnoreImplicit()); if (!CCE || isa(CCE) || @@ -2204,6 +2215,27 @@ static const CXXRecordDecl *getTrackedLocalAggregate(const VarDecl *V) { return RD; } +// If V is copy- or move-constructed from a directly named variable, return +// that source's DeclRefExpr; null otherwise. The explicit-cast peel resolves +// the move form (`Agg b = static_cast(a);`) to its named operand, +// mirroring the parse-time recognizers' pass-through. +static const DeclRefExpr *getLocalCopySourceRef(const VarDecl *V) { + const Expr *Init = V->getInit(); + if (!Init) + return nullptr; + const auto *CCE = dyn_cast(Init->IgnoreImplicit()); + if (!CCE || CCE->getNumArgs() < 1 || + !CCE->getConstructor()->isCopyOrMoveConstructor()) + return nullptr; + const Expr *Arg = CCE->getArg(0)->IgnoreParenImpCasts(); + while (const auto *CE = dyn_cast(Arg)) { + if (!CE->getSubExpr()->isGLValue()) + break; + Arg = CE->getSubExpr()->IgnoreParenImpCasts(); + } + return dyn_cast(Arg); +} + // std::init local-aggregate member check (paper §7.1 "initialized ... before // use", the local-variable analog of the ctor-body pass above). // @@ -2271,6 +2303,56 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { } } } + + // Second harvest: locals copy- or move-constructed from a *tracked* local. + // The copy's members inherit the source's per-member state at the copy + // point -- a copy does not inherit initialization (paper §5.2), it + // inherits whatever state the source has -- modeled by a per-member Copy + // transfer event at the DeclStmt. Keyed per source *field* (not position: + // a copy sliced from a tracked derived object shares its base's + // FieldDecls). Iterated to a fixpoint so a copy of a copy resolves + // regardless of CFG block order. + llvm::DenseMap CopySource; + for (bool Added = true; Added;) { + Added = false; + for (const CFGBlock *B : *cfg) { + for (const CFGElement &Elem : *B) { + auto CS = Elem.getAs(); + if (!CS) + continue; + const auto *DS = dyn_cast(CS->getStmt()); + if (!DS) + continue; + for (const Decl *Dcl : DS->decls()) { + const auto *V = dyn_cast(Dcl); + if (!V || VarRange.count(V)) + continue; + const CXXRecordDecl *RD = getTrackableLocalClass(V); + if (!RD) + continue; + const DeclRefExpr *SrcRef = getLocalCopySourceRef(V); + const auto *Src = + SrcRef ? dyn_cast(SrcRef->getDecl()) : nullptr; + if (!Src || !VarRange.count(Src)) + continue; + SmallVector Members; + FieldIdxScratch.clear(); + collectTrackedUninitMembers(S, RD, Members, FieldIdxScratch); + if (Members.empty()) + continue; + unsigned Begin = PairField.size(); + for (const FieldDecl *F : Members) { + PairIdx[{V, F}] = PairField.size(); + PairField.push_back(F); + } + VarRange[V] = {Begin, PairField.size()}; + CopySource[V] = Src; + Added = true; + } + } + } + } + if (PairField.empty()) return; const unsigned N = PairField.size(); @@ -2316,6 +2398,15 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { } else if (const auto *UO = dyn_cast(St)) { if (UO->isIncrementDecrementOp()) Base = LookupPair(UO->getSubExpr()).Base; + } else if (const auto *DS = dyn_cast(St)) { + // The source ref of a tracked copy is consumed by the copy's + // implicit (retention-free) constructor and modeled by the Copy + // event, so it is not an escape -- an escape here would wrongly + // mark the *source* fully assigned. + for (const Decl *Dcl : DS->decls()) + if (const auto *V = dyn_cast(Dcl)) + if (CopySource.count(V)) + Base = getLocalCopySourceRef(V); } if (Base) Benign.insert(Base); @@ -2324,13 +2415,16 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { // Second pass: per-block ordered events. A load of `V.m` is a Read; a // member store is a Write (a compound assignment or ++/-- reads the old - // value first); any non-benign DeclRefExpr naming a tracked local is an - // escape, modeled as a Write of every one of its tracked members. - enum EventKind { Read, Write, ReadWrite }; + // value first); a tracked copy's DeclStmt is a per-member Copy, whose + // dest-member state becomes the source member's at that point; any + // non-benign DeclRefExpr naming a tracked local is an escape, modeled as + // a Write of every one of its tracked members. + enum EventKind { Read, Write, ReadWrite, Copy }; struct Event { EventKind Kind; unsigned Idx; const Expr *E; + unsigned SrcIdx = 0; // Copy only: the source (V, F) pair. }; const unsigned NumBlocks = cfg->getNumBlockIDs(); std::vector> Events(NumBlocks); @@ -2373,6 +2467,31 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { continue; for (unsigned Idx = It->second.first; Idx != It->second.second; ++Idx) BlockEvents.push_back({Write, Idx, DRE}); + } else if (const auto *DS = dyn_cast(St)) { + // A tracked copy: each dest member's state becomes its source + // member's, keyed per FieldDecl (a sliced copy shares the base's + // FieldDecls with a differently laid out source range). The + // DeclStmt element follows its initializer's subexpression + // elements, so the transfer sees the source's state at the copy + // point. A source field the source range does not track cannot + // occur (the dest's fields are a subset of the source's); fall + // back to Write (assume assigned) if it somehow does. + for (const Decl *Dcl : DS->decls()) { + const auto *V = dyn_cast(Dcl); + if (!V) + continue; + auto CopyIt = CopySource.find(V); + if (CopyIt == CopySource.end()) + continue; + auto Range = VarRange.find(V)->second; + for (unsigned Idx = Range.first; Idx != Range.second; ++Idx) { + auto SrcIt = PairIdx.find({CopyIt->second, PairField[Idx]}); + if (SrcIt != PairIdx.end()) + BlockEvents.push_back({Copy, Idx, V->getInit(), SrcIt->second}); + else + BlockEvents.push_back({Write, Idx, V->getInit()}); + } + } } } } @@ -2380,7 +2499,9 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { // Replay a block's events over State: the block-level transfer function, // shared by the fixpoint below and the reporting replay after it so the // two always agree. When Offending is non-null, a read of a member not - // definitely assigned at its program point is collected. + // definitely assigned at its program point is collected. Copy projects + // the source bit onto the dest bit -- monotone like the set-only kinds, + // so the fixpoint below still terminates. auto ApplyEvents = [](ArrayRef BlockEvents, llvm::BitVector &State, std::vector> *Offending) { @@ -2398,6 +2519,12 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { (*Offending)[Ev.Idx].push_back(Ev.E); State.set(Ev.Idx); break; + case Copy: + if (State.test(Ev.SrcIdx)) + State.set(Ev.Idx); + else + State.reset(Ev.Idx); + break; } } }; diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp index 6ba4e71c83b2c..485d3f6ddbc2a 100644 --- a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -20,7 +20,7 @@ int leading_unrelated_error = undeclared_identifier; // common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} struct Agg { - int m [[uninit]]; // expected-note 10 {{member 'm' declared here}} + int m [[uninit]]; // expected-note 16 {{member 'm' declared here}} }; void take_ref(Agg &); // The pointee is uninitialized memory, so the parameter carries the marker @@ -294,3 +294,100 @@ int template_local_member_read() { return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} } template int template_local_member_read(); // expected-note {{in instantiation of function template specialization 'template_local_member_read' requested here}} + +// ============================================================ +// Copies of tracked locals +// ============================================================ + +// A copy of a tracked local inherits the source's per-member state at the +// copy point -- a copy does not inherit initialization (paper §5.2), it +// inherits whatever state the source has -- so reading the copy's member is +// exactly as (in)valid as reading the source's was there. +int test_copy_read_before_source_assigned() { + Agg a; + Agg b = a; + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +int test_copy_after_source_assigned() { + Agg a; + a.m = 5; + Agg b = a; + return b.m; // OK: the source was assigned at the copy point +} + +int test_copy_then_dest_assigned() { + Agg a; + Agg b = a; + b.m = 1; + return b.m; // OK +} + +// The copy consumes the source ref without escaping it: the source keeps +// its own (unassigned) state. +int test_copy_keeps_source_tracked() { + Agg a; + Agg b = a; + b.m = 1; + return a.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// State transfers at the copy point, not later: a source assignment after +// the copy does not reach the copy. +int test_copy_point_state() { + Agg a; + Agg b = a; + a.m = 5; + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// The all-branches rule (§1.2) applies through the copy. +int test_copy_after_branch(bool c) { + Agg a; + if (c) + a.m = 5; + Agg b = a; + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// State flows through a chain of copies (harvested to a fixpoint, so the +// chain resolves regardless of declaration order in the CFG's block list). +int test_copy_of_copy() { + Agg a; + a.m = 5; + Agg b = a; + Agg c = b; + return c.m; // OK +} + +int test_copy_of_copy_unassigned() { + Agg a; + Agg b = a; + Agg c = b; + return c.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// Move construction transfers state the same way (for these classes a move +// is a copy; the explicit-cast peel resolves the directly named source). +int test_move_construction() { + Agg a; + Agg b = static_cast(a); + return b.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +// Paren and brace copy forms behave identically to the `=` form. +int test_copy_forms() { + Agg a; + a.m = 5; + Agg b(a); + Agg c{a}; + return b.m + c.m; // OK +} + +// An escape of the copy credits the copy, like any tracked local. +int test_copy_escape() { + Agg a; + Agg b = a; + take_ref(b); + return b.m; // OK: escaped +} From 5196b8cfd58bacebe3595db8ce59382d5f58c9cf Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Thu, 16 Jul 2026 22:52:43 -0400 Subject: [PATCH 268/289] Track [[uninit]] members of by-value slot parameters --- clang/docs/ProfilesFramework.rst | 26 ++++-- clang/lib/Sema/AnalysisBasedWarnings.cpp | 91 ++++++++++++------- .../safety-profile-init-local-member-read.cpp | 51 +++++++++-- 3 files changed, 118 insertions(+), 50 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 4495b44c95694..1c06d7a702cce 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -375,9 +375,17 @@ variable the analyses instead treat any escape as an assignment (see `Limitations`_). A local copied or moved from a tracked local is tracked too: the copy's members inherit the source's per-member state at the copy point -- a copy does not inherit initialization (§5.2), it inherits -whatever state the source has. Members of an object initialized by a -*user-provided* constructor are trusted (§5.1) and not flow-tracked; only -the defining constructor itself is checked. +whatever state the source has. A by-value *parameter* of such a class is +likewise a copy, of the caller's argument, so its marked members are +tracked from an unassigned start: a read before a local assignment (or an +escape of the parameter) is rejected even if the caller assigned the member +first. That is the call-boundary twin of the constructor-body strictness +-- the paper hands uninitialized-capable storage across calls through +marked pointers and references (§4.3), not by-value slots -- and +``[[profiles::suppress]]`` is the remedy where the by-value flow is +intended. Members of an object initialized by a *user-provided* +constructor are trusted (§5.1) and not flow-tracked; only the defining +constructor itself is checked. Writes to Subobjects of Uninitialized Objects @@ -650,11 +658,13 @@ false positive. the object-argument check. - Members of anonymous structs and unions, and arrays of aggregates, are not flow-tracked. -- ``[[uninit]]`` members of by-value parameters, and of locals copied from - untracked sources, are not flow-tracked: a copy does not inherit - initialization (§5.2) -- it copies indeterminate bits -- but the source's - per-member state is unknown, so reads of such members are trusted (a - missed diagnostic, never a false positive). +- ``[[uninit]]`` members of locals copied from untracked sources (a call + result, a member, an element) are not flow-tracked: a copy does not + inherit initialization (§5.2) -- it copies indeterminate bits -- but the + source's per-member state is unknown, so reads of such members are + trusted (a missed diagnostic, never a false positive). Copies of tracked + locals and by-value parameters *are* tracked (see `Reads of Uninitialized + Objects`_). - Virtual base subobjects are not checked by ``ctor_uninit_member`` (they are initialized by the most-derived class). - A read of a tracked member inside another member's default initializer is diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index c3298bf3917f3..98257a57d3229 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -2162,19 +2162,12 @@ static const DeclRefExpr *getLocalMemberAccess(const Expr *E, return DRE; } -// The class-shape half of the tracked-local guards: V is a non-parameter -// local, not itself [[uninit]]-marked (its subobject accesses are the -// parse-time read-through / uninit_write rules' territory, and tracking it -// here would double-diagnose), of a non-union class -- with no -// user-provided constructor anywhere in the contributing subtree (paper -// §5.1 trusts one: its body may assign, which local analysis cannot see). -// How V's declaration *initializes* the members is the callers' half. -static const CXXRecordDecl *getTrackableLocalClass(const VarDecl *V) { - if (!V->hasLocalStorage() || isa(V) || isa(V)) - return nullptr; - if (V->isInvalidDecl() || V->hasAttr()) - return nullptr; - QualType T = V->getType(); +// The type-shape half of the tracked guards: a non-union, non-dependent +// class with no user-provided constructor anywhere in the contributing +// subtree (paper §5.1 trusts one: its body may assign, which local analysis +// cannot see). A reference type aliases an object also reachable other ways +// and never qualifies. +static const CXXRecordDecl *getTrackableSlotClass(QualType T) { if (T->isReferenceType() || T->isDependentType()) return nullptr; const CXXRecordDecl *RD = T->getAsCXXRecordDecl(); @@ -2186,6 +2179,19 @@ static const CXXRecordDecl *getTrackableLocalClass(const VarDecl *V) { return RD; } +// The declaration-shape half for a local: V is a non-parameter local, not +// itself [[uninit]]-marked (its subobject accesses are the parse-time +// read-through / uninit_write rules' territory, and tracking it here would +// double-diagnose), of a trackable class. How V's declaration *initializes* +// the members is the callers' half. +static const CXXRecordDecl *getTrackableLocalClass(const VarDecl *V) { + if (!V->hasLocalStorage() || isa(V) || isa(V)) + return nullptr; + if (V->isInvalidDecl() || V->hasAttr()) + return nullptr; + return getTrackableSlotClass(V->getType()); +} + // If V is a local whose [[uninit]] members this pass may soundly flow-track // from an all-unassigned start, return its class definition; null otherwise. // Sound means nothing can have assigned the members before V's declaration: @@ -2274,6 +2280,42 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { llvm::DenseMap> VarRange; llvm::DenseMap, unsigned> PairIdx; + auto HarvestVar = [&](const VarDecl *V, const CXXRecordDecl *RD) { + SmallVector Members; + FieldIdxScratch.clear(); + collectTrackedUninitMembers(S, RD, Members, FieldIdxScratch); + if (Members.empty()) + return false; + unsigned Begin = PairField.size(); + for (const FieldDecl *F : Members) { + PairIdx[{V, F}] = PairField.size(); + PairField.push_back(F); + } + VarRange[V] = {Begin, PairField.size()}; + return true; + }; + + // A by-value slot parameter is tracked from an all-unassigned start + // (which is exactly the dataflow's entry state): the parameter is a + // *copy* of the caller's argument, and a copy does not inherit + // initialization (paper §5.2) -- §4.4's Slot contract makes a marked + // member uninitialized until locally proven otherwise ("you can't ask a + // slot if it is initialized"), and the paper's way to hand + // uninitialized-capable storage across a call is a marked pointer or + // reference (§4.3), not a by-value slot. This is the call-boundary twin + // of the ctor-body pass's deliberate strictness: a caller that assigned + // the member before the call is rejected here all the same, with escape + // crediting (any bare use of the parameter) and [[profiles::suppress]] + // as the remedies. Reference parameters alias the caller's own object + // and stay untracked. + if (const auto *EnclosingFD = dyn_cast_or_null(AC.getDecl())) + for (const ParmVarDecl *P : EnclosingFD->parameters()) { + if (P->isInvalidDecl() || P->hasAttr()) + continue; + if (const CXXRecordDecl *RD = getTrackableSlotClass(P->getType())) + HarvestVar(P, RD); + } + for (const CFGBlock *B : *cfg) { for (const CFGElement &Elem : *B) { auto CS = Elem.getAs(); @@ -2289,17 +2331,7 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { const CXXRecordDecl *RD = getTrackedLocalAggregate(V); if (!RD) continue; - SmallVector Members; - FieldIdxScratch.clear(); - collectTrackedUninitMembers(S, RD, Members, FieldIdxScratch); - if (Members.empty()) - continue; - unsigned Begin = PairField.size(); - for (const FieldDecl *F : Members) { - PairIdx[{V, F}] = PairField.size(); - PairField.push_back(F); - } - VarRange[V] = {Begin, PairField.size()}; + HarvestVar(V, RD); } } } @@ -2335,17 +2367,8 @@ static void checkInitProfileLocalMembers(Sema &S, AnalysisDeclContext &AC) { SrcRef ? dyn_cast(SrcRef->getDecl()) : nullptr; if (!Src || !VarRange.count(Src)) continue; - SmallVector Members; - FieldIdxScratch.clear(); - collectTrackedUninitMembers(S, RD, Members, FieldIdxScratch); - if (Members.empty()) + if (!HarvestVar(V, RD)) continue; - unsigned Begin = PairField.size(); - for (const FieldDecl *F : Members) { - PairIdx[{V, F}] = PairField.size(); - PairField.push_back(F); - } - VarRange[V] = {Begin, PairField.size()}; CopySource[V] = Src; Added = true; } diff --git a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp index 485d3f6ddbc2a..5ee25161dae26 100644 --- a/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp +++ b/clang/test/SemaCXX/safety-profile-init-local-member-read.cpp @@ -20,7 +20,7 @@ int leading_unrelated_error = undeclared_identifier; // common-error@-1 {{use of undeclared identifier 'undeclared_identifier'}} struct Agg { - int m [[uninit]]; // expected-note 16 {{member 'm' declared here}} + int m [[uninit]]; // expected-note 18 {{member 'm' declared here}} }; void take_ref(Agg &); // The pointee is uninitialized memory, so the parameter carries the marker @@ -192,16 +192,51 @@ int test_value_initialized_forms() { // source, so such copies stay untracked: a known gap, never a false // positive. Agg make_agg(); -int test_copy_from_untracked_source(Agg other) { - Agg d = other; +int test_copy_from_untracked_source() { Agg e = make_agg(); - return d.m + e.m; // OK: known gap (untracked sources) + return e.m; // OK: known gap (untracked source) +} + +// A by-value parameter is a copy of the caller's argument, and a copy does +// not inherit initialization (§5.2): its marked members are tracked from an +// unassigned start. This is the call-boundary twin of the ctor-body pass's +// deliberate strictness -- the paper hands uninitialized-capable storage +// across calls via marked pointers/references (§4.3), not by-value slots -- +// so a caller-initialized member is rejected all the same; escapes and +// [[profiles::suppress]] are the remedies. +int test_byvalue_parameter_tracked(Agg p) { + return p.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} +} + +int test_byvalue_parameter_assigned(Agg p) { + p.m = 5; + return p.m; // OK +} + +int test_byvalue_parameter_escape(Agg p) { + take_ref(p); + return p.m; // OK: escaped +} + +// Re-passing the parameter by value is an escape like any other bare use +// (the copy-constructor argument reference is not a tracked-copy DeclStmt). +void use_agg(Agg); +int test_byvalue_parameter_repassed(Agg p) { + use_agg(p); + return p.m; // OK: escaped +} + +// A copy from a by-value parameter chains the tracking: the parameter +// starts unassigned, so the copy does too. +int test_copy_from_parameter(Agg other) { + Agg d = other; + return d.m; // expected-error {{member 'm' is read before initialization under profile 'std::init'}} } -// Parameters and references arrive constructed by the caller and are never -// tracked. -int test_parameter_untracked(Agg p, Agg &r) { - return p.m + r.m; // OK +// A reference parameter aliases the caller's own object -- not a copy -- +// and stays untracked. +int test_reference_parameter_untracked(Agg &r) { + return r.m; // OK } // A local that is itself [[uninit]]-marked is the parse-time rules' From 6519490e3bf99964620dc6904013df1f8c04fbe6 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 13:10:25 -0400 Subject: [PATCH 269/289] Exempt system-header code from profile enforcement pending [[profiles::exempt]] --- clang/docs/ProfilesFramework.rst | 8 +++++ clang/include/clang/Basic/LangOptions.def | 1 + clang/include/clang/Options/Options.td | 7 ++++ clang/lib/Driver/ToolChains/Clang.cpp | 3 ++ clang/lib/Sema/SemaProfiles.cpp | 20 +++++++++++ .../fprofiles-exempt-system-headers.cpp | 12 +++++++ ...fety-profile-init-system-header-exempt.cpp | 36 +++++++++++++++++++ 7 files changed, 87 insertions(+) create mode 100644 clang/test/Driver/fprofiles-exempt-system-headers.cpp create mode 100644 clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 1c06d7a702cce..53cc948659a8d 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -699,6 +699,14 @@ Not Yet Implemented ``[[profiles::exempt(...)]]`` (P3589R2 §1.1.6), which would exempt named included source files from the enforcement of a profile, is not implemented. +As a **temporary stopgap** until ``[[profiles::exempt]]`` has wording and an +implementation, Clang exempts code that originates in a *system header* from +profile enforcement. This is on by default whenever a profile is enforced, so +enforcing a profile on a translation unit does not report violations inside the +standard library and the other system headers it transitively includes. Pass +``-fno-profiles-exempt-system-headers`` to disable the exemption and enforce +profiles in system-header code as well (spec-exact behavior). + Extending the Framework ======================= diff --git a/clang/include/clang/Basic/LangOptions.def b/clang/include/clang/Basic/LangOptions.def index 5f797e1c6e5c8..b69975bc574b3 100644 --- a/clang/include/clang/Basic/LangOptions.def +++ b/clang/include/clang/Basic/LangOptions.def @@ -156,6 +156,7 @@ LANGOPT(Modules , 1, 0, NotCompatible, "modules semantics") LANGOPT(CPlusPlusModules , 1, 0, Compatible, "C++ modules syntax") LANGOPT(Profiles , 1, 0, Compatible, "C++ profiles framework") LANGOPT(ProfilesTestProfiles, 1, 0, Benign, "C++ profiles framework built-in test:: profiles") +LANGOPT(ProfilesExemptSystemHeaders, 1, 1, Benign, "exempt system-header code from C++ profile enforcement (temporary stopgap for [[profiles::exempt]])") LANGOPT(SkipODRCheckInGMF , 1, 0, NotCompatible, "Skip ODR checks for decls in the global module fragment") LANGOPT(BuiltinHeadersInSystemModules, 1, 0, NotCompatible, "builtin headers belong to system modules, and _Builtin_ modules are ignored for cstdlib headers") ENUM_LANGOPT(CompilingModule, CompilingModuleKind, 3, CMK_None, Benign, diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td index 34ce80d8cdf8c..6bf99fb642e7c 100644 --- a/clang/include/clang/Options/Options.td +++ b/clang/include/clang/Options/Options.td @@ -1705,6 +1705,13 @@ def fprofiles_test_profiles : Flag<["-"], "fprofiles-test-profiles">, Group, Visibility<[CC1Option]>, HelpText<"Enable the C++ profiles framework's built-in test:: profiles (test suite only)">, MarshallingInfoFlag>; +defm profiles_exempt_system_headers : BoolFOption<"profiles-exempt-system-headers", + LangOpts<"ProfilesExemptSystemHeaders">, DefaultTrue, + PosFlag, + NegFlag>, + ShouldParseIf; defm coro_aligned_allocation : BoolFOption<"coro-aligned-allocation", LangOpts<"CoroAlignedAllocation">, DefaultFalse, diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp index b35d23415b18b..96cece1ca42a7 100644 --- a/clang/lib/Driver/ToolChains/Clang.cpp +++ b/clang/lib/Driver/ToolChains/Clang.cpp @@ -6412,6 +6412,9 @@ void Clang::ConstructJob(Compilation &C, const JobAction &JA, Args.addOptInFlag(CmdArgs, options::OPT_fprofiles, options::OPT_fno_profiles); + Args.addOptOutFlag(CmdArgs, options::OPT_fprofiles_exempt_system_headers, + options::OPT_fno_profiles_exempt_system_headers); + Args.addOptInFlag(CmdArgs, options::OPT_fexperimental_overflow_behavior_types, options::OPT_fno_experimental_overflow_behavior_types); diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index a23a11e05f452..87199319a56ad 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -322,6 +322,20 @@ bool SemaProfiles::isProfileSuppressed(StringRef ProfileName, return isProfileSuppressed(ProfileName, RuleName, AC.getDecl()); } +// Temporary stopgap for the not-yet-implemented [[profiles::exempt]] (P3589R2 +// s1.1.6): exempt code originating in a system header from profile enforcement, +// so enforcing a profile on a translation unit does not diagnose violations +// inside the standard library and the other system headers it transitively +// includes. On by default; -fno-profiles-exempt-system-headers restores +// spec-exact enforcement into system-header code. Remove once +// [[profiles::exempt]] has wording and a real implementation. +static bool isExemptSystemHeaderLoc(const ASTContext &Ctx, + const LangOptions &LangOpts, + SourceLocation Loc) { + return LangOpts.ProfilesExemptSystemHeaders && Loc.isValid() && + Ctx.getSourceManager().isInSystemHeader(Loc); +} + bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, StringRef RuleName, SourceLocation Loc) { @@ -334,6 +348,8 @@ bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, const Decl *D) { if (!isProfileEnforced(ProfileName)) return false; + if (isExemptSystemHeaderLoc(getASTContext(), getLangOpts(), Loc)) + return false; // Honor [[profiles::suppress]] from the parse-time stack and, when a Decl is // available, from the declaration and its lexical parents. The latter does // not depend on a parse-time scope still being active, so finalization checks @@ -390,6 +406,10 @@ bool SemaProfiles::shouldEmitProfileViolation(StringRef ProfileName, AnalysisDeclContext &AC) const { if (!isProfileEnforced(ProfileName)) return false; + SourceLocation Loc = + UseStmt ? UseStmt->getBeginLoc() : AC.getDecl()->getLocation(); + if (isExemptSystemHeaderLoc(getASTContext(), getLangOpts(), Loc)) + return false; if (isProfileSuppressed(ProfileName, RuleName, UseStmt, AC)) return false; return true; diff --git a/clang/test/Driver/fprofiles-exempt-system-headers.cpp b/clang/test/Driver/fprofiles-exempt-system-headers.cpp new file mode 100644 index 0000000000000..ad00ee319aeb6 --- /dev/null +++ b/clang/test/Driver/fprofiles-exempt-system-headers.cpp @@ -0,0 +1,12 @@ +// The system-header exemption is on by default, so the driver forwards +// -fno-profiles-exempt-system-headers to -cc1 only when the user disables it. +// (Patterns match the -fno- spelling specifically; the exemption's cc1 default +// is on, so nothing is emitted otherwise. The bare option name is avoided in +// CHECKs because it also appears in this file's own name in the -### output.) + +// RUN: %clang -### -fprofiles -std=c++23 -c %s 2>&1 | FileCheck -check-prefix=DEFAULT %s +// RUN: %clang -### -fprofiles -fprofiles-exempt-system-headers -std=c++23 -c %s 2>&1 | FileCheck -check-prefix=DEFAULT %s +// RUN: %clang -### -fprofiles -fno-profiles-exempt-system-headers -std=c++23 -c %s 2>&1 | FileCheck -check-prefix=OFF %s + +// DEFAULT-NOT: "-fno-profiles-exempt-system-headers" +// OFF: "-fno-profiles-exempt-system-headers" diff --git a/clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp b/clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp new file mode 100644 index 0000000000000..d19e1beb6a386 --- /dev/null +++ b/clang/test/SemaCXX/safety-profile-init-system-header-exempt.cpp @@ -0,0 +1,36 @@ +// Temporary system-header exemption for profile enforcement (stopgap for the +// not-yet-implemented [[profiles::exempt]], P3589R2 s1.1.6). By default a +// violation originating in a system header is exempt; a violation in a normal +// (-I) header is still enforced. -fno-profiles-exempt-system-headers enforces +// profiles in system-header code too. + +// RUN: rm -rf %t +// RUN: split-file %s %t +// +// Default: exemption on -- only the -I header's violation fires. +// RUN: %clang_cc1 -fsyntax-only -verify=expected -fprofiles -std=c++23 -I %t %t/main.cpp +// +// Exemption off: both the system-header and the -I header violations fire. +// RUN: %clang_cc1 -fsyntax-only -verify=expected,strict -fno-profiles-exempt-system-headers -fprofiles -std=c++23 -I %t %t/main.cpp + +//--- user.h +// A normal header reached via -I is not a system header, so it is enforced in +// both runs. +inline void user_fn() { + int y; // expected-error {{variable 'y' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)y; +} + +//--- sys.h +#pragma clang system_header +// This is a system header: exempt by default, enforced only with +// -fno-profiles-exempt-system-headers. +inline void sys_fn() { + int x; // strict-error {{variable 'x' must be initialized or marked '[[uninit]]' under profile 'std::init'}} + (void)x; +} + +//--- main.cpp +[[profiles::enforce(std::init)]]; +#include "user.h" +#include "sys.h" From 66b9688254d3050e4f616c5b9ba1de0cf54e2c1b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 13:10:41 -0400 Subject: [PATCH 270/289] Enable assertions in the weekly profiles clang build --- .github/workflows/weekly-profiles-release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/weekly-profiles-release.yml b/.github/workflows/weekly-profiles-release.yml index af801aeda60bb..8b53e0f0175d2 100644 --- a/.github/workflows/weekly-profiles-release.yml +++ b/.github/workflows/weekly-profiles-release.yml @@ -27,6 +27,7 @@ jobs: run: | cmake -G Ninja -S llvm -B build \ -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_ENABLE_ASSERTIONS=ON \ -DCMAKE_C_COMPILER=gcc \ -DCMAKE_CXX_COMPILER=g++ \ -DCMAKE_C_COMPILER_LAUNCHER=ccache \ From aeb3bbbb24f630c539cffd608798b0f5a30f38f8 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 13:11:04 -0400 Subject: [PATCH 271/289] Add a weekly CI job compiling Boost with std::init enforced --- .github/workflows/profiles-boost-build.yml | 117 ++++++++++++++++++ .github/workflows/weekly-profiles-release.yml | 16 +++ clang/utils/profiles-boost/prelude.hpp | 10 ++ clang/utils/profiles-boost/user-config.jam | 30 +++++ 4 files changed, 173 insertions(+) create mode 100644 .github/workflows/profiles-boost-build.yml create mode 100644 clang/utils/profiles-boost/prelude.hpp create mode 100644 clang/utils/profiles-boost/user-config.jam diff --git a/.github/workflows/profiles-boost-build.yml b/.github/workflows/profiles-boost-build.yml new file mode 100644 index 0000000000000..e836dda726ac2 --- /dev/null +++ b/.github/workflows/profiles-boost-build.yml @@ -0,0 +1,117 @@ +name: Profiles Boost Build + +# Compiles Boost with the std::init profile enforced, using a clang built by the +# weekly profiles release, and publishes the raw compiler log for inspection. +# +# Normally dispatched by weekly-profiles-release.yml right after the weekly +# release is cut (passing that run's id in clang_run_id). Can also be run +# manually: omit clang_run_id to pull the latest published release instead. + +on: + workflow_dispatch: + inputs: + clang_run_id: + description: "weekly-profiles-release run id to take the clang artifact from (empty = latest release)" + required: false + default: "" + boost_ref: + description: "Boost superproject git ref (branch or tag)" + required: false + default: "master" + +permissions: + actions: read + contents: read + +jobs: + build-boost: + name: Build Boost (std::init enforced) + # ubuntu-24.04 for a newer libstdc++ (cleaner c++23); the 22.04-built clang + # runs fine here. + runs-on: ubuntu-24.04 + steps: + - name: Checkout profiles branch (helpers) + uses: actions/checkout@v4 + + - name: Install a recent libstdc++ + run: sudo apt-get update && sudo apt-get install -y g++-14 + + - name: Download clang (from weekly run) + if: inputs.clang_run_id != '' + uses: actions/download-artifact@v4 + with: + name: clang-profiles-linux-x86_64 + path: clang-dl + run-id: ${{ inputs.clang_run_id }} + github-token: ${{ github.token }} + + - name: Download clang (latest release) + if: inputs.clang_run_id == '' + env: + GH_TOKEN: ${{ github.token }} + run: | + mkdir -p clang-dl + gh release download --repo "${{ github.repository }}" \ + --pattern 'clang-profiles-linux-x86_64.tar.gz' --dir clang-dl + + - name: Unpack clang + run: | + mkdir -p "$RUNNER_TEMP/clang" + tar xzf clang-dl/clang-profiles-linux-x86_64.tar.gz -C "$RUNNER_TEMP/clang" + "$RUNNER_TEMP/clang/bin/clang++" --version + echo "PROFILES_CLANGXX=$RUNNER_TEMP/clang/bin/clang++" >> "$GITHUB_ENV" + echo "PROFILES_PRELUDE=$GITHUB_WORKSPACE/clang/utils/profiles-boost/prelude.hpp" >> "$GITHUB_ENV" + mkdir -p "$GITHUB_WORKSPACE/crash" + echo "PROFILES_CRASH_DIR=$GITHUB_WORKSPACE/crash" >> "$GITHUB_ENV" + + - name: Clone Boost + run: | + git clone --depth 1 --recursive --shallow-submodules \ + --branch "${{ inputs.boost_ref || 'master' }}" \ + https://github.com/boostorg/boost boost + + - name: Bootstrap Boost.Build + working-directory: boost + run: | + ./bootstrap.sh + ./b2 headers + + - name: Build Boost with std::init enforced + working-directory: boost + run: | + set -o pipefail + # Violations are hard errors, so b2 is expected to exit non-zero; the + # log is the deliverable, so never fail the job on that. + ./b2 \ + --user-config="$GITHUB_WORKSPACE/clang/utils/profiles-boost/user-config.jam" \ + toolset=clang-profiles \ + cxxstd=23 \ + testing.execute=off \ + -k -j"$(nproc)" \ + libs/*/test \ + 2>&1 | tee "$GITHUB_WORKSPACE/boost-profiles.log" || true + + - name: Log overview + if: always() + run: | + log="$GITHUB_WORKSPACE/boost-profiles.log" + { + echo "## Boost profiles build" + echo "- Boost ref: \`${{ inputs.boost_ref || 'master' }}\`" + if [ -f "$log" ]; then + echo "- \`error:\` lines: $(grep -c 'error:' "$log" || true)" + echo "- clang crash markers: $(grep -c 'PLEASE submit a bug report' "$log" || true)" + else + echo "- no log produced" + fi + } >> "$GITHUB_STEP_SUMMARY" + + - name: Upload log and crash reproducers + if: always() + uses: actions/upload-artifact@v4 + with: + name: boost-profiles-log + path: | + boost-profiles.log + crash/ + if-no-files-found: warn diff --git a/.github/workflows/weekly-profiles-release.yml b/.github/workflows/weekly-profiles-release.yml index 8b53e0f0175d2..390224c43ab5e 100644 --- a/.github/workflows/weekly-profiles-release.yml +++ b/.github/workflows/weekly-profiles-release.yml @@ -115,3 +115,19 @@ jobs: ### Artifacts - `clang-profiles-linux-x86_64.tar.gz` — Linux x86_64 (Ubuntu 22.04, gcc) - `clang-profiles-windows-x86_64.zip` — Windows x86_64 (MSVC 2022) + + trigger-boost: + name: Trigger Boost profiles build + needs: [create-release] + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Dispatch Boost build against this run's clang + env: + GH_TOKEN: ${{ github.token }} + run: > + gh workflow run profiles-boost-build.yml + --repo ${{ github.repository }} + --ref ${{ github.ref_name }} + -f clang_run_id=${{ github.run_id }} diff --git a/clang/utils/profiles-boost/prelude.hpp b/clang/utils/profiles-boost/prelude.hpp new file mode 100644 index 0000000000000..d7462cc1ad15f --- /dev/null +++ b/clang/utils/profiles-boost/prelude.hpp @@ -0,0 +1,10 @@ +// Force-included (via `clang -include`) at the top of every Boost translation +// unit by the profiles Boost build (.github/workflows/profiles-boost-build.yml) +// so that the std::init profile is enforced across Boost without editing any +// Boost source. +// +// This must contain ONLY the enforcement empty-declaration: a +// [[profiles::enforce(...)]] attribute has to precede every other top-level +// declaration in the translation unit (P3589R2 s1.1.1), which `-include` +// guarantees by inserting this file before the main file's contents. +[[profiles::enforce(std::init)]]; diff --git a/clang/utils/profiles-boost/user-config.jam b/clang/utils/profiles-boost/user-config.jam new file mode 100644 index 0000000000000..cd8cdf99f797e --- /dev/null +++ b/clang/utils/profiles-boost/user-config.jam @@ -0,0 +1,30 @@ +# Boost.Build (b2) toolset for the profiles Boost build +# (.github/workflows/profiles-boost-build.yml). Selected with +# `b2 toolset=clang-profiles`. +# +# The freshly-built profiles clang and the enforcement prelude are read from the +# environment so this committed config works wherever CI unpacks them: +# +# PROFILES_CLANGXX - path to the profiles-enabled clang++ +# PROFILES_PRELUDE - path to prelude.hpp ([[profiles::enforce(std::init)]]) +# PROFILES_CRASH_DIR - directory for clang crash reproducers +# +# `-fprofiles` enables the framework and the prelude enforces std::init; the +# system-header exemption is left at its default (on), so stdlib headers are not +# diagnosed. `-ferror-limit=0` makes each TU report every violation rather than +# stopping after the first 20. + +import os ; + +local clang_cmd = [ os.environ PROFILES_CLANGXX ] ; +local prelude = [ os.environ PROFILES_PRELUDE ] ; +local crash_dir = [ os.environ PROFILES_CRASH_DIR ] ; + +using clang : profiles + : $(clang_cmd) + : -fprofiles + -include + $(prelude) + -ferror-limit=0 + -fcrash-diagnostics-dir=$(crash_dir) + ; From cafa69a7fabb7e0c89e9296d5d8ec8c411da3f64 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 15:11:50 -0400 Subject: [PATCH 272/289] Withdraw before crediting at dual [[now_init]]/[[now_uninit]] calls --- clang/docs/ProfilesFramework.rst | 6 +++++- clang/include/clang/Basic/AttrDocs.td | 5 ++++- clang/lib/Sema/SemaProfiles.cpp | 6 ++++-- .../safety-profile-init-ref-to-uninit.cpp | 18 ++++++++++++++++++ 4 files changed, 31 insertions(+), 4 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 53cc948659a8d..9751237f4b7aa 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -548,7 +548,11 @@ destroy_at(T* p) [[now_uninit]];`` and the construct/destroy/construct cycle is legal, a second destruction is rejected (the storage no longer refers to initialized memory, so binding it to the unmarked parameter is the unmarked-direction violation), and binding the destroyed storage to an -ordinary pointer or reference is rejected the same way. +ordinary pointer or reference is rejected the same way. A function may +carry both attributes -- a reinitializer that destroys and then +reconstructs its argument's storage -- and models destroy-then-construct: +the storage bound to its ``[[ref_to_uninit]]`` parameters is initialized +after the call. Constructors diff --git a/clang/include/clang/Basic/AttrDocs.td b/clang/include/clang/Basic/AttrDocs.td index 7b3d98dce5dbc..987e5ceae489c 100644 --- a/clang/include/clang/Basic/AttrDocs.td +++ b/clang/include/clang/Basic/AttrDocs.td @@ -1039,7 +1039,10 @@ Because the attribute covers *every* pointer or reference parameter, apply it only to functions that end the lifetime of every such argument's storage. It requires at least one parameter of pointer or reference type (it would be vacuous otherwise); this is diagnosed regardless of -``-fprofiles``. +``-fprofiles``. A function may carry both ``[[now_uninit]]`` and +``[[now_init]]``: such a reinitializer models destroy-then-construct, so +the storage bound to its ``[[ref_to_uninit]]`` parameters is initialized +after the call. Outside the initialization profile the attribute is otherwise silently accepted and has no effect, so code that uses it remains valid when compiled diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 87199319a56ad..03979c667d924 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -1429,9 +1429,11 @@ void SemaProfiles::checkInitProfileRefToUninitBinding(SourceLocation Loc, // Run after the check: the binding itself is judged against the // *pre-call* state, and only then does a [[now_init]] callee's promised // initialization -- or a [[now_uninit]] callee's promised destruction -- - // take effect for what follows in parse order. - recordNowInitArgument(Target, T, Src); + // take effect for what follows in parse order. The withdrawal runs before + // the credit so a callee carrying both attributes (a reinitializer) nets + // to destroy-then-construct: the storage is initialized after the call. recordNowUninitArgument(Target, T, Src); + recordNowInitArgument(Target, T, Src); } void SemaProfiles::recordNowInitArgument(const ValueDecl *Target, QualType T, diff --git a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp index 1f0a1cedde4af..bc25431e2732c 100644 --- a/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ref-to-uninit.cpp @@ -1982,6 +1982,24 @@ void test_destroy_through_function_pointer() { (void)q; } +// A callee carrying both markers is a reinitializer: it destroys and then +// constructs its argument's storage, so the net post-call state is +// initialized (withdrawal is recorded before credit). +[[now_init]] [[now_uninit]] void nu_reinit(int *p [[ref_to_uninit]]); +void test_reinit_nets_initialized() { + int u [[uninit]]; + nu_reinit(&u); + int *q = &u; // OK: net state is initialized + nu_wipe(&u); // OK: destroying initialized storage + (void)q; +} +void test_reinit_reverse_direction() { + int u [[uninit]]; + nu_reinit(&u); + int *r [[ref_to_uninit]] = &u; // expected-error {{pointer marked '[[ref_to_uninit]]' must refer to uninitialized memory under profile 'std::init'}} + (void)r; +} + // The reverse direction applies through the assignment funnel too: a // credited marked pointer refers to initialized memory, so assigning it to // another marked pointer is the requires-uninit error -- while an unmarked From f8886047f3df15bc028d53f64350193327dd519b Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 15:15:50 -0400 Subject: [PATCH 273/289] Skip unnamed bit-fields in the union indeterminacy walk --- clang/lib/Sema/SemaProfiles.cpp | 17 ++++++++++++----- clang/test/SemaCXX/safety-profile-init-ctor.cpp | 10 ++++++++++ .../test/SemaCXX/safety-profile-init-union.cpp | 12 ++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index 03979c667d924..c508b9539ea67 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -546,15 +546,22 @@ static bool defaultInitLeavesScalarIndeterminateImpl( return false; // A union's members are mutually exclusive, so the per-member walk below does // not apply. Default-initialization leaves it without an initialized member - // (paper §6.5) unless it has no members, has a user-provided default - // constructor (trusted), or a default member initializer initializes one. + // (paper §6.5) unless it has a user-provided default constructor (trusted), + // a default member initializer initializes one, or it has no members -- + // unnamed bit-fields are not members ([class.bit]) and cannot be named or + // initialized, so a union of only unnamed bit-fields counts as empty. if (RD->isUnion()) { - if (RD->field_empty() || RD->hasUserProvidedDefaultConstructor()) + if (RD->hasUserProvidedDefaultConstructor()) return false; - for (const FieldDecl *F : RD->fields()) + bool AnyMember = false; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField()) + continue; + AnyMember = true; if (F->hasInClassInitializer()) return false; - return true; + } + return AnyMember; } // Break cycles from ill-formed self-containing types (e.g. struct S {S x;}). if (!Visited.insert(RD->getCanonicalDecl()).second) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 6ff0748eedd0d..116a47cd14ca2 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -284,3 +284,13 @@ struct AnonUnionSuppressed { // no-profiles-warning@+1 {{'profiles::suppress' attribute ignored}} [[profiles::suppress(std::init, rule: "ctor_uninit_member")]] AnonUnionSuppressed() {} }; + +// An anonymous union whose only members are unnamed bit-fields has nothing +// for the constructor to initialize: unnamed bit-fields are not members and +// no in-language initializer exists for them. +struct AnonUnionUnnamedBitFieldOnly { + union { + int : 4; + }; + AnonUnionUnnamedBitFieldOnly() {} // OK: nothing to initialize +}; diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index cc65b6abf2d76..ade87c65cc205 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -70,6 +70,18 @@ void test_uninit_union_suppressed() { (void)a; } +// A union whose only members are unnamed bit-fields has nothing that +// default-initialization could leave indeterminate -- unnamed bit-fields are +// not members and no in-language initializer exists for them -- but an +// unnamed bit-field alongside a real member changes nothing. +union BitFieldOnly { int : 4; }; +union BitFieldPlusMember { int : 4; int i; }; +void test_bitfield_only_union() { + BitFieldOnly a; // OK: nothing to initialize + BitFieldPlusMember b; // expected-error {{variable 'b' of union type must be initialized under profile 'std::init'}} + (void)a; (void)b; +} + // A union data member that a constructor leaves uninitialized is diagnosed; one // initialized via its member-initializer is accepted. struct HasUnionMember { From c3f48acd2e17e72f326a0bbe94e803e33ffc151c Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 15:18:38 -0400 Subject: [PATCH 274/289] Exempt all-std::byte unions from the indeterminacy walk --- clang/lib/Sema/SemaProfiles.cpp | 14 +++++++++- .../test/SemaCXX/safety-profile-init-ctor.cpp | 26 +++++++++++++++++++ .../SemaCXX/safety-profile-init-union.cpp | 13 ++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index c508b9539ea67..a1b3638b87378 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -554,14 +554,26 @@ static bool defaultInitLeavesScalarIndeterminateImpl( if (RD->hasUserProvidedDefaultConstructor()) return false; bool AnyMember = false; + bool AllStdByte = true; for (const FieldDecl *F : RD->fields()) { if (F->isUnnamedBitField()) continue; AnyMember = true; if (F->hasInClassInitializer()) return false; + if (!Ctx.getBaseElementType(F->getType())->isStdByteType()) + AllStdByte = false; } - return AnyMember; + // A union of only std::byte members (arrays included) mirrors the scalar + // exemption above: the profile permits std::byte to be uninitialized + // (paper §4.5), so whichever member is active needs no acknowledgement. + if (!AnyMember || AllStdByte) + return false; + // Deliberately not exempted: a member whose type avoids indeterminacy + // only through a trusted user-provided default constructor (union { + // Trusted t; }) -- the union's default-initialization activates no + // member, so that constructor never runs. + return true; } // Break cycles from ill-formed self-containing types (e.g. struct S {S x;}). if (!Visited.insert(RD->getCanonicalDecl()).second) diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index 116a47cd14ca2..b30154cfb7371 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -294,3 +294,29 @@ struct AnonUnionUnnamedBitFieldOnly { }; AnonUnionUnnamedBitFieldOnly() {} // OK: nothing to initialize }; + +// A std::byte-only anonymous union mirrors the scalar std::byte exemption +// (paper §4.5): whichever member is active may be left uninitialized, so +// there is nothing to acknowledge -- matching the anonymous-struct control, +// which the per-member walk already exempts. A non-byte member alongside +// keeps the union flagged. +namespace std { enum class byte : unsigned char {}; } +struct AnonUnionAllByte { + union { + std::byte b; + }; + AnonUnionAllByte() {} // OK: std::byte may be left uninitialized +}; +struct AnonStructByteControl { + struct { + std::byte b; + }; + AnonStructByteControl() {} // OK: already exempt via the member walk +}; +struct AnonUnionMixedByte { + union { // expected-note {{anonymous union declared here}} + std::byte b; + int i; + }; + AnonUnionMixedByte() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} +}; diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index ade87c65cc205..8515e518af210 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -82,6 +82,19 @@ void test_bitfield_only_union() { (void)a; (void)b; } +// A union of only std::byte members (arrays included) mirrors the scalar +// std::byte exemption (paper §4.5): whichever member is active may be left +// uninitialized, so a plain declaration is accepted. A non-byte member +// alongside keeps the union flagged. +namespace std { enum class byte : unsigned char {}; } +union AllByte { std::byte b; std::byte arr[4]; }; +union MixedByte { std::byte b; int i; }; +void test_byte_union() { + AllByte a; // OK: std::byte may be left uninitialized + MixedByte b; // expected-error {{variable 'b' of union type must be initialized under profile 'std::init'}} + (void)a; (void)b; +} + // A union data member that a constructor leaves uninitialized is diagnosed; one // initialized via its member-initializer is accepted. struct HasUnionMember { From ee29811127265ab0194839ac6bb6cb9086ed2041 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 15:22:37 -0400 Subject: [PATCH 275/289] Recurse into NSDMI-activated anonymous variants of unions --- clang/docs/ProfilesFramework.rst | 16 +++++-- clang/lib/Sema/SemaProfiles.cpp | 32 ++++++++++++++ .../test/SemaCXX/safety-profile-init-ctor.cpp | 43 +++++++++++++++++++ .../SemaCXX/safety-profile-init-union.cpp | 11 +++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/clang/docs/ProfilesFramework.rst b/clang/docs/ProfilesFramework.rst index 9751237f4b7aa..9c21b5f4f0ddc 100644 --- a/clang/docs/ProfilesFramework.rst +++ b/clang/docs/ProfilesFramework.rst @@ -264,7 +264,11 @@ initialize its members (§5.1), and a data member that is itself marked ``[[uninit]]`` is acknowledged -- a type whose only indeterminate scalars are all marked does not trigger the rule. Marking a variable of such a type ``[[uninit]]`` is likewise consistent: its default-initialization is a -genuine no-op. +genuine no-op. A union whose default-initialization activates a variant +through a default member initializer -- including initializers written on +the leaves of an anonymous-record variant, provided they initialize the +variant completely -- counts as initialized, and a union of only unnamed +bit-fields or only ``std::byte`` members has nothing to acknowledge (§4.5). Where ``[[uninit]]`` May Not Go @@ -588,9 +592,13 @@ indeterminate (a nested aggregate) is flagged the same way, and the members of an anonymous struct are checked exactly like direct members (a written initializer for one is an indirect member-initializer). An anonymous *union* member instead needs one active member: a written leaf initializer -or a leaf default member initializer satisfies it. A delegating -constructor is exempt -- its target initializes the members -- and so is a -union's own constructor, whose members are mutually exclusive (§5.6). +or a leaf default member initializer satisfies it -- default member +initializers on the leaves of an anonymous-record variant activate that +variant, which satisfies the union when they initialize it completely -- +and an anonymous union of only unnamed bit-fields or only ``std::byte`` +members has nothing to initialize. A delegating constructor is exempt -- +its target initializes the members -- and so is a union's own constructor, +whose members are mutually exclusive (§5.6). Global and Static Variables diff --git a/clang/lib/Sema/SemaProfiles.cpp b/clang/lib/Sema/SemaProfiles.cpp index a1b3638b87378..71b06a52e3b5c 100644 --- a/clang/lib/Sema/SemaProfiles.cpp +++ b/clang/lib/Sema/SemaProfiles.cpp @@ -525,6 +525,28 @@ SemaProfiles::ProfileSuppressScope::~ProfileSuppressScope() { S.Profiles().ProfileSuppressStack.pop_back_n(Count); } +// True if a field of RD -- or, transitively, of an anonymous-record member +// of RD -- carries a default member initializer. Default-initializing a +// union performs the initialization of its single variant member with a +// default member initializer ([class.union.general]p6; at most one variant +// may have one), and initializers written on the leaves of an +// anonymous-record variant activate it the same way. +static bool anyLeafHasNSDMI(const CXXRecordDecl *RD) { + if (!RD->hasDefinition() || RD->isInvalidDecl()) + return false; + for (const FieldDecl *F : RD->fields()) { + if (F->isUnnamedBitField()) + continue; + if (F->hasInClassInitializer()) + return true; + if (F->isAnonymousStructOrUnion()) + if (const auto *FRD = F->getType()->getAsCXXRecordDecl()) + if (anyLeafHasNSDMI(FRD)) + return true; + } + return false; +} + static bool defaultInitLeavesScalarIndeterminateImpl( ASTContext &Ctx, QualType T, bool HonorUninitMarkers, llvm::SmallPtrSetImpl &Visited) { @@ -561,6 +583,16 @@ static bool defaultInitLeavesScalarIndeterminateImpl( AnyMember = true; if (F->hasInClassInitializer()) return false; + // Default member initializers on the leaves of an anonymous-record + // member activate that variant during default-initialization, so the + // union is indeterminate iff the activated variant itself still is + // (returning at the first match is sound: at most one variant may + // carry default member initializers). + if (F->isAnonymousStructOrUnion()) + if (const auto *FRD = F->getType()->getAsCXXRecordDecl(); + FRD && anyLeafHasNSDMI(FRD)) + return defaultInitLeavesScalarIndeterminateImpl( + Ctx, F->getType(), HonorUninitMarkers, Visited); if (!Ctx.getBaseElementType(F->getType())->isStdByteType()) AllStdByte = false; } diff --git a/clang/test/SemaCXX/safety-profile-init-ctor.cpp b/clang/test/SemaCXX/safety-profile-init-ctor.cpp index b30154cfb7371..bb54e97781602 100644 --- a/clang/test/SemaCXX/safety-profile-init-ctor.cpp +++ b/clang/test/SemaCXX/safety-profile-init-ctor.cpp @@ -320,3 +320,46 @@ struct AnonUnionMixedByte { }; AnonUnionMixedByte() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} }; + +// Default member initializers on every leaf of an anonymous-struct variant +// activate it during default-initialization and initialize it completely; +// the static_assert below is the constant-evaluation proof. +struct AnonUnionAllNSDMIVariant { + union { + struct { + int a = 1; + int b = 2; + }; + float f; + }; + constexpr AnonUnionAllNSDMIVariant() {} // OK: the variant is active and complete +}; +static_assert(AnonUnionAllNSDMIVariant().a == 1 && + AnonUnionAllNSDMIVariant().b == 2); + +// A partial-NSDMI variant is activated all the same, but default-init +// leaves its other leaf indeterminate, so the constructor stays diagnosed. +struct AnonUnionPartialNSDMIVariant { + union { // expected-note {{anonymous union declared here}} + struct { + int a = 1; + int b; + }; + float f; + }; + AnonUnionPartialNSDMIVariant() {} // expected-error {{constructor does not initialize any member of the anonymous union under profile 'std::init'}} +}; + +// The activation is visible through nesting: an all-NSDMI anonymous struct +// variant of an anonymous union inside an anonymous struct. +struct AnonNestedNSDMIVariant { + struct { + union { + struct { + int a = 1; + }; + float f; + }; + }; + AnonNestedNSDMIVariant() {} // OK: the nested variant is active and complete +}; diff --git a/clang/test/SemaCXX/safety-profile-init-union.cpp b/clang/test/SemaCXX/safety-profile-init-union.cpp index 8515e518af210..ad19baaaef47c 100644 --- a/clang/test/SemaCXX/safety-profile-init-union.cpp +++ b/clang/test/SemaCXX/safety-profile-init-union.cpp @@ -95,6 +95,17 @@ void test_byte_union() { (void)a; (void)b; } +// Default member initializers on the leaves of an anonymous-struct variant +// activate it during default-initialization: an all-NSDMI variant is +// initialized completely, a partial one leaves its other leaf indeterminate. +union NSDMIVariant { struct { int a = 1; int b = 2; }; float f; }; +union PartialNSDMIVariant { struct { int a = 1; int b; }; float f; }; +void test_nsdmi_variant_union() { + NSDMIVariant a; // OK: default-init activates the complete variant + PartialNSDMIVariant b; // expected-error {{variable 'b' of union type must be initialized under profile 'std::init'}} + (void)a; (void)b; +} + // A union data member that a constructor leaves uninitialized is diagnosed; one // initialized via its member-initializer is accepted. struct HasUnionMember { From c039f0d7f11f4a3668d3b7370657bf28cbc8fda6 Mon Sep 17 00:00:00 2001 From: Krystian Stasiowski Date: Fri, 17 Jul 2026 15:58:37 -0400 Subject: [PATCH 276/289] Summarize std::init profile violations from the Boost build --- .github/workflows/profiles-boost-build.yml | 24 +- clang/utils/profiles-boost/summarize.py | 420 +++++++++++++++++++++ 2 files changed, 431 insertions(+), 13 deletions(-) create mode 100644 clang/utils/profiles-boost/summarize.py diff --git a/.github/workflows/profiles-boost-build.yml b/.github/workflows/profiles-boost-build.yml index e836dda726ac2..890c92e5c8bc7 100644 --- a/.github/workflows/profiles-boost-build.yml +++ b/.github/workflows/profiles-boost-build.yml @@ -91,27 +91,25 @@ jobs: libs/*/test \ 2>&1 | tee "$GITHUB_WORKSPACE/boost-profiles.log" || true - - name: Log overview + - name: Summarize & visualize profile violations if: always() run: | - log="$GITHUB_WORKSPACE/boost-profiles.log" - { - echo "## Boost profiles build" - echo "- Boost ref: \`${{ inputs.boost_ref || 'master' }}\`" - if [ -f "$log" ]; then - echo "- \`error:\` lines: $(grep -c 'error:' "$log" || true)" - echo "- clang crash markers: $(grep -c 'PLEASE submit a bug report' "$log" || true)" - else - echo "- no log produced" - fi - } >> "$GITHUB_STEP_SUMMARY" + echo "_Boost ref: \`${{ inputs.boost_ref || 'master' }}\`_" >> "$GITHUB_STEP_SUMMARY" + python3 clang/utils/profiles-boost/summarize.py \ + --log "$GITHUB_WORKSPACE/boost-profiles.log" \ + --boost-root "$GITHUB_WORKSPACE/boost" \ + --json "$GITHUB_WORKSPACE/summary.json" \ + --html "$GITHUB_WORKSPACE/report.html" \ + >> "$GITHUB_STEP_SUMMARY" - - name: Upload log and crash reproducers + - name: Upload log, report and crash reproducers if: always() uses: actions/upload-artifact@v4 with: name: boost-profiles-log path: | boost-profiles.log + summary.json + report.html crash/ if-no-files-found: warn diff --git a/clang/utils/profiles-boost/summarize.py b/clang/utils/profiles-boost/summarize.py new file mode 100644 index 0000000000000..7c5da00703ea4 --- /dev/null +++ b/clang/utils/profiles-boost/summarize.py @@ -0,0 +1,420 @@ +#!/usr/bin/env python3 +"""Summarize std::init profile violations from a Boost build log. + +Reads the plain-text clang log produced by the profiles Boost build +(.github/workflows/profiles-boost-build.yml), classifies each std::init profile +violation by rule kind and by Boost library, and emits: + + * a Markdown report on stdout (for $GITHUB_STEP_SUMMARY): a mermaid pie of the + rule distribution, a ranked top-libraries table, and a rule x library matrix; + * an optional summary.json with the full aggregates (--json); + * an optional self-contained report.html dashboard (--html). + +Text parsing is used deliberately: the human-readable log is a required +deliverable and per-TU SARIF capture under parallel b2 is not practical. Every +profile diagnostic ends in "under profile 'std::init'", which isolates our +errors and excludes the test:: profiles (which share some wording) and unrelated +Boost/clang errors. See the diagnostic catalog in DiagnosticSemaKinds.td. + +Uses only the Python standard library. +""" + +import argparse +import html +import json +import os +import re +import sys +from collections import Counter, defaultdict + +# A clang diagnostic line: "path:line:col: error: message". +_DIAG_RE = re.compile( + r"^(?P[^:\n]+):(?P\d+):(?P\d+):\s+" + r"(?Perror|warning|note):\s+(?P.*)$" +) +_PROFILE_RE = re.compile(r"under profile '(?P[^']+)'") +_CRASH_MARKER = "PLEASE submit a bug report" + +# Rule classification, applied in order; first match wins. Each entry is +# (rule, predicate(msg)). Ordering matters where messages share substrings +# (e.g. uninit_read's "...read through a '[[ref_to_uninit]]'..." must be caught +# before the ref_to_uninit check, and uninit_write's "does not initialize it" +# before ctor_uninit_member's "constructor does not initialize"). See +# clang/include/clang/Basic/DiagnosticSemaKinds.td for the source messages. +def _has(*needles): + return lambda m: all(n in m for n in needles) + + +def _any(*needles): + return lambda m: any(n in m for n in needles) + + +_RULES = [ + ("uninit_read", _any("is read before initialization", + "accesses uninitialized memory")), + ("uninit_write", _has("does not initialize it")), + ("ctor_uninit_member", _has("constructor does not initialize")), + ("ref_to_uninit", _any("[[ref_to_uninit]]", + "binds a reference to uninitialized memory", + "binds its implicit object parameter")), + ("pointer_marker", _has("'[[uninit]]' cannot be applied to a pointer")), + ("static_marker", _has("'[[uninit]]' cannot be applied to variable", + "storage duration")), + ("union_marker", _has("'[[uninit]]' cannot be applied to", "union")), + ("uninit_with_initializer", + _any("cannot be both '[[uninit]]' and have an initializer", + "default-initialization of its type")), + ("uninit_decl", _any("must be initialized or marked '[[uninit]]'", + "of union type must be initialized")), + ("static_runtime_init", _has("requires constant initialization")), +] + +# Stable order for display (matches _RULES, plus the catch-all). +RULE_ORDER = [name for name, _ in _RULES] + ["other"] + +_LIBS_RE = re.compile(r"(?:^|/)libs/([^/]+)/") + + +def classify_rule(msg): + for name, pred in _RULES: + if pred(msg): + return name + return "other" + + +def _lib_from_path_text(path): + # A compiled-library source: libs//... wins (most precise). + m = _LIBS_RE.search(path) + if m: + return m.group(1) + # A header: the component follows the include root's "boost" directory. Take + # the segment after the LAST "boost" segment so a doubled root like + # ".../boost/boost/container/..." (checkout dir + include dir) yields + # "container", not "boost". Strip the extension for single-file headers + # (boost/optional.hpp -> optional). + parts = path.split("/") + boost_idx = [i for i, p in enumerate(parts) if p == "boost"] + if boost_idx and boost_idx[-1] + 1 < len(parts): + comp = parts[boost_idx[-1] + 1] + return comp.split(".")[0] if "." in comp else comp + return None + + +def library_for_path(path, boost_root, cache): + """Attribute a diagnostic path to a Boost library. + + Headers under boost/ are symlinks into libs//include/..., so resolving + the real path (when boost_root is given) yields precise attribution; fall + back to the textual boost/ heuristic otherwise. + """ + if path in cache: + return cache[path] + candidates = [] + if boost_root: + for base in (boost_root, os.getcwd()): + p = path if os.path.isabs(path) else os.path.join(base, path) + try: + candidates.append(os.path.realpath(p)) + except OSError: + pass + candidates.append(path) + lib = None + for cand in candidates: + lib = _lib_from_path_text(cand) + if lib: + break + lib = lib or "" + cache[path] = lib + return lib + + +def parse_log(lines, boost_root=None): + by_rule = Counter() + by_library = Counter() + matrix = defaultdict(Counter) # library -> rule -> count + by_file = Counter() + files = set() + crash_markers = 0 + non_profile_errors = 0 + cache = {} + + for line in lines: + if _CRASH_MARKER in line: + crash_markers += 1 + m = _DIAG_RE.match(line.rstrip("\n")) + if not m or m.group("level") != "error": + continue + msg = m.group("msg") + pm = _PROFILE_RE.search(msg) + if not pm: + non_profile_errors += 1 + continue + if pm.group("profile") != "std::init": + # A different profile (e.g. a test:: profile); not counted here. + continue + path = m.group("path") + rule = classify_rule(msg) + lib = library_for_path(path, boost_root, cache) + by_rule[rule] += 1 + by_library[lib] += 1 + matrix[lib][rule] += 1 + by_file[path] += 1 + files.add(path) + + total = sum(by_rule.values()) + return { + "total_violations": total, + "files": len(files), + "libraries": len(by_library), + "crash_markers": crash_markers, + "non_profile_errors": non_profile_errors, + "by_rule": {r: by_rule.get(r, 0) for r in RULE_ORDER if by_rule.get(r)}, + "by_library": dict(by_library.most_common()), + "matrix": {lib: dict(counts) for lib, counts in matrix.items()}, + "top_files": by_file.most_common(50), + } + + +# -------------------------------------------------------------------------- +# Markdown (step summary) +# -------------------------------------------------------------------------- +def _bar(count, maximum, width=24): + if maximum <= 0: + return "" + n = max(1, round(count / maximum * width)) + return "█" * n + + +def render_markdown(data, top_libraries=20, matrix_libraries=15): + out = [] + out.append("## std::init profile violations — Boost build\n") + total = data["total_violations"] + if total == 0: + out.append("No `std::init` profile violations found in the log.\n") + if data["non_profile_errors"]: + out.append(f"\n_({data['non_profile_errors']} non-profile " + "errors seen — likely unrelated Boost/clang issues.)_\n") + if data["crash_markers"]: + out.append(f"\n**{data['crash_markers']} clang crash " + "marker(s) detected.**\n") + return "".join(out) + + out.append( + f"**{total}** violations across **{data['files']}** files in " + f"**{data['libraries']}** libraries. \n" + ) + extra = [] + if data["crash_markers"]: + extra.append(f"**{data['crash_markers']} clang crash " + "marker(s)**") + if data["non_profile_errors"]: + extra.append(f"{data['non_profile_errors']} non-profile errors") + if extra: + out.append(" · ".join(extra) + "\n") + + # Rule distribution (mermaid pie). + out.append("\n### By rule kind\n\n") + out.append("```mermaid\npie showData title Violations by rule\n") + for rule, count in sorted(data["by_rule"].items(), + key=lambda kv: kv[1], reverse=True): + out.append(f' "{rule}" : {count}\n') + out.append("```\n") + + # Top libraries. + libs = list(data["by_library"].items())[:top_libraries] + if libs: + maximum = libs[0][1] + out.append("\n### Top libraries\n\n") + out.append("| Library | Violations | |\n|---|--:|:--|\n") + for lib, count in libs: + out.append(f"| `{lib}` | {count} | {_bar(count, maximum)} |\n") + + # Rule x library matrix (top libraries as rows, occurring rules as columns). + present_rules = [r for r in RULE_ORDER if data["by_rule"].get(r)] + mlibs = list(data["by_library"].items())[:matrix_libraries] + if mlibs and present_rules: + out.append(f"\n### Rule × library (top {len(mlibs)})\n\n") + header = "| Library | " + " | ".join(present_rules) + " | Total |\n" + sep = "|---|" + "|".join(["--:"] * (len(present_rules) + 1)) + "|\n" + out.append(header) + out.append(sep) + for lib, total_lib in mlibs: + row = data["matrix"].get(lib, {}) + cells = " | ".join(str(row.get(r, 0)) for r in present_rules) + out.append(f"| `{lib}` | {cells} | {total_lib} |\n") + + return "".join(out) + + +# -------------------------------------------------------------------------- +# HTML dashboard (self-contained; no external resources) +# -------------------------------------------------------------------------- +_HTML_TEMPLATE = """ + + + + +std::init violations — Boost + + + +

std::init profile violations — Boost

+

+
+ +

By rule kind

+
+ +
RuleCountShare
+ +

By library

+
+ +
LibraryViolationsShare
+ +

Rule × library

+
+ + + + + +""" + + +def render_html(data): + payload = json.dumps(data) + # The JSON is embedded in a