Skip to content

Commit 010aede

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 7a24238 commit 010aede

24 files changed

+781
-20
lines changed

clang/include/clang/AST/Type.h

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,6 +1985,10 @@ class alignas(TypeAlignment) Type : public ExtQualsTypeCommonBase {
19851985
LLVM_PREFERRED_TYPE(bool)
19861986
unsigned HasTrailingReturn : 1;
19871987

1988+
/// Whether this function has is a cfi unchecked callee.
1989+
LLVM_PREFERRED_TYPE(bool)
1990+
unsigned CFIUncheckedCallee : 1;
1991+
19881992
/// Extra information which affects how the function is called, like
19891993
/// regparm and the calling convention.
19901994
LLVM_PREFERRED_TYPE(CallingConv)
@@ -2566,6 +2570,8 @@ class alignas(TypeAlignment) Type : public ExtQualsTypeCommonBase {
25662570
bool isSignableIntegerType(const ASTContext &Ctx) const;
25672571
bool isAnyPointerType() const; // Any C pointer or ObjC object pointer
25682572
bool isCountAttributedType() const;
2573+
bool isCFIUncheckedCalleeFunctionType() const;
2574+
bool isPointerToCFIUncheckedCalleeFunctionType() const;
25692575
bool isBlockPointerType() const;
25702576
bool isVoidPointerType() const;
25712577
bool isReferenceType() const;
@@ -4712,6 +4718,10 @@ class FunctionType : public Type {
47124718
/// type.
47134719
bool getNoReturnAttr() const { return getExtInfo().getNoReturn(); }
47144720

4721+
/// Determine whether this is a function prototype that includes the
4722+
/// cfi_unchecked_callee attribute.
4723+
bool getCFIUncheckedCalleeAttr() const;
4724+
47154725
bool getCmseNSCallAttr() const { return getExtInfo().getCmseNSCall(); }
47164726
CallingConv getCallConv() const { return getExtInfo().getCC(); }
47174727
ExtInfo getExtInfo() const { return ExtInfo(FunctionTypeBits.ExtInfo); }
@@ -5250,6 +5260,7 @@ class FunctionProtoType final
52505260
FunctionType::ExtInfo ExtInfo;
52515261
unsigned Variadic : 1;
52525262
unsigned HasTrailingReturn : 1;
5263+
unsigned CFIUncheckedCallee : 1;
52535264
unsigned AArch64SMEAttributes : 9;
52545265
Qualifiers TypeQuals;
52555266
RefQualifierKind RefQualifier = RQ_None;
@@ -5259,19 +5270,25 @@ class FunctionProtoType final
52595270
FunctionEffectsRef FunctionEffects;
52605271

52615272
ExtProtoInfo()
5262-
: Variadic(false), HasTrailingReturn(false),
5273+
: Variadic(false), HasTrailingReturn(false), CFIUncheckedCallee(false),
52635274
AArch64SMEAttributes(SME_NormalFunction) {}
52645275

52655276
ExtProtoInfo(CallingConv CC)
52665277
: ExtInfo(CC), Variadic(false), HasTrailingReturn(false),
5267-
AArch64SMEAttributes(SME_NormalFunction) {}
5278+
CFIUncheckedCallee(false), AArch64SMEAttributes(SME_NormalFunction) {}
52685279

52695280
ExtProtoInfo withExceptionSpec(const ExceptionSpecInfo &ESI) {
52705281
ExtProtoInfo Result(*this);
52715282
Result.ExceptionSpec = ESI;
52725283
return Result;
52735284
}
52745285

5286+
ExtProtoInfo withCFIUncheckedCallee(bool CFIUncheckedCallee) {
5287+
ExtProtoInfo Result(*this);
5288+
Result.CFIUncheckedCallee = CFIUncheckedCallee;
5289+
return Result;
5290+
}
5291+
52755292
bool requiresFunctionProtoTypeExtraBitfields() const {
52765293
return ExceptionSpec.Type == EST_Dynamic ||
52775294
requiresFunctionProtoTypeArmAttributes() ||
@@ -5431,6 +5448,7 @@ class FunctionProtoType final
54315448
EPI.Variadic = isVariadic();
54325449
EPI.EllipsisLoc = getEllipsisLoc();
54335450
EPI.HasTrailingReturn = hasTrailingReturn();
5451+
EPI.CFIUncheckedCallee = hasCFIUncheckedCallee();
54345452
EPI.ExceptionSpec = getExceptionSpecInfo();
54355453
EPI.TypeQuals = getMethodQuals();
54365454
EPI.RefQualifier = getRefQualifier();
@@ -5556,6 +5574,10 @@ class FunctionProtoType final
55565574
/// Whether this function prototype has a trailing return type.
55575575
bool hasTrailingReturn() const { return FunctionTypeBits.HasTrailingReturn; }
55585576

5577+
bool hasCFIUncheckedCallee() const {
5578+
return FunctionTypeBits.CFIUncheckedCallee;
5579+
}
5580+
55595581
Qualifiers getMethodQuals() const {
55605582
if (hasExtQualifiers())
55615583
return *getTrailingObjects<Qualifiers>();
@@ -8253,6 +8275,19 @@ inline bool Type::isObjectPointerType() const {
82538275
return false;
82548276
}
82558277

8278+
inline bool Type::isCFIUncheckedCalleeFunctionType() const {
8279+
if (const auto *Fn = getAs<FunctionProtoType>())
8280+
return Fn->hasCFIUncheckedCallee();
8281+
return false;
8282+
}
8283+
8284+
inline bool Type::isPointerToCFIUncheckedCalleeFunctionType() const {
8285+
QualType Pointee = getPointeeType();
8286+
if (Pointee.isNull())
8287+
return false;
8288+
return Pointee->isCFIUncheckedCalleeFunctionType();
8289+
}
8290+
82568291
inline bool Type::isFunctionPointerType() const {
82578292
if (const auto *T = getAs<PointerType>())
82588293
return T->getPointeeType()->isFunctionType();

clang/include/clang/AST/TypeProperties.td

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,9 @@ let Class = FunctionProtoType in {
317317
def : Property<"trailingReturn", Bool> {
318318
let Read = [{ node->hasTrailingReturn() }];
319319
}
320+
def : Property<"cfiUncheckedCallee", Bool> {
321+
let Read = [{ node->hasCFIUncheckedCallee() }];
322+
}
320323
def : Property<"methodQualifiers", Qualifiers> {
321324
let Read = [{ node->getMethodQuals() }];
322325
}
@@ -353,6 +356,7 @@ let Class = FunctionProtoType in {
353356
epi.ExtInfo = extInfo;
354357
epi.Variadic = variadic;
355358
epi.HasTrailingReturn = trailingReturn;
359+
epi.CFIUncheckedCallee = cfiUncheckedCallee;
356360
epi.TypeQuals = methodQualifiers;
357361
epi.RefQualifier = refQualifier;
358362
epi.ExceptionSpec = exceptionSpecifier;

clang/include/clang/Basic/Attr.td

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3098,6 +3098,11 @@ def NoDeref : TypeAttr {
30983098
let Documentation = [NoDerefDocs];
30993099
}
31003100

3101+
def CFIUncheckedCallee : TypeAttr {
3102+
let Spellings = [Clang<"cfi_unchecked_callee">];
3103+
let Documentation = [CFIUncheckedCalleeDocs];
3104+
}
3105+
31013106
def ReqdWorkGroupSize : InheritableAttr {
31023107
// Does not have a [[]] spelling because it is an OpenCL-related attribute.
31033108
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
@@ -6869,6 +6869,54 @@ for references or Objective-C object pointers.
68696869
}];
68706870
}
68716871

6872+
def CFIUncheckedCalleeDocs : Documentation {
6873+
let Category = DocCatType;
6874+
let Content = [{
6875+
``cfi_unchecked_callee`` is a function type attribute which prevents the compiler from instrumenting
6876+
`Control Flow Integrity <https://clang.llvm.org/docs/ControlFlowIntegrity.html>`_ checks on indirect
6877+
function calls. Specifically, the attribute has the following semantics:
6878+
6879+
1. Indirect calls to a function type with this attribute will not be instrumented with CFI. That is,
6880+
the indirect call will not be checked. Note that this only changes the behavior for indirect calls
6881+
on pointers to function types having this attribute. It does not prevent all indirect function calls
6882+
for a given type from being checked.
6883+
2. All direct references to a function whose type has this attribute will always reference the
6884+
function definition rather than an entry in the CFI jump table.
6885+
3. When a pointer to a function with this attribute is implicitly cast to a pointer to a function
6886+
without this attribute, the compiler will give a warning saying this attribute is discarded. This
6887+
warning can be silenced with an explicit cast. Note an explicit cast just disables the warning, so
6888+
direct references to a function with a ``cfi_unchecked_callee`` attribute will still reference the
6889+
function definition rather than the CFI jump table.
6890+
6891+
.. code-block:: c
6892+
6893+
#define CFI_UNCHECKED_CALLEE __attribute__((cfi_unchecked_callee))
6894+
6895+
void no_cfi() CFI_UNCHECKED_CALLEE {}
6896+
6897+
void (*with_cfi)() = no_cfi; // warning: implicit conversion discards `cfi_unchecked_callee` attribute.
6898+
// `with_cfi` also points to the actual definition of `no_cfi` rather than
6899+
// its jump table entry.
6900+
6901+
void invoke(void (CFI_UNCHECKED_CALLEE *func)()) {
6902+
func(); // CFI will not instrument this indirect call.
6903+
6904+
void (*func2)() = func; // warning: implicit conversion discards `cfi_unchecked_callee` attribute.
6905+
6906+
func2(); // CFI will instrument this indirect call. Users should be careful however because if this
6907+
// references a function with type `cfi_unchecked_callee`, then the CFI check may incorrectly
6908+
// fail because the reference will be to the function definition rather than the CFI jump
6909+
// table entry.
6910+
}
6911+
6912+
This attribute can only be applied on functions or member functions. This attribute can be a good
6913+
alternative to ``no_sanitize("cfi")`` if you only want to disable innstrumentation for specific indirect
6914+
calls rather than applying ``no_sanitize("cfi")`` on the whole function containing indirect call. Note
6915+
that ``cfi_unchecked_attribute`` is a type attribute doesn't disable CFI instrumentation on a function
6916+
body.
6917+
}];
6918+
}
6919+
68726920
def ReinitializesDocs : Documentation {
68736921
let Category = DocCatFunction;
68746922
let Content = [{

clang/include/clang/Basic/DiagnosticGroups.td

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

16261626
def NoDeref : DiagGroup<"noderef">;
16271627

1628+
def CFIUncheckedCallee : DiagGroup<"cfi-unchecked-callee">;
1629+
16281630
// -fbounds-safety and bounds annotation related warnings
16291631
def BoundsSafetyCountedByEltTyUnknownSize :
16301632
DiagGroup<"bounds-safety-counted-by-elt-type-unknown-size">;

clang/include/clang/Basic/DiagnosticSemaKinds.td

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12681,6 +12681,11 @@ def warn_noderef_on_non_pointer_or_array : Warning<
1268112681
def warn_noderef_to_dereferenceable_pointer : Warning<
1268212682
"casting to dereferenceable pointer removes 'noderef' attribute">, InGroup<NoDeref>;
1268312683

12684+
def warn_cast_discards_cfi_unchecked_callee
12685+
: Warning<"implicit conversion from %0 to %1 discards "
12686+
"`cfi_unchecked_callee` attribute">,
12687+
InGroup<CFIUncheckedCallee>;
12688+
1268412689
def err_builtin_launder_invalid_arg : Error<
1268512690
"%select{non-pointer|function pointer|void pointer}0 argument to "
1268612691
"'__builtin_launder' is not allowed">;

clang/include/clang/Sema/Sema.h

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,6 +2645,16 @@ class Sema final : public SemaBase {
26452645
/// void*).
26462646
void DiscardMisalignedMemberAddress(const Type *T, Expr *E);
26472647

2648+
/// Returns true if `From` is a function or pointer to a function with the
2649+
/// `cfi_unchecked_callee` attribute but `To` is a function or pointer to
2650+
/// function without this attribute.
2651+
bool DiscardingCFIUncheckedCallee(QualType From, QualType To) const;
2652+
2653+
/// Returns true if `From` is a function or pointer to a function without the
2654+
/// `cfi_unchecked_callee` attribute but `To` is a function or pointer to
2655+
/// function with this attribute.
2656+
bool AddingCFIUncheckedCallee(QualType From, QualType To) const;
2657+
26482658
/// This function calls Action when it determines that E designates a
26492659
/// misaligned member due to the packed attribute. This is used to emit
26502660
/// local diagnostics like in reference binding.
@@ -10238,9 +10248,15 @@ class Sema final : public SemaBase {
1023810248
bool CStyle, bool &ObjCLifetimeConversion);
1023910249

1024010250
/// Determine whether the conversion from FromType to ToType is a valid
10241-
/// conversion that strips "noexcept" or "noreturn" off the nested function
10242-
/// type.
10243-
bool IsFunctionConversion(QualType FromType, QualType ToType) const;
10251+
/// conversion that strips "noexcept" or "noreturn" or "cfi_unchecked_callee"
10252+
/// off the nested function type. This also checks if "cfi_unchecked_callee"
10253+
/// was added to the function type. If "cfi_unchecked_callee" is added and
10254+
/// `AddingCFIUncheckedCallee` is provided, it will be set to true. The same
10255+
/// thing applies for `DiscardingCFIUncheckedCallee` if the attribute is
10256+
/// discarded.
10257+
bool IsFunctionConversion(QualType FromType, QualType ToType,
10258+
bool *DiscardingCFIUncheckedCallee = nullptr,
10259+
bool *AddingCFIUncheckedCallee = nullptr) const;
1024410260

1024510261
/// Same as `IsFunctionConversion`, but if this would return true, it sets
1024610262
/// `ResultTy` to `ToType`.

clang/lib/AST/Type.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3584,6 +3584,12 @@ QualType QualType::getNonLValueExprType(const ASTContext &Context) const {
35843584
return *this;
35853585
}
35863586

3587+
bool FunctionType::getCFIUncheckedCalleeAttr() const {
3588+
if (const auto *FPT = getAs<FunctionProtoType>())
3589+
return FPT->hasCFIUncheckedCallee();
3590+
return false;
3591+
}
3592+
35873593
StringRef FunctionType::getNameForCallConv(CallingConv CC) {
35883594
switch (CC) {
35893595
case CC_C:
@@ -3675,6 +3681,7 @@ FunctionProtoType::FunctionProtoType(QualType result, ArrayRef<QualType> params,
36753681
FunctionTypeBits.HasExtParameterInfos = !!epi.ExtParameterInfos;
36763682
FunctionTypeBits.Variadic = epi.Variadic;
36773683
FunctionTypeBits.HasTrailingReturn = epi.HasTrailingReturn;
3684+
FunctionTypeBits.CFIUncheckedCallee = epi.CFIUncheckedCallee;
36783685

36793686
if (epi.requiresFunctionProtoTypeExtraBitfields()) {
36803687
FunctionTypeBits.HasExtraBitfields = true;
@@ -3942,6 +3949,7 @@ void FunctionProtoType::Profile(llvm::FoldingSetNodeID &ID, QualType Result,
39423949

39433950
ID.AddInteger((EffectCount << 3) | (HasConds << 2) |
39443951
(epi.AArch64SMEAttributes << 1) | epi.HasTrailingReturn);
3952+
ID.AddInteger(epi.CFIUncheckedCallee);
39453953

39463954
for (unsigned Idx = 0; Idx != EffectCount; ++Idx) {
39473955
ID.AddInteger(epi.FunctionEffects.Effects[Idx].toOpaqueInt32());

clang/lib/AST/TypePrinter.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1049,6 +1049,9 @@ void TypePrinter::printFunctionProtoAfter(const FunctionProtoType *T,
10491049
OS << "))";
10501050
}
10511051

1052+
if (T->hasCFIUncheckedCallee())
1053+
OS << " __attribute__((cfi_unchecked_callee))";
1054+
10521055
if (T->hasTrailingReturn()) {
10531056
OS << " -> ";
10541057
print(T->getReturnType(), OS, StringRef());
@@ -2089,6 +2092,9 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
20892092
case attr::NoDeref:
20902093
OS << "noderef";
20912094
break;
2095+
case attr::CFIUncheckedCallee:
2096+
OS << "cfi_unchecked_callee";
2097+
break;
20922098
case attr::AcquireHandle:
20932099
OS << "acquire_handle";
20942100
break;

clang/lib/CodeGen/CGExpr.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3027,9 +3027,13 @@ static LValue EmitFunctionDeclLValue(CodeGenFunction &CGF, const Expr *E,
30273027
GlobalDecl GD) {
30283028
const FunctionDecl *FD = cast<FunctionDecl>(GD.getDecl());
30293029
llvm::Constant *V = CGF.CGM.getFunctionPointer(GD);
3030+
auto ETy = E->getType();
3031+
if (ETy->isCFIUncheckedCalleeFunctionType()) {
3032+
if (auto *GV = dyn_cast<llvm::GlobalValue>(V))
3033+
V = llvm::NoCFIValue::get(GV);
3034+
}
30303035
CharUnits Alignment = CGF.getContext().getDeclAlign(FD);
3031-
return CGF.MakeAddrLValue(V, E->getType(), Alignment,
3032-
AlignmentSource::Decl);
3036+
return CGF.MakeAddrLValue(V, ETy, Alignment, AlignmentSource::Decl);
30333037
}
30343038

30353039
static LValue EmitCapturedFieldLValue(CodeGenFunction &CGF, const FieldDecl *FD,
@@ -6314,10 +6318,12 @@ RValue CodeGenFunction::EmitCall(QualType CalleeType,
63146318
FD && FD->hasAttr<OpenCLKernelAttr>())
63156319
CGM.getTargetCodeGenInfo().setOCLKernelStubCallingConvention(FnType);
63166320

6321+
bool CFIUnchecked = CalleeType->isPointerToCFIUncheckedCalleeFunctionType();
6322+
63176323
// If we are checking indirect calls and this call is indirect, check that the
63186324
// function pointer is a member of the bit set for the function type.
63196325
if (SanOpts.has(SanitizerKind::CFIICall) &&
6320-
(!TargetDecl || !isa<FunctionDecl>(TargetDecl))) {
6326+
(!TargetDecl || !isa<FunctionDecl>(TargetDecl)) && !CFIUnchecked) {
63216327
SanitizerScope SanScope(this);
63226328
EmitSanitizerStatReport(llvm::SanStat_CFI_ICall);
63236329
ApplyDebugLocation ApplyTrapDI(

clang/lib/CodeGen/CGExprConstant.cpp

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

2239-
if (const auto *FD = dyn_cast<FunctionDecl>(D))
2240-
return PtrAuthSign(CGM.getRawFunctionPointer(FD));
2239+
if (const auto *FD = dyn_cast<FunctionDecl>(D)) {
2240+
llvm::Constant *C = CGM.getRawFunctionPointer(FD);
2241+
if (FD->getType()->isCFIUncheckedCalleeFunctionType())
2242+
C = llvm::NoCFIValue::get(cast<llvm::GlobalValue>(C));
2243+
return PtrAuthSign(C);
2244+
}
22412245

22422246
if (const auto *VD = dyn_cast<VarDecl>(D)) {
22432247
// 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
@@ -520,6 +520,11 @@ llvm::Constant *CodeGenModule::getMemberFunctionPointer(llvm::Constant *Pointer,
520520
Pointer, PointerAuth.getKey(), nullptr,
521521
cast_or_null<llvm::ConstantInt>(PointerAuth.getDiscriminator()));
522522

523+
if (const auto *MFT = dyn_cast<MemberPointerType>(FT.getTypePtr())) {
524+
if (MFT->isPointerToCFIUncheckedCalleeFunctionType())
525+
Pointer = llvm::NoCFIValue::get(cast<llvm::GlobalValue>(Pointer));
526+
}
527+
523528
return Pointer;
524529
}
525530

clang/lib/CodeGen/ItaniumCXXABI.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,17 @@ 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+
BinOp->getRHS()
701+
->getType()
702+
->isPointerToCFIUncheckedCalleeFunctionType())
703+
ShouldEmitCFICheck = false;
704+
}
705+
}
706+
696707
bool ShouldEmitVFEInfo = CGM.getCodeGenOpts().VirtualFunctionElimination &&
697708
CGM.HasHiddenLTOVisibility(RD);
698709
bool ShouldEmitWPDInfo =

0 commit comments

Comments
 (0)