Skip to content

Commit ac29169

Browse files
committed
[clang] Function type attribute to prevent CFI instrumentation
This introduces the attribute discussed in https://discourse.llvm.org/t/rfc-function-type-attribute-to-prevent-cfi-instrumentation/85458. The proposed name has been changed from `no_cfi` to `cfi_unchecked_callee` to help differentiate from `no_sanitize("cfi")` more easily. The proposed attribute has the following semantics: 1. Indirect calls to a function type with this attribute will not be instrumented with CFI. That is, the indirect call will not be checked. Note that this only changes the behavior for indirect calls on pointers to function types having this attribute. It does not prevent all indirect function calls for a given type from being checked. 2. All direct references to a function whose type has this attribute will always reference the true function definition rather than an entry in the CFI jump table. 3. When a pointer to a function with this attribute is implicitly cast to a pointer to a function without this attribute, the compiler will give a warning saying this attribute is discarded. This warning can be silenced with an explicit C-style cast or C++ static_cast.
1 parent d0c973a commit ac29169

19 files changed

+584
-7
lines changed

clang/include/clang/AST/Type.h

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2730,6 +2730,15 @@ class alignas(TypeAlignment) Type : public ExtQualsTypeCommonBase {
27302730

27312731
bool isOverloadableType() const;
27322732

2733+
/// Return true if this type is marked with the `cfi_unchecked_callee`
2734+
/// attribute.
2735+
bool hasCFIUncheckedCallee(const ASTContext &) const;
2736+
2737+
/// Return true if this type is a pointer to a function or member function
2738+
/// marked with the `cfi_unchecked_callee` attribute.
2739+
bool isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
2740+
const ASTContext &) const;
2741+
27332742
/// Determine wither this type is a C++ elaborated-type-specifier.
27342743
bool isElaboratedTypeSpecifier() const;
27352744

@@ -8627,6 +8636,40 @@ inline bool Type::isOverloadableType() const {
86278636
!isMemberPointerType();
86288637
}
86298638

8639+
inline bool Type::hasCFIUncheckedCallee(const ASTContext &Context) const {
8640+
// Carefully strip sugar coating the underlying attributed type. We don't
8641+
// want to remove all the sugar because this will remove any wrapping
8642+
// attributed types.
8643+
QualType Ty(this, /*Quals=*/0);
8644+
8645+
while (1) {
8646+
if (Ty->hasAttr(attr::CFIUncheckedCallee))
8647+
return true;
8648+
8649+
QualType Desugared = Ty.getSingleStepDesugaredType(Context);
8650+
8651+
// This means the type has no more sugar.
8652+
if (Ty == Desugared)
8653+
break;
8654+
8655+
Ty = Desugared;
8656+
}
8657+
8658+
return false;
8659+
}
8660+
8661+
inline bool Type::isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
8662+
const ASTContext &Context) const {
8663+
if (const auto *MFT = dyn_cast<MemberPointerType>(this)) {
8664+
if (MFT->isMemberFunctionPointer())
8665+
return MFT->getPointeeType()->hasCFIUncheckedCallee(Context);
8666+
} else if (const auto *PtrTy = dyn_cast<PointerType>(this)) {
8667+
return PtrTy->getPointeeType()->hasCFIUncheckedCallee(Context);
8668+
}
8669+
8670+
return false;
8671+
}
8672+
86308673
/// Determines whether this type is written as a typedef-name.
86318674
inline bool Type::isTypedefNameType() const {
86328675
if (getAs<TypedefType>())

clang/include/clang/Basic/Attr.td

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3053,6 +3053,11 @@ def NoDeref : TypeAttr {
30533053
let Documentation = [NoDerefDocs];
30543054
}
30553055

3056+
def CFIUncheckedCallee : TypeAttr {
3057+
let Spellings = [Clang<"cfi_unchecked_callee">];
3058+
let Documentation = [CFIUncheckedCalleeDocs];
3059+
}
3060+
30563061
def ReqdWorkGroupSize : InheritableAttr {
30573062
// Does not have a [[]] spelling because it is an OpenCL-related attribute.
30583063
let Spellings = [GNU<"reqd_work_group_size">];

clang/include/clang/Basic/AttrDocs.td

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6821,6 +6821,54 @@ for references or Objective-C object pointers.
68216821
}];
68226822
}
68236823

6824+
def CFIUncheckedCalleeDocs : Documentation {
6825+
let Category = DocCatType;
6826+
let Content = [{
6827+
``cfi_unchecked_callee`` is a function type attribute which prevents the compiler from instrumenting
6828+
`Control Flow Integrity <https://clang.llvm.org/docs/ControlFlowIntegrity.html>`_ checks on indirect
6829+
function calls. Specifically, the attribute has the following semantics:
6830+
6831+
1. Indirect calls to a function type with this attribute will not be instrumented with CFI. That is,
6832+
the indirect call will not be checked. Note that this only changes the behavior for indirect calls
6833+
on pointers to function types having this attribute. It does not prevent all indirect function calls
6834+
for a given type from being checked.
6835+
2. All direct references to a function whose type has this attribute will always reference the
6836+
function definition rather than an entry in the CFI jump table.
6837+
3. When a pointer to a function with this attribute is implicitly cast to a pointer to a function
6838+
without this attribute, the compiler will give a warning saying this attribute is discarded. This
6839+
warning can be silenced with an explicit cast. Note an explicit cast just disables the warning, so
6840+
direct references to a function with a ``cfi_unchecked_callee`` attribute will still reference the
6841+
function definition rather than the CFI jump table.
6842+
6843+
.. code-block:: c
6844+
6845+
#define CFI_UNCHECKED_CALLEE __attribute__((cfi_unchecked_callee))
6846+
6847+
void no_cfi() CFI_UNCHECKED_CALLEE {}
6848+
6849+
void (*with_cfi)() = no_cfi; // warning: implicit conversion discards `cfi_unchecked_callee` attribute.
6850+
// `with_cfi` also points to the actual definition of `no_cfi` rather than
6851+
// its jump table entry.
6852+
6853+
void invoke(void (CFI_UNCHECKED_CALLEE *func)()) {
6854+
func(); // CFI will not instrument this indirect call.
6855+
6856+
void (*func2)() = func; // warning: implicit conversion discards `cfi_unchecked_callee` attribute.
6857+
6858+
func2(); // CFI will instrument this indirect call. Users should be careful however because if this
6859+
// references a function with type `cfi_unchecked_callee`, then the CFI check may incorrectly
6860+
// fail because the reference will be to the function definition rather than the CFI jump
6861+
// table entry.
6862+
}
6863+
6864+
This attribute can only be applied on functions or member functions. This attribute can be a good
6865+
alternative to ``no_sanitize("cfi")`` if you only want to disable innstrumentation for specific indirect
6866+
calls rather than applying ``no_sanitize("cfi")`` on the whole function containing indirect call. Note
6867+
that ``cfi_unchecked_attribute`` is a type attribute doesn't disable CFI instrumentation on a function
6868+
body.
6869+
}];
6870+
}
6871+
68246872
def ReinitializesDocs : Documentation {
68256873
let Category = DocCatFunction;
68266874
let Content = [{

clang/include/clang/Basic/DiagnosticGroups.td

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1541,6 +1541,8 @@ def FunctionMultiVersioning
15411541

15421542
def NoDeref : DiagGroup<"noderef">;
15431543

1544+
def CFIUncheckedCallee : DiagGroup<"cfi-unchecked-callee">;
1545+
15441546
// -fbounds-safety and bounds annotation related warnings
15451547
def BoundsSafetyCountedByEltTyUnknownSize :
15461548
DiagGroup<"bounds-safety-counted-by-elt-type-unknown-size">;

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12468,6 +12468,15 @@ def warn_noderef_on_non_pointer_or_array : Warning<
1246812468
def warn_noderef_to_dereferenceable_pointer : Warning<
1246912469
"casting to dereferenceable pointer removes 'noderef' attribute">, InGroup<NoDeref>;
1247012470

12471+
def warn_cfi_unchecked_callee_on_non_function
12472+
: Warning<"use of `cfi_unchecked_callee` on %0; can only be used on "
12473+
"function types">,
12474+
InGroup<CFIUncheckedCallee>;
12475+
def warn_cast_discards_cfi_unchecked_callee
12476+
: Warning<"implicit conversion from %0 to %1 discards "
12477+
"`cfi_unchecked_callee` attribute">,
12478+
InGroup<CFIUncheckedCallee>;
12479+
1247112480
def err_builtin_launder_invalid_arg : Error<
1247212481
"%select{non-pointer|function pointer|void pointer}0 argument to "
1247312482
"'__builtin_launder' is not allowed">;

clang/lib/AST/TypePrinter.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,6 +2088,9 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
20882088
case attr::NoDeref:
20892089
OS << "noderef";
20902090
break;
2091+
case attr::CFIUncheckedCallee:
2092+
OS << "cfi_unchecked_callee";
2093+
break;
20912094
case attr::AcquireHandle:
20922095
OS << "acquire_handle";
20932096
break;

clang/lib/CodeGen/CGExpr.cpp

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2973,6 +2973,10 @@ static LValue EmitFunctionDeclLValue(CodeGenFunction &CGF, const Expr *E,
29732973
GlobalDecl GD) {
29742974
const FunctionDecl *FD = cast<FunctionDecl>(GD.getDecl());
29752975
llvm::Constant *V = CGF.CGM.getFunctionPointer(GD);
2976+
if (E->getType()->hasAttr(attr::CFIUncheckedCallee)) {
2977+
if (auto *GV = dyn_cast<llvm::GlobalValue>(V))
2978+
V = llvm::NoCFIValue::get(GV);
2979+
}
29762980
CharUnits Alignment = CGF.getContext().getDeclAlign(FD);
29772981
return CGF.MakeAddrLValue(V, E->getType(), Alignment,
29782982
AlignmentSource::Decl);
@@ -6064,15 +6068,15 @@ LValue CodeGenFunction::EmitStmtExprLValue(const StmtExpr *E) {
60646068
AlignmentSource::Decl);
60656069
}
60666070

6067-
RValue CodeGenFunction::EmitCall(QualType CalleeType,
6071+
RValue CodeGenFunction::EmitCall(QualType OriginalCalleeType,
60686072
const CGCallee &OrigCallee, const CallExpr *E,
60696073
ReturnValueSlot ReturnValue,
60706074
llvm::Value *Chain,
60716075
llvm::CallBase **CallOrInvoke,
60726076
CGFunctionInfo const **ResolvedFnInfo) {
60736077
// Get the actual function type. The callee type will always be a pointer to
60746078
// function type or a block pointer type.
6075-
assert(CalleeType->isFunctionPointerType() &&
6079+
assert(OriginalCalleeType->isFunctionPointerType() &&
60766080
"Call must have function pointer type!");
60776081

60786082
const Decl *TargetDecl =
@@ -6082,7 +6086,7 @@ RValue CodeGenFunction::EmitCall(QualType CalleeType,
60826086
!cast<FunctionDecl>(TargetDecl)->isImmediateFunction()) &&
60836087
"trying to emit a call to an immediate function");
60846088

6085-
CalleeType = getContext().getCanonicalType(CalleeType);
6089+
QualType CalleeType = getContext().getCanonicalType(OriginalCalleeType);
60866090

60876091
auto PointeeType = cast<PointerType>(CalleeType)->getPointeeType();
60886092

@@ -6168,10 +6172,16 @@ RValue CodeGenFunction::EmitCall(QualType CalleeType,
61686172
FD && FD->hasAttr<OpenCLKernelAttr>())
61696173
CGM.getTargetCodeGenInfo().setOCLKernelStubCallingConvention(FnType);
61706174

6175+
// Use the original callee type because the canonical type will have
6176+
// attributes stripped.
6177+
bool CFIUnchecked =
6178+
OriginalCalleeType->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
6179+
CGM.getContext());
6180+
61716181
// If we are checking indirect calls and this call is indirect, check that the
61726182
// function pointer is a member of the bit set for the function type.
61736183
if (SanOpts.has(SanitizerKind::CFIICall) &&
6174-
(!TargetDecl || !isa<FunctionDecl>(TargetDecl))) {
6184+
(!TargetDecl || !isa<FunctionDecl>(TargetDecl)) && !CFIUnchecked) {
61756185
SanitizerScope SanScope(this);
61766186
EmitSanitizerStatReport(llvm::SanStat_CFI_ICall);
61776187

clang/lib/CodeGen/CGExprConstant.cpp

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2219,8 +2219,12 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) {
22192219
return ConstantLValue(C);
22202220
};
22212221

2222-
if (const auto *FD = dyn_cast<FunctionDecl>(D))
2223-
return PtrAuthSign(CGM.getRawFunctionPointer(FD));
2222+
if (const auto *FD = dyn_cast<FunctionDecl>(D)) {
2223+
llvm::Constant *C = CGM.getRawFunctionPointer(FD);
2224+
if (FD->getType()->hasAttr(attr::CFIUncheckedCallee))
2225+
C = llvm::NoCFIValue::get(cast<llvm::GlobalValue>(C));
2226+
return PtrAuthSign(C);
2227+
}
22242228

22252229
if (const auto *VD = dyn_cast<VarDecl>(D)) {
22262230
// We can never refer to a variable with local storage.

clang/lib/CodeGen/CGPointerAuth.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,11 @@ llvm::Constant *CodeGenModule::getMemberFunctionPointer(llvm::Constant *Pointer,
388388
Pointer, PointerAuth.getKey(), nullptr,
389389
cast_or_null<llvm::ConstantInt>(PointerAuth.getDiscriminator()));
390390

391+
if (const auto *MFT = dyn_cast<MemberPointerType>(FT.getTypePtr())) {
392+
if (MFT->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(Context))
393+
Pointer = llvm::NoCFIValue::get(cast<llvm::GlobalValue>(Pointer));
394+
}
395+
391396
return Pointer;
392397
}
393398

clang/lib/CodeGen/ItaniumCXXABI.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,19 @@ CGCallee ItaniumCXXABI::EmitLoadOfMemberFunctionPointer(
693693
llvm::Constant *CheckTypeDesc;
694694
bool ShouldEmitCFICheck = CGF.SanOpts.has(SanitizerKind::CFIMFCall) &&
695695
CGM.HasHiddenLTOVisibility(RD);
696+
697+
if (ShouldEmitCFICheck) {
698+
if (const auto *BinOp = dyn_cast<BinaryOperator>(E)) {
699+
if (BinOp->isPtrMemOp()) {
700+
if (BinOp->getRHS()
701+
->getType()
702+
->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
703+
CGM.getContext()))
704+
ShouldEmitCFICheck = false;
705+
}
706+
}
707+
}
708+
696709
bool ShouldEmitVFEInfo = CGM.getCodeGenOpts().VirtualFunctionElimination &&
697710
CGM.HasHiddenLTOVisibility(RD);
698711
bool ShouldEmitWPDInfo =

clang/lib/Sema/SemaDeclAttr.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -916,6 +916,18 @@ static void handleDiagnoseIfAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
916916
cast<NamedDecl>(D)));
917917
}
918918

919+
static void handleCFIUncheckedCalleeAttr(Sema &S, Decl *D,
920+
const ParsedAttr &AL) {
921+
// Just check for TagDecls (structs/classes) here. All other types are
922+
// diagnosed elsewhere in Sema.
923+
if (const auto *TD = dyn_cast<TagDecl>(D)) {
924+
if (!D->getFunctionType() && !TD->isDependentType()) {
925+
S.Diag(AL.getLoc(), diag::warn_cfi_unchecked_callee_on_non_function)
926+
<< QualType(TD->getTypeForDecl(), /*Quals=*/0);
927+
}
928+
}
929+
}
930+
919931
static void handleNoBuiltinAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
920932
static constexpr const StringRef kWildcard = "*";
921933

@@ -7103,6 +7115,9 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
71037115
case ParsedAttr::AT_NoBuiltin:
71047116
handleNoBuiltinAttr(S, D, AL);
71057117
break;
7118+
case ParsedAttr::AT_CFIUncheckedCallee:
7119+
handleCFIUncheckedCalleeAttr(S, D, AL);
7120+
break;
71067121
case ParsedAttr::AT_ExtVectorType:
71077122
handleExtVectorTypeAttr(S, D, AL);
71087123
break;

clang/lib/Sema/SemaExpr.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9837,6 +9837,17 @@ Sema::CheckSingleAssignmentConstraints(QualType LHSType, ExprResult &CallerRHS,
98379837
Sema::AssignConvertType result =
98389838
CheckAssignmentConstraints(LHSType, RHS, Kind, ConvertRHS);
98399839

9840+
if (const auto *IC = dyn_cast_or_null<ImplicitCastExpr>(RHS.get())) {
9841+
if (result != Incompatible && !IC->isPartOfExplicitCast() &&
9842+
IC->getType()->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
9843+
Context) &&
9844+
!LHSType->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
9845+
Context)) {
9846+
Diag(IC->getExprLoc(), diag::warn_cast_discards_cfi_unchecked_callee)
9847+
<< IC->getType() << LHSType;
9848+
}
9849+
}
9850+
98409851
// C99 6.5.16.1p2: The value of the right operand is converted to the
98419852
// type of the assignment expression.
98429853
// CheckAssignmentConstraints allows the left-hand side to be a reference,

clang/lib/Sema/SemaExprCXX.cpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4619,6 +4619,16 @@ Sema::PerformImplicitConversion(Expr *From, QualType ToType,
46194619
return ExprError();
46204620
}
46214621

4622+
if (CCK == CheckedConversionKind::Implicit) {
4623+
if (From->getType()->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
4624+
Context) &&
4625+
!ToType->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(
4626+
Context)) {
4627+
Diag(From->getExprLoc(), diag::warn_cast_discards_cfi_unchecked_callee)
4628+
<< From->getType() << ToType;
4629+
}
4630+
}
4631+
46224632
// Everything went well.
46234633
return From;
46244634
}
@@ -7920,10 +7930,25 @@ QualType Sema::FindCompositePointerType(SourceLocation Loc,
79207930
EPI1.ExceptionSpec, EPI2.ExceptionSpec, ExceptionTypeStorage,
79217931
getLangOpts().CPlusPlus17);
79227932

7933+
// `cfi_unchecked_callee` needs to be preserved to prevent diagnosing on
7934+
// implicit casts when finding common types for binary operations. If
7935+
// one of the operands has the attribute, let's make the common type
7936+
// have it also.
7937+
bool HasCFIUncheckedCallee =
7938+
Composite1->hasCFIUncheckedCallee(Context) ||
7939+
Composite2->hasCFIUncheckedCallee(Context);
7940+
79237941
Composite1 = Context.getFunctionType(FPT1->getReturnType(),
79247942
FPT1->getParamTypes(), EPI1);
79257943
Composite2 = Context.getFunctionType(FPT2->getReturnType(),
79267944
FPT2->getParamTypes(), EPI2);
7945+
7946+
if (HasCFIUncheckedCallee) {
7947+
Composite1 = Context.getAttributedType(attr::CFIUncheckedCallee,
7948+
Composite1, Composite1);
7949+
Composite2 = Context.getAttributedType(attr::CFIUncheckedCallee,
7950+
Composite2, Composite2);
7951+
}
79277952
}
79287953
}
79297954
}

clang/lib/Sema/SemaInit.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7936,7 +7936,7 @@ ExprResult InitializationSequence::Perform(Sema &S,
79367936
break;
79377937
}
79387938

7939-
case SK_BindReference:
7939+
case SK_BindReference: {
79407940
// Reference binding does not have any corresponding ASTs.
79417941

79427942
// Check exception specifications
@@ -7957,7 +7957,21 @@ ExprResult InitializationSequence::Perform(Sema &S,
79577957
}
79587958

79597959
CheckForNullPointerDereference(S, CurInit.get());
7960+
7961+
QualType InitTy = CurInit.get()->getType();
7962+
const ASTContext &Ctx = S.Context;
7963+
bool DiscardingCFIUnchecked =
7964+
InitTy->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(Ctx) &&
7965+
!DestType->isPointerToCFIUncheckedCalleeFunctionOrMemberFunction(Ctx);
7966+
DiscardingCFIUnchecked |= InitTy->hasCFIUncheckedCallee(Ctx) &&
7967+
!DestType->hasCFIUncheckedCallee(Ctx);
7968+
if (DiscardingCFIUnchecked) {
7969+
S.Diag(CurInit.get()->getExprLoc(),
7970+
diag::warn_cast_discards_cfi_unchecked_callee)
7971+
<< CurInit.get()->getType() << DestType;
7972+
}
79607973
break;
7974+
}
79617975

79627976
case SK_BindReferenceToTemporary: {
79637977
// Make sure the "temporary" is actually an rvalue.

0 commit comments

Comments
 (0)