Skip to content

[clang] Function type attribute to prevent CFI instrumentation #135836

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions clang/include/clang/AST/Type.h
Original file line number Diff line number Diff line change
Expand Up @@ -1985,6 +1985,10 @@ class alignas(TypeAlignment) Type : public ExtQualsTypeCommonBase {
LLVM_PREFERRED_TYPE(bool)
unsigned HasTrailingReturn : 1;

/// Whether this function has is a cfi unchecked callee.
LLVM_PREFERRED_TYPE(bool)
unsigned CFIUncheckedCallee : 1;

/// Extra information which affects how the function is called, like
/// regparm and the calling convention.
LLVM_PREFERRED_TYPE(CallingConv)
Expand Down Expand Up @@ -2566,6 +2570,8 @@ class alignas(TypeAlignment) Type : public ExtQualsTypeCommonBase {
bool isSignableIntegerType(const ASTContext &Ctx) const;
bool isAnyPointerType() const; // Any C pointer or ObjC object pointer
bool isCountAttributedType() const;
bool isCFIUncheckedCalleeFunctionType() const;
bool hasPointeeToToCFIUncheckedCalleeFunctionType() const;
bool isBlockPointerType() const;
bool isVoidPointerType() const;
bool isReferenceType() const;
Expand Down Expand Up @@ -4713,6 +4719,10 @@ class FunctionType : public Type {
/// type.
bool getNoReturnAttr() const { return getExtInfo().getNoReturn(); }

/// Determine whether this is a function prototype that includes the
/// cfi_unchecked_callee attribute.
bool getCFIUncheckedCalleeAttr() const;

bool getCmseNSCallAttr() const { return getExtInfo().getCmseNSCall(); }
CallingConv getCallConv() const { return getExtInfo().getCC(); }
ExtInfo getExtInfo() const { return ExtInfo(FunctionTypeBits.ExtInfo); }
Expand Down Expand Up @@ -5249,8 +5259,12 @@ class FunctionProtoType final
/// the various bits of extra information about a function prototype.
struct ExtProtoInfo {
FunctionType::ExtInfo ExtInfo;
LLVM_PREFERRED_TYPE(bool)
unsigned Variadic : 1;
LLVM_PREFERRED_TYPE(bool)
unsigned HasTrailingReturn : 1;
LLVM_PREFERRED_TYPE(bool)
unsigned CFIUncheckedCallee : 1;
unsigned AArch64SMEAttributes : 9;
Qualifiers TypeQuals;
RefQualifierKind RefQualifier = RQ_None;
Expand All @@ -5260,19 +5274,25 @@ class FunctionProtoType final
FunctionEffectsRef FunctionEffects;

ExtProtoInfo()
: Variadic(false), HasTrailingReturn(false),
: Variadic(false), HasTrailingReturn(false), CFIUncheckedCallee(false),
AArch64SMEAttributes(SME_NormalFunction) {}

ExtProtoInfo(CallingConv CC)
: ExtInfo(CC), Variadic(false), HasTrailingReturn(false),
AArch64SMEAttributes(SME_NormalFunction) {}
CFIUncheckedCallee(false), AArch64SMEAttributes(SME_NormalFunction) {}

ExtProtoInfo withExceptionSpec(const ExceptionSpecInfo &ESI) {
ExtProtoInfo Result(*this);
Result.ExceptionSpec = ESI;
return Result;
}

ExtProtoInfo withCFIUncheckedCallee(bool CFIUncheckedCallee) {
ExtProtoInfo Result(*this);
Result.CFIUncheckedCallee = CFIUncheckedCallee;
return Result;
}

bool requiresFunctionProtoTypeExtraBitfields() const {
return ExceptionSpec.Type == EST_Dynamic ||
requiresFunctionProtoTypeArmAttributes() ||
Expand Down Expand Up @@ -5432,6 +5452,7 @@ class FunctionProtoType final
EPI.Variadic = isVariadic();
EPI.EllipsisLoc = getEllipsisLoc();
EPI.HasTrailingReturn = hasTrailingReturn();
EPI.CFIUncheckedCallee = hasCFIUncheckedCallee();
EPI.ExceptionSpec = getExceptionSpecInfo();
EPI.TypeQuals = getMethodQuals();
EPI.RefQualifier = getRefQualifier();
Expand Down Expand Up @@ -5557,6 +5578,10 @@ class FunctionProtoType final
/// Whether this function prototype has a trailing return type.
bool hasTrailingReturn() const { return FunctionTypeBits.HasTrailingReturn; }

bool hasCFIUncheckedCallee() const {
return FunctionTypeBits.CFIUncheckedCallee;
}

Qualifiers getMethodQuals() const {
if (hasExtQualifiers())
return *getTrailingObjects<Qualifiers>();
Expand Down Expand Up @@ -8391,6 +8416,27 @@ inline bool Type::isObjectPointerType() const {
return false;
}

inline bool Type::isCFIUncheckedCalleeFunctionType() const {
if (const auto *Fn = getAs<FunctionProtoType>())
return Fn->hasCFIUncheckedCallee();
return false;
}

inline bool Type::hasPointeeToToCFIUncheckedCalleeFunctionType() const {
QualType Pointee;
if (const auto *PT = getAs<PointerType>())
Pointee = PT->getPointeeType();
else if (const auto *RT = getAs<ReferenceType>())
Pointee = RT->getPointeeType();
else if (const auto *MPT = getAs<MemberPointerType>())
Pointee = MPT->getPointeeType();
else if (const auto *DT = getAs<DecayedType>())
Pointee = DT->getPointeeType();
else
return false;
return Pointee->isCFIUncheckedCalleeFunctionType();
}

inline bool Type::isFunctionPointerType() const {
if (const auto *T = getAs<PointerType>())
return T->getPointeeType()->isFunctionType();
Expand Down
4 changes: 4 additions & 0 deletions clang/include/clang/AST/TypeProperties.td
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@ let Class = FunctionProtoType in {
def : Property<"trailingReturn", Bool> {
let Read = [{ node->hasTrailingReturn() }];
}
def : Property<"cfiUncheckedCallee", Bool> {
let Read = [{ node->hasCFIUncheckedCallee() }];
}
def : Property<"methodQualifiers", Qualifiers> {
let Read = [{ node->getMethodQuals() }];
}
Expand Down Expand Up @@ -353,6 +356,7 @@ let Class = FunctionProtoType in {
epi.ExtInfo = extInfo;
epi.Variadic = variadic;
epi.HasTrailingReturn = trailingReturn;
epi.CFIUncheckedCallee = cfiUncheckedCallee;
epi.TypeQuals = methodQualifiers;
epi.RefQualifier = refQualifier;
epi.ExceptionSpec = exceptionSpecifier;
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/Basic/Attr.td
Original file line number Diff line number Diff line change
Expand Up @@ -3111,6 +3111,11 @@ def NoDeref : TypeAttr {
let Documentation = [NoDerefDocs];
}

def CFIUncheckedCallee : TypeAttr {
let Spellings = [Clang<"cfi_unchecked_callee">];
let Documentation = [CFIUncheckedCalleeDocs];
}

def ReqdWorkGroupSize : InheritableAttr {
// Does not have a [[]] spelling because it is an OpenCL-related attribute.
let Spellings = [GNU<"reqd_work_group_size">];
Expand Down
48 changes: 48 additions & 0 deletions clang/include/clang/Basic/AttrDocs.td
Original file line number Diff line number Diff line change
Expand Up @@ -6869,6 +6869,54 @@ for references or Objective-C object pointers.
}];
}

def CFIUncheckedCalleeDocs : Documentation {
let Category = DocCatType;
let Content = [{
``cfi_unchecked_callee`` is a function type attribute which prevents the compiler from instrumenting
`Control Flow Integrity <https://clang.llvm.org/docs/ControlFlowIntegrity.html>`_ checks on indirect
function calls. Specifically, the 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
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 cast. Note an explicit cast just disables the warning, so
direct references to a function with a ``cfi_unchecked_callee`` attribute will still reference the
function definition rather than the CFI jump table.

.. code-block:: c

#define CFI_UNCHECKED_CALLEE __attribute__((cfi_unchecked_callee))

void no_cfi() CFI_UNCHECKED_CALLEE {}

void (*with_cfi)() = no_cfi; // warning: implicit conversion discards `cfi_unchecked_callee` attribute.
// `with_cfi` also points to the actual definition of `no_cfi` rather than
// its jump table entry.

void invoke(void (CFI_UNCHECKED_CALLEE *func)()) {
func(); // CFI will not instrument this indirect call.

void (*func2)() = func; // warning: implicit conversion discards `cfi_unchecked_callee` attribute.

func2(); // CFI will instrument this indirect call. Users should be careful however because if this
// references a function with type `cfi_unchecked_callee`, then the CFI check may incorrectly
// fail because the reference will be to the function definition rather than the CFI jump
// table entry.
}

This attribute can only be applied on functions or member functions. This attribute can be a good
alternative to ``no_sanitize("cfi")`` if you only want to disable innstrumentation for specific indirect
calls rather than applying ``no_sanitize("cfi")`` on the whole function containing indirect call. Note
that ``cfi_unchecked_attribute`` is a type attribute doesn't disable CFI instrumentation on a function
body.
}];
}

def ReinitializesDocs : Documentation {
let Category = DocCatFunction;
let Content = [{
Expand Down
5 changes: 5 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -12711,6 +12711,11 @@ def warn_noderef_on_non_pointer_or_array : Warning<
def warn_noderef_to_dereferenceable_pointer : Warning<
"casting to dereferenceable pointer removes 'noderef' attribute">, InGroup<NoDeref>;

def warn_cast_discards_cfi_unchecked_callee
: Warning<"implicit conversion from %0 to %1 discards "
"'cfi_unchecked_callee' attribute">,
InGroup<DiagGroup<"cfi-unchecked-callee">>;

def err_builtin_launder_invalid_arg : Error<
"%select{non-pointer|function pointer|void pointer}0 argument to "
"'__builtin_launder' is not allowed">;
Expand Down
22 changes: 19 additions & 3 deletions clang/include/clang/Sema/Sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -2645,6 +2645,16 @@ class Sema final : public SemaBase {
/// void*).
void DiscardMisalignedMemberAddress(const Type *T, Expr *E);

/// Returns true if `From` is a function or pointer to a function with the
/// `cfi_unchecked_callee` attribute but `To` is a function or pointer to
/// function without this attribute.
bool DiscardingCFIUncheckedCallee(QualType From, QualType To) const;

/// Returns true if `From` is a function or pointer to a function without the
/// `cfi_unchecked_callee` attribute but `To` is a function or pointer to
/// function with this attribute.
bool AddingCFIUncheckedCallee(QualType From, QualType To) const;

/// This function calls Action when it determines that E designates a
/// misaligned member due to the packed attribute. This is used to emit
/// local diagnostics like in reference binding.
Expand Down Expand Up @@ -10254,9 +10264,15 @@ class Sema final : public SemaBase {
bool CStyle, bool &ObjCLifetimeConversion);

/// Determine whether the conversion from FromType to ToType is a valid
/// conversion that strips "noexcept" or "noreturn" off the nested function
/// type.
bool IsFunctionConversion(QualType FromType, QualType ToType) const;
/// conversion that strips "noexcept" or "noreturn" or "cfi_unchecked_callee"
/// off the nested function type. This also checks if "cfi_unchecked_callee"
/// was added to the function type. If "cfi_unchecked_callee" is added and
/// `AddingCFIUncheckedCallee` is provided, it will be set to true. The same
/// thing applies for `DiscardingCFIUncheckedCallee` if the attribute is
/// discarded.
bool IsFunctionConversion(QualType FromType, QualType ToType,
bool *DiscardingCFIUncheckedCallee = nullptr,
bool *AddingCFIUncheckedCallee = nullptr) const;

/// Same as `IsFunctionConversion`, but if this would return true, it sets
/// `ResultTy` to `ToType`.
Expand Down
8 changes: 8 additions & 0 deletions clang/lib/AST/Type.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3571,6 +3571,12 @@ QualType QualType::getNonLValueExprType(const ASTContext &Context) const {
return *this;
}

bool FunctionType::getCFIUncheckedCalleeAttr() const {
if (const auto *FPT = getAs<FunctionProtoType>())
return FPT->hasCFIUncheckedCallee();
return false;
}

StringRef FunctionType::getNameForCallConv(CallingConv CC) {
switch (CC) {
case CC_C:
Expand Down Expand Up @@ -3662,6 +3668,7 @@ FunctionProtoType::FunctionProtoType(QualType result, ArrayRef<QualType> params,
FunctionTypeBits.HasExtParameterInfos = !!epi.ExtParameterInfos;
FunctionTypeBits.Variadic = epi.Variadic;
FunctionTypeBits.HasTrailingReturn = epi.HasTrailingReturn;
FunctionTypeBits.CFIUncheckedCallee = epi.CFIUncheckedCallee;

if (epi.requiresFunctionProtoTypeExtraBitfields()) {
FunctionTypeBits.HasExtraBitfields = true;
Expand Down Expand Up @@ -3929,6 +3936,7 @@ void FunctionProtoType::Profile(llvm::FoldingSetNodeID &ID, QualType Result,

ID.AddInteger((EffectCount << 3) | (HasConds << 2) |
(epi.AArch64SMEAttributes << 1) | epi.HasTrailingReturn);
ID.AddInteger(epi.CFIUncheckedCallee);

for (unsigned Idx = 0; Idx != EffectCount; ++Idx) {
ID.AddInteger(epi.FunctionEffects.Effects[Idx].toOpaqueInt32());
Expand Down
6 changes: 6 additions & 0 deletions clang/lib/AST/TypePrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,9 @@ void TypePrinter::printFunctionProtoAfter(const FunctionProtoType *T,
OS << "))";
}

if (T->hasCFIUncheckedCallee())
OS << " __attribute__((cfi_unchecked_callee))";

if (T->hasTrailingReturn()) {
OS << " -> ";
print(T->getReturnType(), OS, StringRef());
Expand Down Expand Up @@ -2090,6 +2093,9 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
case attr::NoDeref:
OS << "noderef";
break;
case attr::CFIUncheckedCallee:
OS << "cfi_unchecked_callee";
break;
case attr::AcquireHandle:
OS << "acquire_handle";
break;
Expand Down
13 changes: 10 additions & 3 deletions clang/lib/CodeGen/CGExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3053,9 +3053,13 @@ static LValue EmitFunctionDeclLValue(CodeGenFunction &CGF, const Expr *E,
GlobalDecl GD) {
const FunctionDecl *FD = cast<FunctionDecl>(GD.getDecl());
llvm::Constant *V = CGF.CGM.getFunctionPointer(GD);
QualType ETy = E->getType();
if (ETy->isCFIUncheckedCalleeFunctionType()) {
if (auto *GV = dyn_cast<llvm::GlobalValue>(V))
V = llvm::NoCFIValue::get(GV);
}
CharUnits Alignment = CGF.getContext().getDeclAlign(FD);
return CGF.MakeAddrLValue(V, E->getType(), Alignment,
AlignmentSource::Decl);
return CGF.MakeAddrLValue(V, ETy, Alignment, AlignmentSource::Decl);
}

static LValue EmitCapturedFieldLValue(CodeGenFunction &CGF, const FieldDecl *FD,
Expand Down Expand Up @@ -6349,10 +6353,13 @@ RValue CodeGenFunction::EmitCall(QualType CalleeType,
FD && FD->hasAttr<OpenCLKernelAttr>())
CGM.getTargetCodeGenInfo().setOCLKernelStubCallingConvention(FnType);

bool CFIUnchecked =
CalleeType->hasPointeeToToCFIUncheckedCalleeFunctionType();

// If we are checking indirect calls and this call is indirect, check that the
// function pointer is a member of the bit set for the function type.
if (SanOpts.has(SanitizerKind::CFIICall) &&
(!TargetDecl || !isa<FunctionDecl>(TargetDecl))) {
(!TargetDecl || !isa<FunctionDecl>(TargetDecl)) && !CFIUnchecked) {
SanitizerScope SanScope(this);
EmitSanitizerStatReport(llvm::SanStat_CFI_ICall);
ApplyDebugLocation ApplyTrapDI(
Expand Down
8 changes: 6 additions & 2 deletions clang/lib/CodeGen/CGExprConstant.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2236,8 +2236,12 @@ ConstantLValueEmitter::tryEmitBase(const APValue::LValueBase &base) {
return ConstantLValue(C);
};

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

if (const auto *VD = dyn_cast<VarDecl>(D)) {
// We can never refer to a variable with local storage.
Expand Down
5 changes: 5 additions & 0 deletions clang/lib/CodeGen/CGPointerAuth.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,11 @@ llvm::Constant *CodeGenModule::getMemberFunctionPointer(llvm::Constant *Pointer,
Pointer, PointerAuth.getKey(), nullptr,
cast_or_null<llvm::ConstantInt>(PointerAuth.getDiscriminator()));

if (const auto *MFT = dyn_cast<MemberPointerType>(FT.getTypePtr())) {
if (MFT->hasPointeeToToCFIUncheckedCalleeFunctionType())
Pointer = llvm::NoCFIValue::get(cast<llvm::GlobalValue>(Pointer));
}

return Pointer;
}

Expand Down
11 changes: 11 additions & 0 deletions clang/lib/CodeGen/ItaniumCXXABI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,17 @@ CGCallee ItaniumCXXABI::EmitLoadOfMemberFunctionPointer(
llvm::Constant *CheckTypeDesc;
bool ShouldEmitCFICheck = CGF.SanOpts.has(SanitizerKind::CFIMFCall) &&
CGM.HasHiddenLTOVisibility(RD);

if (ShouldEmitCFICheck) {
if (const auto *BinOp = dyn_cast<BinaryOperator>(E)) {
if (BinOp->isPtrMemOp() &&
BinOp->getRHS()
->getType()
->hasPointeeToToCFIUncheckedCalleeFunctionType())
ShouldEmitCFICheck = false;
}
}

bool ShouldEmitVFEInfo = CGM.getCodeGenOpts().VirtualFunctionElimination &&
CGM.HasHiddenLTOVisibility(RD);
bool ShouldEmitWPDInfo =
Expand Down
Loading
Loading