[clang] [PAC] Implement authentication for C++ member function pointers (PR #99576)
via cfe-commits
cfe-commits at lists.llvm.org
Fri Jul 19 16:22:45 PDT 2024
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-codegen
Author: Oliver Hunt (ojhunt)
<details>
<summary>Changes</summary>
Introduces type based signing of member function pointers. To support this discrimination schema we no longer emit member function pointer to virtual methods and indices into a vtable but migrate to using thunks. This does mean member function pointers are no longer necessarily directly comparable, however as such comparisons are UB this is acceptable.
We derive the discriminator from the C++ mangling of the type of the pointer being authenticated.
Co-Authored-By: Akira Hatanaka ahatanaka@<!-- -->apple.com
Co-Authored-By: John McCall rjmccall@<!-- -->apple.com
---
Patch is 55.31 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/99576.diff
11 Files Affected:
- (modified) clang/include/clang/AST/ASTContext.h (+1-1)
- (modified) clang/include/clang/Basic/PointerAuthOptions.h (+3)
- (modified) clang/lib/AST/ASTContext.cpp (+7-5)
- (modified) clang/lib/CodeGen/CGCall.cpp (+114-98)
- (modified) clang/lib/CodeGen/CGPointerAuth.cpp (+34)
- (modified) clang/lib/CodeGen/CodeGenFunction.h (+2-1)
- (modified) clang/lib/CodeGen/CodeGenModule.h (+8)
- (modified) clang/lib/CodeGen/ItaniumCXXABI.cpp (+242-12)
- (modified) clang/lib/Frontend/CompilerInvocation.cpp (+2)
- (added) clang/test/CodeGenCXX/ptrauth-member-function-pointer.cpp (+433)
- (modified) clang/test/Preprocessor/ptrauth_feature.c (+9)
``````````diff
diff --git a/clang/include/clang/AST/ASTContext.h b/clang/include/clang/AST/ASTContext.h
index 608bd90fcc3ff..f2a17a56dc201 100644
--- a/clang/include/clang/AST/ASTContext.h
+++ b/clang/include/clang/AST/ASTContext.h
@@ -1287,7 +1287,7 @@ class ASTContext : public RefCountedBase<ASTContext> {
getPointerAuthVTablePointerDiscriminator(const CXXRecordDecl *RD);
/// Return the "other" type-specific discriminator for the given type.
- uint16_t getPointerAuthTypeDiscriminator(QualType T) const;
+ uint16_t getPointerAuthTypeDiscriminator(QualType T);
/// Apply Objective-C protocol qualifiers to the given type.
/// \param allowOnPointerType specifies if we can apply protocol
diff --git a/clang/include/clang/Basic/PointerAuthOptions.h b/clang/include/clang/Basic/PointerAuthOptions.h
index 197d63642ca6d..595322b8271b6 100644
--- a/clang/include/clang/Basic/PointerAuthOptions.h
+++ b/clang/include/clang/Basic/PointerAuthOptions.h
@@ -175,6 +175,9 @@ struct PointerAuthOptions {
/// The ABI for variadic C++ virtual function pointers.
PointerAuthSchema CXXVirtualVariadicFunctionPointers;
+
+ /// The ABI for C++ member function pointers.
+ PointerAuthSchema CXXMemberFunctionPointers;
};
} // end namespace clang
diff --git a/clang/lib/AST/ASTContext.cpp b/clang/lib/AST/ASTContext.cpp
index 90bcbea072e39..7af9ea7105bb0 100644
--- a/clang/lib/AST/ASTContext.cpp
+++ b/clang/lib/AST/ASTContext.cpp
@@ -3407,7 +3407,7 @@ static void encodeTypeForFunctionPointerAuth(const ASTContext &Ctx,
}
}
-uint16_t ASTContext::getPointerAuthTypeDiscriminator(QualType T) const {
+uint16_t ASTContext::getPointerAuthTypeDiscriminator(QualType T) {
assert(!T->isDependentType() &&
"cannot compute type discriminator of a dependent type");
@@ -3417,11 +3417,13 @@ uint16_t ASTContext::getPointerAuthTypeDiscriminator(QualType T) const {
if (T->isFunctionPointerType() || T->isFunctionReferenceType())
T = T->getPointeeType();
- if (T->isFunctionType())
+ if (T->isFunctionType()) {
encodeTypeForFunctionPointerAuth(*this, Out, T);
- else
- llvm_unreachable(
- "type discrimination of non-function type not implemented yet");
+ } else {
+ T = T.getUnqualifiedType();
+ std::unique_ptr<MangleContext> MC(createMangleContext());
+ MC->mangleCanonicalTypeName(T, Out);
+ }
return llvm::getPointerAuthStableSipHash(Str);
}
diff --git a/clang/lib/CodeGen/CGCall.cpp b/clang/lib/CodeGen/CGCall.cpp
index d582aba679ddc..52e61cf37189d 100644
--- a/clang/lib/CodeGen/CGCall.cpp
+++ b/clang/lib/CodeGen/CGCall.cpp
@@ -5034,7 +5034,8 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
ReturnValueSlot ReturnValue,
const CallArgList &CallArgs,
llvm::CallBase **callOrInvoke, bool IsMustTail,
- SourceLocation Loc) {
+ SourceLocation Loc,
+ bool IsVirtualFunctionPointerThunk) {
// FIXME: We no longer need the types from CallArgs; lift up and simplify.
assert(Callee.isOrdinary() || Callee.isVirtual());
@@ -5098,7 +5099,11 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
RawAddress SRetAlloca = RawAddress::invalid();
llvm::Value *UnusedReturnSizePtr = nullptr;
if (RetAI.isIndirect() || RetAI.isInAlloca() || RetAI.isCoerceAndExpand()) {
- if (!ReturnValue.isNull()) {
+ if (IsVirtualFunctionPointerThunk && RetAI.isIndirect()) {
+ SRetPtr = makeNaturalAddressForPointer(CurFn->arg_begin() +
+ IRFunctionArgs.getSRetArgNo(),
+ RetTy, CharUnits::fromQuantity(1));
+ } else if (!ReturnValue.isNull()) {
SRetPtr = ReturnValue.getAddress();
} else {
SRetPtr = CreateMemTemp(RetTy, "tmp", &SRetAlloca);
@@ -5877,119 +5882,130 @@ RValue CodeGenFunction::EmitCall(const CGFunctionInfo &CallInfo,
CallArgs.freeArgumentMemory(*this);
// Extract the return value.
- RValue Ret = [&] {
- switch (RetAI.getKind()) {
- case ABIArgInfo::CoerceAndExpand: {
- auto coercionType = RetAI.getCoerceAndExpandType();
-
- Address addr = SRetPtr.withElementType(coercionType);
-
- assert(CI->getType() == RetAI.getUnpaddedCoerceAndExpandType());
- bool requiresExtract = isa<llvm::StructType>(CI->getType());
+ RValue Ret;
- unsigned unpaddedIndex = 0;
- for (unsigned i = 0, e = coercionType->getNumElements(); i != e; ++i) {
- llvm::Type *eltType = coercionType->getElementType(i);
- if (ABIArgInfo::isPaddingForCoerceAndExpand(eltType)) continue;
- Address eltAddr = Builder.CreateStructGEP(addr, i);
- llvm::Value *elt = CI;
- if (requiresExtract)
- elt = Builder.CreateExtractValue(elt, unpaddedIndex++);
- else
- assert(unpaddedIndex == 0);
- Builder.CreateStore(elt, eltAddr);
+ // If the current function is a virtual function pointer thunk, avoid copying
+ // the return value of the musttail call to a temporary.
+ if (IsVirtualFunctionPointerThunk)
+ Ret = RValue::get(CI);
+ else
+ Ret = [&] {
+ switch (RetAI.getKind()) {
+ case ABIArgInfo::CoerceAndExpand: {
+ auto coercionType = RetAI.getCoerceAndExpandType();
+
+ Address addr = SRetPtr.withElementType(coercionType);
+
+ assert(CI->getType() == RetAI.getUnpaddedCoerceAndExpandType());
+ bool requiresExtract = isa<llvm::StructType>(CI->getType());
+
+ unsigned unpaddedIndex = 0;
+ for (unsigned i = 0, e = coercionType->getNumElements(); i != e; ++i) {
+ llvm::Type *eltType = coercionType->getElementType(i);
+ if (ABIArgInfo::isPaddingForCoerceAndExpand(eltType))
+ continue;
+ Address eltAddr = Builder.CreateStructGEP(addr, i);
+ llvm::Value *elt = CI;
+ if (requiresExtract)
+ elt = Builder.CreateExtractValue(elt, unpaddedIndex++);
+ else
+ assert(unpaddedIndex == 0);
+ Builder.CreateStore(elt, eltAddr);
+ }
+ [[fallthrough]];
}
- [[fallthrough]];
- }
-
- case ABIArgInfo::InAlloca:
- case ABIArgInfo::Indirect: {
- RValue ret = convertTempToRValue(SRetPtr, RetTy, SourceLocation());
- if (UnusedReturnSizePtr)
- PopCleanupBlock();
- return ret;
- }
- case ABIArgInfo::Ignore:
- // If we are ignoring an argument that had a result, make sure to
- // construct the appropriate return value for our caller.
- return GetUndefRValue(RetTy);
+ case ABIArgInfo::InAlloca:
+ case ABIArgInfo::Indirect: {
+ RValue ret = convertTempToRValue(SRetPtr, RetTy, SourceLocation());
+ if (UnusedReturnSizePtr)
+ PopCleanupBlock();
+ return ret;
+ }
- case ABIArgInfo::Extend:
- case ABIArgInfo::Direct: {
- llvm::Type *RetIRTy = ConvertType(RetTy);
- if (RetAI.getCoerceToType() == RetIRTy && RetAI.getDirectOffset() == 0) {
- switch (getEvaluationKind(RetTy)) {
- case TEK_Complex: {
- llvm::Value *Real = Builder.CreateExtractValue(CI, 0);
- llvm::Value *Imag = Builder.CreateExtractValue(CI, 1);
- return RValue::getComplex(std::make_pair(Real, Imag));
- }
- case TEK_Aggregate: {
- Address DestPtr = ReturnValue.getAddress();
- bool DestIsVolatile = ReturnValue.isVolatile();
+ case ABIArgInfo::Ignore:
+ // If we are ignoring an argument that had a result, make sure to
+ // construct the appropriate return value for our caller.
+ return GetUndefRValue(RetTy);
+
+ case ABIArgInfo::Extend:
+ case ABIArgInfo::Direct: {
+ llvm::Type *RetIRTy = ConvertType(RetTy);
+ if (RetAI.getCoerceToType() == RetIRTy &&
+ RetAI.getDirectOffset() == 0) {
+ switch (getEvaluationKind(RetTy)) {
+ case TEK_Complex: {
+ llvm::Value *Real = Builder.CreateExtractValue(CI, 0);
+ llvm::Value *Imag = Builder.CreateExtractValue(CI, 1);
+ return RValue::getComplex(std::make_pair(Real, Imag));
+ }
+ case TEK_Aggregate: {
+ Address DestPtr = ReturnValue.getAddress();
+ bool DestIsVolatile = ReturnValue.isVolatile();
- if (!DestPtr.isValid()) {
- DestPtr = CreateMemTemp(RetTy, "agg.tmp");
- DestIsVolatile = false;
+ if (!DestPtr.isValid()) {
+ DestPtr = CreateMemTemp(RetTy, "agg.tmp");
+ DestIsVolatile = false;
+ }
+ EmitAggregateStore(CI, DestPtr, DestIsVolatile);
+ return RValue::getAggregate(DestPtr);
+ }
+ case TEK_Scalar: {
+ // If the argument doesn't match, perform a bitcast to coerce it.
+ // This can happen due to trivial type mismatches.
+ llvm::Value *V = CI;
+ if (V->getType() != RetIRTy)
+ V = Builder.CreateBitCast(V, RetIRTy);
+ return RValue::get(V);
+ }
}
- EmitAggregateStore(CI, DestPtr, DestIsVolatile);
- return RValue::getAggregate(DestPtr);
+ llvm_unreachable("bad evaluation kind");
}
- case TEK_Scalar: {
- // If the argument doesn't match, perform a bitcast to coerce it. This
- // can happen due to trivial type mismatches.
+
+ // If coercing a fixed vector from a scalable vector for ABI
+ // compatibility, and the types match, use the llvm.vector.extract
+ // intrinsic to perform the conversion.
+ if (auto *FixedDstTy = dyn_cast<llvm::FixedVectorType>(RetIRTy)) {
llvm::Value *V = CI;
- if (V->getType() != RetIRTy)
- V = Builder.CreateBitCast(V, RetIRTy);
- return RValue::get(V);
- }
+ if (auto *ScalableSrcTy =
+ dyn_cast<llvm::ScalableVectorType>(V->getType())) {
+ if (FixedDstTy->getElementType() ==
+ ScalableSrcTy->getElementType()) {
+ llvm::Value *Zero = llvm::Constant::getNullValue(CGM.Int64Ty);
+ V = Builder.CreateExtractVector(FixedDstTy, V, Zero,
+ "cast.fixed");
+ return RValue::get(V);
+ }
+ }
}
- llvm_unreachable("bad evaluation kind");
- }
- // If coercing a fixed vector from a scalable vector for ABI
- // compatibility, and the types match, use the llvm.vector.extract
- // intrinsic to perform the conversion.
- if (auto *FixedDstTy = dyn_cast<llvm::FixedVectorType>(RetIRTy)) {
- llvm::Value *V = CI;
- if (auto *ScalableSrcTy =
- dyn_cast<llvm::ScalableVectorType>(V->getType())) {
- if (FixedDstTy->getElementType() == ScalableSrcTy->getElementType()) {
- llvm::Value *Zero = llvm::Constant::getNullValue(CGM.Int64Ty);
- V = Builder.CreateExtractVector(FixedDstTy, V, Zero, "cast.fixed");
- return RValue::get(V);
- }
+ Address DestPtr = ReturnValue.getValue();
+ bool DestIsVolatile = ReturnValue.isVolatile();
+
+ if (!DestPtr.isValid()) {
+ DestPtr = CreateMemTemp(RetTy, "coerce");
+ DestIsVolatile = false;
}
- }
- Address DestPtr = ReturnValue.getValue();
- bool DestIsVolatile = ReturnValue.isVolatile();
+ // An empty record can overlap other data (if declared with
+ // no_unique_address); omit the store for such types - as there is no
+ // actual data to store.
+ if (!isEmptyRecord(getContext(), RetTy, true)) {
+ // If the value is offset in memory, apply the offset now.
+ Address StorePtr = emitAddressAtOffset(*this, DestPtr, RetAI);
+ CreateCoercedStore(CI, StorePtr, DestIsVolatile, *this);
+ }
- if (!DestPtr.isValid()) {
- DestPtr = CreateMemTemp(RetTy, "coerce");
- DestIsVolatile = false;
+ return convertTempToRValue(DestPtr, RetTy, SourceLocation());
}
- // An empty record can overlap other data (if declared with
- // no_unique_address); omit the store for such types - as there is no
- // actual data to store.
- if (!isEmptyRecord(getContext(), RetTy, true)) {
- // If the value is offset in memory, apply the offset now.
- Address StorePtr = emitAddressAtOffset(*this, DestPtr, RetAI);
- CreateCoercedStore(CI, StorePtr, DestIsVolatile, *this);
+ case ABIArgInfo::Expand:
+ case ABIArgInfo::IndirectAliased:
+ llvm_unreachable("Invalid ABI kind for return argument");
}
- return convertTempToRValue(DestPtr, RetTy, SourceLocation());
- }
-
- case ABIArgInfo::Expand:
- case ABIArgInfo::IndirectAliased:
- llvm_unreachable("Invalid ABI kind for return argument");
- }
-
- llvm_unreachable("Unhandled ABIArgInfo::Kind");
- } ();
+ llvm_unreachable("Unhandled ABIArgInfo::Kind");
+ }();
// Emit the assume_aligned check on the return value.
if (Ret.isScalar() && TargetDecl) {
diff --git a/clang/lib/CodeGen/CGPointerAuth.cpp b/clang/lib/CodeGen/CGPointerAuth.cpp
index 72aed9f24d595..36bbab38e5792 100644
--- a/clang/lib/CodeGen/CGPointerAuth.cpp
+++ b/clang/lib/CodeGen/CGPointerAuth.cpp
@@ -365,6 +365,40 @@ llvm::Constant *CodeGenModule::getFunctionPointer(GlobalDecl GD,
return getFunctionPointer(getRawFunctionPointer(GD, Ty), FuncType);
}
+CGPointerAuthInfo CodeGenModule::getMemberFunctionPointerAuthInfo(QualType FT) {
+ assert(FT->getAs<MemberPointerType>() && "MemberPointerType expected");
+ auto &Schema = getCodeGenOpts().PointerAuth.CXXMemberFunctionPointers;
+ if (!Schema)
+ return CGPointerAuthInfo();
+
+ assert(!Schema.isAddressDiscriminated() &&
+ "function pointers cannot use address-specific discrimination");
+
+ llvm::ConstantInt *Discriminator =
+ getPointerAuthOtherDiscriminator(Schema, GlobalDecl(), FT);
+ return CGPointerAuthInfo(Schema.getKey(), Schema.getAuthenticationMode(),
+ /* IsIsaPointer */ false,
+ /* AuthenticatesNullValues */ false, Discriminator);
+}
+
+llvm::Constant *CodeGenModule::getMemberFunctionPointer(llvm::Constant *Pointer,
+ QualType FT) {
+ if (CGPointerAuthInfo PointerAuth = getMemberFunctionPointerAuthInfo(FT))
+ return getConstantSignedPointer(
+ Pointer, PointerAuth.getKey(), nullptr,
+ cast_or_null<llvm::ConstantInt>(PointerAuth.getDiscriminator()));
+
+ return Pointer;
+}
+
+llvm::Constant *CodeGenModule::getMemberFunctionPointer(const FunctionDecl *FD,
+ llvm::Type *Ty) {
+ QualType FT = FD->getType();
+ FT = getContext().getMemberPointerType(
+ FT, cast<CXXMethodDecl>(FD)->getParent()->getTypeForDecl());
+ return getMemberFunctionPointer(getRawFunctionPointer(FD, Ty), FT);
+}
+
std::optional<PointerAuthQualifier>
CodeGenModule::computeVTPointerAuthentication(const CXXRecordDecl *ThisClass) {
auto DefaultAuthentication = getCodeGenOpts().PointerAuth.CXXVTablePointers;
diff --git a/clang/lib/CodeGen/CodeGenFunction.h b/clang/lib/CodeGen/CodeGenFunction.h
index eebb865e8f42c..ba7b565d97559 100644
--- a/clang/lib/CodeGen/CodeGenFunction.h
+++ b/clang/lib/CodeGen/CodeGenFunction.h
@@ -4374,7 +4374,8 @@ class CodeGenFunction : public CodeGenTypeCache {
RValue EmitCall(const CGFunctionInfo &CallInfo, const CGCallee &Callee,
ReturnValueSlot ReturnValue, const CallArgList &Args,
llvm::CallBase **callOrInvoke, bool IsMustTail,
- SourceLocation Loc);
+ SourceLocation Loc,
+ bool IsVirtualFunctionPointerThunk = false);
RValue EmitCall(const CGFunctionInfo &CallInfo, const CGCallee &Callee,
ReturnValueSlot ReturnValue, const CallArgList &Args,
llvm::CallBase **callOrInvoke = nullptr,
diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h
index 657e681730c3a..284bba823baeb 100644
--- a/clang/lib/CodeGen/CodeGenModule.h
+++ b/clang/lib/CodeGen/CodeGenModule.h
@@ -973,8 +973,16 @@ class CodeGenModule : public CodeGenTypeCache {
llvm::Constant *getFunctionPointer(llvm::Constant *Pointer,
QualType FunctionType);
+ llvm::Constant *getMemberFunctionPointer(const FunctionDecl *FD,
+ llvm::Type *Ty = nullptr);
+
+ llvm::Constant *getMemberFunctionPointer(llvm::Constant *Pointer,
+ QualType FT);
+
CGPointerAuthInfo getFunctionPointerAuthInfo(QualType T);
+ CGPointerAuthInfo getMemberFunctionPointerAuthInfo(QualType FT);
+
CGPointerAuthInfo getPointerAuthInfoForPointeeType(QualType type);
CGPointerAuthInfo getPointerAuthInfoForType(QualType type);
diff --git a/clang/lib/CodeGen/ItaniumCXXABI.cpp b/clang/lib/CodeGen/ItaniumCXXABI.cpp
index 37b436a21fbc0..124c9536c899a 100644
--- a/clang/lib/CodeGen/ItaniumCXXABI.cpp
+++ b/clang/lib/CodeGen/ItaniumCXXABI.cpp
@@ -388,6 +388,9 @@ class ItaniumCXXABI : public CodeGen::CGCXXABI {
bool NeedsVTTParameter(GlobalDecl GD) override;
+ llvm::Constant *
+ getOrCreateVirtualFunctionPointerThunk(const CXXMethodDecl *MD);
+
/**************************** RTTI Uniqueness ******************************/
protected:
@@ -426,6 +429,9 @@ class ItaniumCXXABI : public CodeGen::CGCXXABI {
const CXXRecordDecl *RD) override;
private:
+ llvm::Constant *
+ getSignedVirtualMemberFunctionPointer(const CXXMethodDecl *MD);
+
bool hasAnyUnusedVirtualInlineFunction(const CXXRecordDecl *RD) const {
const auto &VtableLayout =
CGM.getItaniumVTableContext().getVTableLayout(RD);
@@ -835,7 +841,25 @@ CGCallee ItaniumCXXABI::EmitLoadOfMemberFunctionPointer(
CalleePtr->addIncoming(VirtualFn, FnVirtual);
CalleePtr->addIncoming(NonVirtualFn, FnNonVirtual);
- CGCallee Callee(FPT, CalleePtr);
+ CGPointerAuthInfo PointerAuth;
+
+ if (const auto &Schema =
+ CGM.getCodeGenOpts().PointerAuth.CXXMemberFunctionPointers) {
+ llvm::PHINode *DiscriminatorPHI = Builder.CreatePHI(CGF.IntPtrTy, 2);
+ DiscriminatorPHI->addIncoming(llvm::ConstantInt::get(CGF.IntPtrTy, 0),
+ FnVirtual);
+ const auto &AuthInfo =
+ CGM.getMemberFunctionPointerAuthInfo(QualType(MPT, 0));
+ assert(Schema.getKey() == AuthInfo.getKey() &&
+ "Keys for virtual and non-virtual member functions must match");
+ auto *NonVirtualDiscriminator = AuthInfo.getDiscriminator();
+ DiscriminatorPHI->addIncoming(NonVirtualDiscriminator, FnNonVirtual);
+ PointerAuth = CGPointerAuthInfo(
+ Schema.getKey(), Schema.getAuthenticationMode(), Schema.isIsaPointer(),
+ Schema.authenticatesNullValues(), DiscriminatorPHI);
+ }
+
+ CGCallee Callee(FPT, CalleePtr, PointerAuth);
return Callee;
}
@@ -853,6 +877,25 @@ llvm::Value *ItaniumCXXABI::EmitMemberDataPointerAddress(
"memptr.offset");
}
+// See if it's possible to return a constant signed pointer.
+static llvm::Constant *pointerAuthResignConstant(
+ llvm::Value *Ptr, const CGPointerAuthInfo &CurAuthInfo,
+ const CGPointerAuthInfo &NewAuthInfo, CodeGenModule &CGM) {
+ auto *CPA = dyn_cast<llvm::ConstantPtrAuth>(Ptr);
+
+ if (!CPA)
+ return nullptr;
+
+ assert(CPA->getKey()->getZExtValue() == CurAuthInfo.getKey() &&
+ CPA->getAddrDiscriminator()->isZeroValue() &&
+ CPA->getDiscriminator() == CurAuthInfo.getDiscriminator() &&
+ "unexpected key or discriminators");
+
+ return CGM.getConstantSignedPointer(
+ CPA->getPointer(), NewAuthInfo.getKey(), nullptr,
+ cast<llvm::ConstantInt>(NewAuthInfo.getDiscriminator()));
+}
+
/// Perform a bitcast, derived-to-base, or base-to-derived member pointer
/// conversion.
///
@@ -880,21 +923,63 @@ llvm::Value *
ItaniumCXXABI::EmitMemberPointerConversion(CodeGenFunction &CGF,
const CastExpr *E,
llvm::Value *src) {
+ // Use ...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/99576
More information about the cfe-commits
mailing list