llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT--> @llvm/pr-subscribers-clang-codegen @llvm/pr-subscribers-clang Author: Oliver Hunt (ojhunt) <details> <summary>Changes</summary> [clang][PAC] add support for options parameter to __ptrauth This PR adds support for an 'options' parameter for the __ptrauth qualifier. The initial version only exposes the authehntication modes: * "strip" * "sign-and-strip" * "sign-and-auth" We also support parsing the options but not yet the implementation * "isa-pointer" * "authenticates-null-values" The initial support for authentication mode controls exist to support ABI changes over time, and as a byproduct support basic initial tests for option parsing. --- Patch is 44.28 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/136828.diff 12 Files Affected: - (modified) clang/include/clang/Basic/Attr.td (+3-3) - (modified) clang/include/clang/Basic/DiagnosticParseKinds.td (+2-2) - (modified) clang/include/clang/Basic/DiagnosticSemaKinds.td (+17-1) - (modified) clang/include/clang/Basic/LangOptions.h (+11) - (modified) clang/lib/CodeGen/CGExprConstant.cpp (+17-7) - (modified) clang/lib/Parse/ParseDecl.cpp (+1-1) - (modified) clang/lib/Sema/SemaType.cpp (+150-9) - (added) clang/test/CodeGen/ptrauth-stripping.c (+327) - (modified) clang/test/Parser/ptrauth-qualifier.c (+1-1) - (added) clang/test/Sema/ptrauth-qualifier-options.c (+65) - (modified) clang/test/Sema/ptrauth-qualifier.c (+33-6) - (added) clang/test/SemaCXX/ptrauth-qualifier-constexpr-options.cpp (+65) ``````````diff diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td index 37c80ac90182c..1941bb2d1febc 100644 --- a/clang/include/clang/Basic/Attr.td +++ b/clang/include/clang/Basic/Attr.td @@ -3595,9 +3595,9 @@ def ObjCRequiresPropertyDefs : InheritableAttr { def PointerAuth : TypeAttr { let Spellings = [CustomKeyword<"__ptrauth">]; - let Args = [IntArgument<"Key">, - BoolArgument<"AddressDiscriminated", 1>, - IntArgument<"ExtraDiscriminator", 1>]; + let Args = [IntArgument<"Key">, BoolArgument<"AddressDiscriminated", 1>, + IntArgument<"ExtraDiscriminator", 1>, + StringArgument<"Options", 1>]; let Documentation = [PtrAuthDocs]; } diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td index 3bbdc49946dac..8e0f818714c42 100644 --- a/clang/include/clang/Basic/DiagnosticParseKinds.td +++ b/clang/include/clang/Basic/DiagnosticParseKinds.td @@ -1735,8 +1735,8 @@ def warn_pragma_unroll_cuda_value_in_parens : Warning< "argument to '#pragma unroll' should not be in parentheses in CUDA C/C++">, InGroup<CudaCompat>; -def err_ptrauth_qualifier_bad_arg_count : Error< - "'__ptrauth' qualifier must take between 1 and 3 arguments">; +def err_ptrauth_qualifier_bad_arg_count + : Error<"'__ptrauth' qualifier must take between 1 and 4 arguments">; def warn_cuda_attr_lambda_position : Warning< "nvcc does not allow '__%0__' to appear after the parameter list in lambdas">, diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td index 89b2d664d66a0..0ae2c09b1e4fb 100644 --- a/clang/include/clang/Basic/DiagnosticSemaKinds.td +++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td @@ -1045,6 +1045,23 @@ def err_ptrauth_extra_discriminator_invalid : Error< "invalid extra discriminator flag '%0'; '__ptrauth' requires a value between " "'0' and '%1'">; +// __ptrauth qualifier options string +def note_ptrauth_evaluating_options + : Note<"options parameter evaluated to '%0'">; +def err_ptrauth_invalid_option : Error<"'%0' options parameter %1">; +def err_ptrauth_unknown_authentication_option + : Error<"unknown '%0' authentication option '%1'">; +def err_ptrauth_repeated_authentication_option + : Error<"repeated '%0' authentication %select{mode|option}1%select{, prior mode was '%3'| '%2'}1">; +def note_ptrauth_previous_authentication_option + : Note<"previous '%0' authentication %select{mode|option}1">; +def err_ptrauth_unexpected_option_end + : Error<"unexpected end of options parameter for %0">; +def err_ptrauth_option_unexpected_token + : Error<"unexpected character '%0' in '%1' options">; +def err_ptrauth_option_missing_comma + : Error<"missing comma after '%1' option in '%0' qualifier">; + /// main() // static main() is not an error in C, just in C++. def warn_static_main : Warning<"'main' should not be declared static">, @@ -1735,7 +1752,6 @@ def err_static_assert_requirement_failed : Error< def note_expr_evaluates_to : Note< "expression evaluates to '%0 %1 %2'">; - def subst_user_defined_msg : TextSubstitution< "%select{the message|the expression}0 in " "%select{a static assertion|this asm operand}0">; diff --git a/clang/include/clang/Basic/LangOptions.h b/clang/include/clang/Basic/LangOptions.h index 491e8bee9fd5c..3944946374d30 100644 --- a/clang/include/clang/Basic/LangOptions.h +++ b/clang/include/clang/Basic/LangOptions.h @@ -65,6 +65,17 @@ enum class PointerAuthenticationMode : unsigned { SignAndAuth }; +static constexpr llvm::StringLiteral PointerAuthenticationOptionStrip = "strip"; +static constexpr llvm::StringLiteral PointerAuthenticationOptionSignAndStrip = + "sign-and-strip"; +static constexpr llvm::StringLiteral PointerAuthenticationOptionSignAndAuth = + "sign-and-auth"; +static constexpr llvm::StringLiteral PointerAuthenticationOptionIsaPointer = + "isa-pointer"; +static constexpr llvm::StringLiteral + PointerAuthenticationOptionAuthenticatesNullValues = + "authenticates-null-values"; + /// Bitfields of LangOptions, split out from LangOptions in order to ensure that /// this large collection of bitfields is a trivial class type. class LangOptionsBase { diff --git a/clang/lib/CodeGen/CGExprConstant.cpp b/clang/lib/CodeGen/CGExprConstant.cpp index 6ba45f42db4d1..1de3d8a181343 100644 --- a/clang/lib/CodeGen/CGExprConstant.cpp +++ b/clang/lib/CodeGen/CGExprConstant.cpp @@ -2129,6 +2129,13 @@ class ConstantLValueEmitter : public ConstStmtVisitor<ConstantLValueEmitter, } +static bool shouldSignPointer(const PointerAuthQualifier &PointerAuth) { + PointerAuthenticationMode AuthenticationMode = + PointerAuth.getAuthenticationMode(); + return AuthenticationMode == PointerAuthenticationMode::SignAndStrip || + AuthenticationMode == PointerAuthenticationMode::SignAndAuth; +} + llvm::Constant *ConstantLValueEmitter::tryEmit() { const APValue::LValueBase &base = Value.getLValueBase(); @@ -2162,7 +2169,8 @@ llvm::Constant *ConstantLValueEmitter::tryEmit() { // Apply pointer-auth signing from the destination type. if (PointerAuthQualifier PointerAuth = DestType.getPointerAuth(); - PointerAuth && !result.HasDestPointerAuth) { + PointerAuth && !result.HasDestPointerAuth && + shouldSignPointer(PointerAuth)) { value = Emitter.tryEmitConstantSignedPointer(value, PointerAuth); if (!value) return nullptr; @@ -2210,8 +2218,9 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) { if (D->hasAttr<WeakRefAttr>()) return CGM.GetWeakRefReference(D).getPointer(); - auto PtrAuthSign = [&](llvm::Constant *C) { - if (PointerAuthQualifier PointerAuth = DestType.getPointerAuth()) { + auto PtrAuthSign = [&](llvm::Constant *C, bool IsFunction) { + if (PointerAuthQualifier PointerAuth = DestType.getPointerAuth(); + PointerAuth && shouldSignPointer(PointerAuth)) { C = applyOffset(C); C = Emitter.tryEmitConstantSignedPointer(C, PointerAuth); return ConstantLValue(C, /*applied offset*/ true, /*signed*/ true); @@ -2219,7 +2228,7 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) { CGPointerAuthInfo AuthInfo; - if (EnablePtrAuthFunctionTypeDiscrimination) + if (IsFunction && EnablePtrAuthFunctionTypeDiscrimination) AuthInfo = CGM.getFunctionPointerAuthInfo(DestType); if (AuthInfo) { @@ -2237,17 +2246,18 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) { }; if (const auto *FD = dyn_cast<FunctionDecl>(D)) - return PtrAuthSign(CGM.getRawFunctionPointer(FD)); + return PtrAuthSign(CGM.getRawFunctionPointer(FD), /*IsFunction=*/true); if (const auto *VD = dyn_cast<VarDecl>(D)) { // We can never refer to a variable with local storage. if (!VD->hasLocalStorage()) { if (VD->isFileVarDecl() || VD->hasExternalStorage()) - return CGM.GetAddrOfGlobalVar(VD); + return PtrAuthSign(CGM.GetAddrOfGlobalVar(VD), /*IsFunction=*/false); if (VD->isLocalVarDecl()) { - return CGM.getOrCreateStaticVarDecl( + llvm::Constant *C = CGM.getOrCreateStaticVarDecl( *VD, CGM.getLLVMLinkageVarDefinition(VD)); + return PtrAuthSign(C, /*IsFunction=*/false); } } } diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp index 4fe3565687905..259ce5029271e 100644 --- a/clang/lib/Parse/ParseDecl.cpp +++ b/clang/lib/Parse/ParseDecl.cpp @@ -3428,7 +3428,7 @@ void Parser::ParsePtrauthQualifier(ParsedAttributes &Attrs) { T.consumeClose(); SourceLocation EndLoc = T.getCloseLocation(); - if (ArgExprs.empty() || ArgExprs.size() > 3) { + if (ArgExprs.empty() || ArgExprs.size() > 4) { Diag(KwLoc, diag::err_ptrauth_qualifier_bad_arg_count); return; } diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp index a8e85c885069e..654ac4d903f51 100644 --- a/clang/lib/Sema/SemaType.cpp +++ b/clang/lib/Sema/SemaType.cpp @@ -8350,14 +8350,16 @@ static void HandleNeonVectorTypeAttr(QualType &CurType, const ParsedAttr &Attr, /// Handle the __ptrauth qualifier. static void HandlePtrAuthQualifier(ASTContext &Ctx, QualType &T, const ParsedAttr &Attr, Sema &S) { - - assert((Attr.getNumArgs() > 0 && Attr.getNumArgs() <= 3) && - "__ptrauth qualifier takes between 1 and 3 arguments"); + assert((Attr.getNumArgs() > 0 && Attr.getNumArgs() <= 4) && + "__ptrauth qualifier takes between 1 and 4 arguments"); + StringRef AttrName = Attr.getAttrName()->getName(); Expr *KeyArg = Attr.getArgAsExpr(0); Expr *IsAddressDiscriminatedArg = Attr.getNumArgs() >= 2 ? Attr.getArgAsExpr(1) : nullptr; Expr *ExtraDiscriminatorArg = Attr.getNumArgs() >= 3 ? Attr.getArgAsExpr(2) : nullptr; + Expr *AuthenticationOptionsArg = + Attr.getNumArgs() >= 4 ? Attr.getArgAsExpr(3) : nullptr; unsigned Key; if (S.checkConstantPointerAuthKey(KeyArg, Key)) { @@ -8373,10 +8375,140 @@ static void HandlePtrAuthQualifier(ASTContext &Ctx, QualType &T, IsAddressDiscriminated); IsInvalid |= !S.checkPointerAuthDiscriminatorArg( ExtraDiscriminatorArg, PointerAuthDiscArgKind::Extra, ExtraDiscriminator); + std::string LastAuthenticationMode; + std::optional<PointerAuthenticationMode> AuthenticationMode = std::nullopt; + bool IsIsaPointer = false; + bool AuthenticatesNullValues = false; + + if (AuthenticationOptionsArg && !AuthenticationOptionsArg->containsErrors()) { + StringRef OptionsString; + std::string EvaluatedString; + bool HasEvaluatedOptionsString = false; + const StringLiteral *OptionsStringLiteral = + dyn_cast<StringLiteral>(AuthenticationOptionsArg); + SourceRange AuthenticationOptionsRange = + AuthenticationOptionsArg->getSourceRange(); + bool ReportedEvaluation = false; + auto ReportEvaluationOfExpressionIfNeeded = [&]() { + if (OptionsStringLiteral || !HasEvaluatedOptionsString || + ReportedEvaluation) + return; + ReportedEvaluation = true; + S.Diag(AuthenticationOptionsRange.getBegin(), + diag::note_ptrauth_evaluating_options) + << OptionsString << AuthenticationOptionsRange; + }; + auto DiagnoseInvalidOptionsParameter = [&](llvm::StringRef Reason) { + S.Diag(AuthenticationOptionsRange.getBegin(), + diag::err_ptrauth_invalid_option) + << AttrName << Reason; + Attr.setInvalid(); + IsInvalid = true; + ReportEvaluationOfExpressionIfNeeded(); + }; + if (AuthenticationOptionsArg->isValueDependent() || + AuthenticationOptionsArg->isTypeDependent()) { + DiagnoseInvalidOptionsParameter("is dependent"); + return; + } + if (OptionsStringLiteral) { + OptionsString = OptionsStringLiteral->getString(); + HasEvaluatedOptionsString = true; + } else { + Expr::EvalResult Eval; + bool Result = AuthenticationOptionsArg->EvaluateAsRValue(Eval, Ctx); + if (Result && Eval.Val.isLValue()) { + auto *BaseExpr = Eval.Val.getLValueBase().dyn_cast<const Expr *>(); + const StringLiteral *EvaluatedStringLiteral = + dyn_cast<StringLiteral>(const_cast<Expr *>(BaseExpr)); + if (EvaluatedStringLiteral) { + CharUnits StartOffset = Eval.Val.getLValueOffset(); + EvaluatedString = EvaluatedStringLiteral->getString().drop_front( + StartOffset.getQuantity()); + OptionsString = EvaluatedString; + HasEvaluatedOptionsString = true; + } + } + } + if (!HasEvaluatedOptionsString) { + DiagnoseInvalidOptionsParameter( + "must be a string of comma separated flags"); + return; + } + for (char Ch : OptionsString) { + if (Ch != '-' && Ch != ',' && !isWhitespace(Ch) && !isalpha(Ch)) { + DiagnoseInvalidOptionsParameter("contains invalid characters"); + return; + } + } + HasEvaluatedOptionsString = true; + OptionsString = OptionsString.trim(); + llvm::SmallVector<StringRef> Options; + if (!OptionsString.empty()) + OptionsString.split(Options, ','); + + auto OptionHandler = [&](auto Value, auto *Option, + std::string *LastOption = nullptr) { + return [&, Value, Option, LastOption](StringRef OptionString) { + if (!*Option) { + *Option = Value; + if (LastOption) + *LastOption = OptionString; + return true; + } + bool IsAuthenticationMode = + std::is_same_v<decltype(Value), PointerAuthenticationMode>; + S.Diag(AuthenticationOptionsRange.getBegin(), + diag::err_ptrauth_repeated_authentication_option) + << AttrName << !IsAuthenticationMode << OptionString + << (LastOption ? *LastOption : ""); + return false; + }; + }; - if (IsInvalid) { - Attr.setInvalid(); - return; + for (unsigned Idx = 0; Idx < Options.size(); ++Idx) { + StringRef Option = Options[Idx].trim(); + if (Option.empty()) { + bool IsLastOption = Idx == (Options.size() - 1); + DiagnoseInvalidOptionsParameter( + IsLastOption ? "has a trailing comma" : "contains an empty option"); + continue; + } + auto SelectedHandler = + llvm::StringSwitch<std::function<bool(StringRef)>>(Option) + .Case(PointerAuthenticationOptionStrip, + OptionHandler(PointerAuthenticationMode::Strip, + &AuthenticationMode, &LastAuthenticationMode)) + .Case(PointerAuthenticationOptionSignAndStrip, + OptionHandler(PointerAuthenticationMode::SignAndStrip, + &AuthenticationMode, &LastAuthenticationMode)) + .Case(PointerAuthenticationOptionSignAndAuth, + OptionHandler(PointerAuthenticationMode::SignAndAuth, + &AuthenticationMode, &LastAuthenticationMode)) + .Case(PointerAuthenticationOptionIsaPointer, + OptionHandler(true, + &IsIsaPointer)) + .Case(PointerAuthenticationOptionAuthenticatesNullValues, + OptionHandler(true, + &AuthenticatesNullValues)) + .Default([&](StringRef Option) { + if (size_t WhitespaceIndex = + Option.find_first_of(" \t\n\v\f\r"); + WhitespaceIndex != Option.npos) { + StringRef LeadingOption = Option.slice(0, WhitespaceIndex); + S.Diag(AuthenticationOptionsRange.getBegin(), + diag::err_ptrauth_option_missing_comma) + << AttrName << LeadingOption; + } else { + S.Diag(AuthenticationOptionsRange.getBegin(), + diag::err_ptrauth_unknown_authentication_option) + << AttrName << Option; + } + return false; + }); + if (!SelectedHandler(Option)) + IsInvalid = true; + } } if (!T->isSignableType(Ctx) && !T->isDependentType()) { @@ -8385,6 +8517,9 @@ static void HandlePtrAuthQualifier(ASTContext &Ctx, QualType &T, return; } + if (!AuthenticationMode) + AuthenticationMode = PointerAuthenticationMode::SignAndAuth; + if (T.getPointerAuth()) { S.Diag(Attr.getLoc(), diag::err_ptrauth_qualifier_redundant) << T; Attr.setInvalid(); @@ -8397,13 +8532,19 @@ static void HandlePtrAuthQualifier(ASTContext &Ctx, QualType &T, return; } + if (IsInvalid) { + Attr.setInvalid(); + return; + } + assert((!IsAddressDiscriminatedArg || IsAddressDiscriminated <= 1) && "address discriminator arg should be either 0 or 1"); PointerAuthQualifier Qual = PointerAuthQualifier::Create( - Key, IsAddressDiscriminated, ExtraDiscriminator, - PointerAuthenticationMode::SignAndAuth, /*IsIsaPointer=*/false, - /*AuthenticatesNullValues=*/false); + Key, IsAddressDiscriminated, ExtraDiscriminator, *AuthenticationMode, + IsIsaPointer, AuthenticatesNullValues); + assert(Qual.getAuthenticationMode() == *AuthenticationMode); T = S.Context.getPointerAuthType(T, Qual); + assert(T.getPointerAuth().getAuthenticationMode() == *AuthenticationMode); } /// HandleArmSveVectorBitsTypeAttr - The "arm_sve_vector_bits" attribute is diff --git a/clang/test/CodeGen/ptrauth-stripping.c b/clang/test/CodeGen/ptrauth-stripping.c new file mode 100644 index 0000000000000..4e187d8debdc7 --- /dev/null +++ b/clang/test/CodeGen/ptrauth-stripping.c @@ -0,0 +1,327 @@ +// RUN: %clang_cc1 -triple arm64-apple-ios -fptrauth-calls -fptrauth-intrinsics -emit-llvm %s -o - | FileCheck %s + +typedef void *NonePointer; +typedef void *__ptrauth(1, 1, 101, "strip") StripPointer; +typedef void *__ptrauth(1, 1, 102, "sign-and-strip") SignAndStripPointer; +typedef void *__ptrauth(1, 1, 103, "sign-and-auth") SignAndAuthPointer; +typedef __UINT64_TYPE__ NoneIntptr; +typedef __UINT64_TYPE__ __ptrauth(1, 0, 105, "strip") StripIntptr; +typedef __UINT64_TYPE__ __ptrauth(1, 0, 106, "sign-and-strip") SignAndStripIntptr; +typedef __UINT64_TYPE__ __ptrauth(1, 0, 107, "sign-and-auth") SignAndAuthIntptr; + +NonePointer globalNonePointer = "foo0"; +StripPointer globalStripPointer = "foo1"; +SignAndStripPointer globalSignAndStripPointer = "foo2"; +SignAndAuthPointer globalSignAndAuthPointer = "foo3"; +NoneIntptr globalNoneIntptr = (__UINT64_TYPE__)&globalNonePointer; +StripIntptr globalStripIntptr = (__UINT64_TYPE__)&globalStripPointer; +SignAndStripIntptr globalSignAndStripIntptr = (__UINT64_TYPE__)&globalSignAndStripPointer; +SignAndAuthIntptr globalSignAndAuthIntptr = (__UINT64_TYPE__)&globalSignAndAuthPointer; + +// CHECK: @.str = private unnamed_addr constant [5 x i8] c"foo0\00", align 1 +// CHECK: @globalNonePointer = global ptr @.str, align 8 +// CHECK: @.str.1 = private unnamed_addr constant [5 x i8] c"foo1\00", align 1 +// CHECK: @globalStripPointer = global ptr @.str.1, align 8 +// CHECK: @.str.2 = private unnamed_addr constant [5 x i8] c"foo2\00", align 1 +// CHECK: @globalSignAndStripPointer = global ptr ptrauth (ptr @.str.2, i32 1, i64 102, ptr @globalSignAndStripPointer), align 8 +// CHECK: @.str.3 = private unnamed_addr constant [5 x i8] c"foo3\00", align 1 +// CHECK: @globalSignAndAuthPointer = global ptr ptrauth (ptr @.str.3, i32 1, i64 103, ptr @globalSignAndAuthPointer), align 8 +// CHECK: @globalNoneIntptr = global i64 ptrtoint (ptr @globalNonePointer to i64), align 8 +// CHECK: @globalStripIntptr = global i64 ptrtoint (ptr @globalStripPointer to i64), align 8 +// CHECK: @globalSignAndStripIntptr = global i64 ptrtoint (ptr ptrauth (ptr @globalSignAndStripPointer, i32 1, i64 106) to i64), align 8 +// CHECK: @globalSignAndAuthIntptr = global i64 ptrtoint (ptr ptrauth (ptr @globalSignAndAuthPointer, i32 1, i64 107) to i64), align 8 + +typedef struct { + NonePointer ptr; + NoneIntptr i; +} NoneStruct; +typedef struct { + StripPointer ptr; + StripIntptr i; +} StripStruct; +typedef struct { + SignAndStripPointer ptr; + SignAndStripIntptr i; +} SignAndStripStruct; +typedef struct { + SignAndAuthPointer ptr; + SignAndAuthIntptr i; +} SignAndAuthStruct; + +// CHECK-LABEL: @testNone +NoneStruct testNone(NoneStruct *a, NoneStruct *b, NoneStruct c) { + globalNonePointer += 1; + // CHECK: [[GLOBALP:%.*]] = load ptr, ptr @globalNonePointer + // CHECK: [[GLOBALPP:%.*]] = getelementptr inbounds i8, ptr [[GLOBALP]], i64 1 + // CHECK: store ptr [[GLOBALPP]], ptr @globalNonePointer + globalNoneIntptr += 1; + // CHECK: [[GLOBALI:%.*]] = load i64, ptr @globalNoneIntptr + // CHECK: [[GLOBALIP:%.*]] = add i64 [[GLOBALI]], 1 + // CHECK: store i64 [[GLOBALIP]], ptr @globalNoneIntptr + a->ptr += 1; + // CHECK: [[PTR:%.*]] = load ptr, ptr %a.addr, align 8 + // CHECK: [[PTR_PTR:%.*]] = getelementptr inbounds nuw %struct.NoneStruct, ptr [[PTR]], i32 0, i32 0 + // CHECK: [[PTR:%.*]] = load ptr, ptr [[PTR_PTR]], align 8 + // CHECK: [[AP:%.*]] = getelementptr inbounds i8, ptr [[PTR]], i64 1 + // CHECK: stor... [truncated] `````````` </details> https://github.com/llvm/llvm-project/pull/136828 _______________________________________________ cfe-commits mailing list cfe-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits