diff --git a/clang/docs/ClangCommandLineReference.rst b/clang/docs/ClangCommandLineReference.rst index 0cc9c71ebe63c2f7f0472d31981f34a3aaa976d3..0a0fce9190f43094161ee9f17e9463ce6d8863ad 100644 --- a/clang/docs/ClangCommandLineReference.rst +++ b/clang/docs/ClangCommandLineReference.rst @@ -740,6 +740,10 @@ Path to blacklist file for sanitizers Enable control flow integrity (CFI) checks for cross-DSO calls. +.. option:: -fsanitize-cfi-icall-generalize-pointers + +Generalize pointers in function type signatures used for Control Flow Integrity (CFI) indirect call checking + .. option:: -fsanitize-coverage=,..., -fno-sanitize-coverage=,... Specify the type of coverage instrumentation for Sanitizers diff --git a/clang/docs/ControlFlowIntegrity.rst b/clang/docs/ControlFlowIntegrity.rst index 04fb43a70f1990c6c2c98424a9d335cdf0a935b1..12b4610f8a282adbcf8d3772d3d60df48a33100f 100644 --- a/clang/docs/ControlFlowIntegrity.rst +++ b/clang/docs/ControlFlowIntegrity.rst @@ -215,6 +215,23 @@ shared library boundaries are handled as if the callee was not compiled with This scheme is currently only supported on the x86 and x86_64 architectures. +``-fsanitize-cfi-icall-generalize-pointers`` +-------------------------------------------- + +Mismatched pointer types are a common cause of cfi-icall check failures. +Translation units compiled with the ``-fsanitize-cfi-icall-generalize-pointers`` +flag relax pointer type checking for call sites in that translation unit, +applied across all functions compiled with ``-fsanitize=cfi-icall``. + +Specifically, pointers in return and argument types are treated as equivalent as +long as the qualifiers for the type they point to match. For example, ``char*`` +``char**`, and ``int*`` are considered equivalent types. However, ``char*`` and +``const char*`` are considered separate types. + +``-fsanitize-cfi-icall-generalize-pointers`` is not compatible with +``-fsanitize-cfi-cross-dso``. + + ``-fsanitize=cfi-icall`` and ``-fsanitize=function`` ---------------------------------------------------- diff --git a/clang/docs/UsersManual.rst b/clang/docs/UsersManual.rst index 2757ced1c24c65de4cc5eb6b0bfc69533fa3426f..023f9f5528ba494edc53ab29fb76c79bb04b6f82 100644 --- a/clang/docs/UsersManual.rst +++ b/clang/docs/UsersManual.rst @@ -1147,6 +1147,11 @@ are listed below. the behavior of sanitizers in the ``cfi`` group to allow checking of cross-DSO virtual and indirect calls. +.. option:: -fsanitize-cfi-icall-generalize-pointers + + Generalize pointers in return and argument types in function type signatures + checked by Control Flow Integrity indirect call checking. See + :doc:`ControlFlowIntegrity` for more details. .. option:: -fstrict-vtable-pointers diff --git a/clang/include/clang/Driver/Options.td b/clang/include/clang/Driver/Options.td index eef0eca713cde8722d474e1a808cdec73376208b..e3476c721a7ecd75a2c5e2efdae80112391f853e 100644 --- a/clang/include/clang/Driver/Options.td +++ b/clang/include/clang/Driver/Options.td @@ -914,6 +914,9 @@ def fno_sanitize_cfi_cross_dso : Flag<["-"], "fno-sanitize-cfi-cross-dso">, Flags<[CoreOption, DriverOption]>, Group, HelpText<"Disable control flow integrity (CFI) checks for cross-DSO calls.">; +def fsanitize_cfi_icall_generalize_pointers : Flag<["-"], "fsanitize-cfi-icall-generalize-pointers">, + Group, + HelpText<"Generalize pointers in CFI indirect call type signature checks">; def fsanitize_stats : Flag<["-"], "fsanitize-stats">, Group, HelpText<"Enable sanitizer statistics gathering.">; diff --git a/clang/include/clang/Driver/SanitizerArgs.h b/clang/include/clang/Driver/SanitizerArgs.h index 52b6698857e8d08b52e9180750d4d4d556bf962f..19309c3f3b7f1e7306312a4dfdd7d42f86b74da2 100644 --- a/clang/include/clang/Driver/SanitizerArgs.h +++ b/clang/include/clang/Driver/SanitizerArgs.h @@ -32,6 +32,7 @@ class SanitizerArgs { int MsanTrackOrigins = 0; bool MsanUseAfterDtor = false; bool CfiCrossDso = false; + bool CfiICallGeneralizePointers = false; int AsanFieldPadding = 0; bool SharedRuntime = false; bool AsanUseAfterScope = true; diff --git a/clang/include/clang/Frontend/CodeGenOptions.def b/clang/include/clang/Frontend/CodeGenOptions.def index 8cac8e45c8e636770f49b78b686ae837326f1582..8f2aae2f140c580abff7a49dfb469bdce65a3f5e 100644 --- a/clang/include/clang/Frontend/CodeGenOptions.def +++ b/clang/include/clang/Frontend/CodeGenOptions.def @@ -154,6 +154,8 @@ CODEGENOPT(SanitizeMemoryUseAfterDtor, 1, 0) ///< Enable use-after-delete detect CODEGENOPT(SanitizeCfiCrossDso, 1, 0) ///< Enable cross-dso support in CFI. CODEGENOPT(SanitizeMinimalRuntime, 1, 0) ///< Use "_minimal" sanitizer runtime for ///< diagnostics. +CODEGENOPT(SanitizeCfiICallGeneralizePointers, 1, 0) ///< Generalize pointer types in + ///< CFI icall function signatures CODEGENOPT(SanitizeCoverageType, 2, 0) ///< Type of sanitizer coverage ///< instrumentation. CODEGENOPT(SanitizeCoverageIndirectCalls, 1, 0) ///< Enable sanitizer coverage diff --git a/clang/lib/CodeGen/CGExpr.cpp b/clang/lib/CodeGen/CGExpr.cpp index d3b03e556fba068140a1d93aa17fd7e1ebf7211e..88116f7d810eaffa7d8414cdc0096103815ac2d5 100644 --- a/clang/lib/CodeGen/CGExpr.cpp +++ b/clang/lib/CodeGen/CGExpr.cpp @@ -4475,7 +4475,12 @@ RValue CodeGenFunction::EmitCall(QualType CalleeType, const CGCallee &OrigCallee SanitizerScope SanScope(this); EmitSanitizerStatReport(llvm::SanStat_CFI_ICall); - llvm::Metadata *MD = CGM.CreateMetadataIdentifierForType(QualType(FnType, 0)); + llvm::Metadata *MD; + if (CGM.getCodeGenOpts().SanitizeCfiICallGeneralizePointers) + MD = CGM.CreateMetadataIdentifierGeneralized(QualType(FnType, 0)); + else + MD = CGM.CreateMetadataIdentifierForType(QualType(FnType, 0)); + llvm::Value *TypeId = llvm::MetadataAsValue::get(getLLVMContext(), MD); llvm::Value *CalleePtr = Callee.getFunctionPointer(); diff --git a/clang/lib/CodeGen/CodeGenModule.cpp b/clang/lib/CodeGen/CodeGenModule.cpp index 2254f3e70d58a5f33265e299bcc1bebaa8106249..b2a18a03f2910a7a49f8561953ad9f10cbae970f 100644 --- a/clang/lib/CodeGen/CodeGenModule.cpp +++ b/clang/lib/CodeGen/CodeGenModule.cpp @@ -1152,6 +1152,7 @@ void CodeGenModule::CreateFunctionTypeMetadata(const FunctionDecl *FD, llvm::Metadata *MD = CreateMetadataIdentifierForType(FD->getType()); F->addTypeMetadata(0, MD); + F->addTypeMetadata(0, CreateMetadataIdentifierGeneralized(FD->getType())); // Emit a hash-based bit set entry for cross-DSO calls. if (CodeGenOpts.SanitizeCfiCrossDso) @@ -4543,6 +4544,60 @@ llvm::Metadata *CodeGenModule::CreateMetadataIdentifierForType(QualType T) { return InternalId; } +// Generalize pointer types to a void pointer with the qualifiers of the +// originally pointed-to type, e.g. 'const char *' and 'char * const *' +// generalize to 'const void *' while 'char *' and 'const char **' generalize to +// 'void *'. +static QualType GeneralizeType(ASTContext &Ctx, QualType Ty) { + if (!Ty->isPointerType()) + return Ty; + + return Ctx.getPointerType( + QualType(Ctx.VoidTy).withCVRQualifiers( + Ty->getPointeeType().getCVRQualifiers())); +} + +// Apply type generalization to a FunctionType's return and argument types +static QualType GeneralizeFunctionType(ASTContext &Ctx, QualType Ty) { + if (auto *FnType = Ty->getAs()) { + SmallVector GeneralizedParams; + for (auto &Param : FnType->param_types()) + GeneralizedParams.push_back(GeneralizeType(Ctx, Param)); + + return Ctx.getFunctionType( + GeneralizeType(Ctx, FnType->getReturnType()), + GeneralizedParams, FnType->getExtProtoInfo()); + } + + if (auto *FnType = Ty->getAs()) + return Ctx.getFunctionNoProtoType( + GeneralizeType(Ctx, FnType->getReturnType())); + + llvm_unreachable("Encountered unknown FunctionType"); +} + +llvm::Metadata *CodeGenModule::CreateMetadataIdentifierGeneralized(QualType T) { + T = GeneralizeFunctionType(getContext(), T); + + llvm::Metadata *&InternalId = GeneralizedMetadataIdMap[T.getCanonicalType()]; + if (InternalId) + return InternalId; + + if (isExternallyVisible(T->getLinkage())) { + std::string OutName; + llvm::raw_string_ostream Out(OutName); + getCXXABI().getMangleContext().mangleTypeName(T, Out); + Out << ".generalized"; + + InternalId = llvm::MDString::get(getLLVMContext(), Out.str()); + } else { + InternalId = llvm::MDNode::getDistinct(getLLVMContext(), + llvm::ArrayRef()); + } + + return InternalId; +} + /// Returns whether this module needs the "all-vtables" type identifier. bool CodeGenModule::NeedAllVtablesTypeId() const { // Returns true if at least one of vtable-based CFI checkers is enabled and diff --git a/clang/lib/CodeGen/CodeGenModule.h b/clang/lib/CodeGen/CodeGenModule.h index 21bab6cfa054022e82a1ce292686489af2ef58cb..7a47c576c0db16040ce9d1882d7bfa2bab41088c 100644 --- a/clang/lib/CodeGen/CodeGenModule.h +++ b/clang/lib/CodeGen/CodeGenModule.h @@ -497,7 +497,9 @@ private: /// Mapping from canonical types to their metadata identifiers. We need to /// maintain this mapping because identifiers may be formed from distinct /// MDNodes. - llvm::DenseMap MetadataIdMap; + typedef llvm::DenseMap MetadataTypeMap; + MetadataTypeMap MetadataIdMap; + MetadataTypeMap GeneralizedMetadataIdMap; public: CodeGenModule(ASTContext &C, const HeaderSearchOptions &headersearchopts, @@ -1209,6 +1211,11 @@ public: /// internal identifiers). llvm::Metadata *CreateMetadataIdentifierForType(QualType T); + /// Create a metadata identifier for the generalization of the given type. + /// This may either be an MDString (for external identifiers) or a distinct + /// unnamed MDNode (for internal identifiers). + llvm::Metadata *CreateMetadataIdentifierGeneralized(QualType T); + /// Create and attach type metadata to the given function. void CreateFunctionTypeMetadata(const FunctionDecl *FD, llvm::Function *F); diff --git a/clang/lib/Driver/SanitizerArgs.cpp b/clang/lib/Driver/SanitizerArgs.cpp index 037989680f2d23904b2f3f5a8eb41b171eb731e7..32c1c43a5bd99a0c29e15153613aa7dbfe3b0482 100644 --- a/clang/lib/Driver/SanitizerArgs.cpp +++ b/clang/lib/Driver/SanitizerArgs.cpp @@ -520,6 +520,13 @@ SanitizerArgs::SanitizerArgs(const ToolChain &TC, // Without PIE, external function address may resolve to a PLT record, which // can not be verified by the target module. NeedPIE |= CfiCrossDso; + CfiICallGeneralizePointers = + Args.hasArg(options::OPT_fsanitize_cfi_icall_generalize_pointers); + + if (CfiCrossDso && CfiICallGeneralizePointers) + D.Diag(diag::err_drv_argument_not_allowed_with) + << "-fsanitize-cfi-cross-dso" + << "-fsanitize-cfi-icall-generalize-pointers"; } Stats = Args.hasFlag(options::OPT_fsanitize_stats, @@ -807,6 +814,9 @@ void SanitizerArgs::addArgs(const ToolChain &TC, const llvm::opt::ArgList &Args, if (CfiCrossDso) CmdArgs.push_back("-fsanitize-cfi-cross-dso"); + if (CfiICallGeneralizePointers) + CmdArgs.push_back("-fsanitize-cfi-icall-generalize-pointers"); + if (Stats) CmdArgs.push_back("-fsanitize-stats"); diff --git a/clang/lib/Frontend/CompilerInvocation.cpp b/clang/lib/Frontend/CompilerInvocation.cpp index 19e26c18bfdf5a648872141131c229e14d17763e..2c0d99b4befd63f9d5de621d48b9154b46760dea 100644 --- a/clang/lib/Frontend/CompilerInvocation.cpp +++ b/clang/lib/Frontend/CompilerInvocation.cpp @@ -839,6 +839,8 @@ static bool ParseCodeGenArgs(CodeGenOptions &Opts, ArgList &Args, InputKind IK, false); Opts.SanitizeMinimalRuntime = Args.hasArg(OPT_fsanitize_minimal_runtime); Opts.SanitizeCfiCrossDso = Args.hasArg(OPT_fsanitize_cfi_cross_dso); + Opts.SanitizeCfiICallGeneralizePointers = + Args.hasArg(OPT_fsanitize_cfi_icall_generalize_pointers); Opts.SanitizeStats = Args.hasArg(OPT_fsanitize_stats); if (Arg *A = Args.getLastArg(OPT_fsanitize_address_use_after_scope, OPT_fno_sanitize_address_use_after_scope)) { diff --git a/clang/test/CodeGen/cfi-icall-cross-dso.c b/clang/test/CodeGen/cfi-icall-cross-dso.c index 636a9e4aedb4d02b06d30369ea86823e4213bcc2..43ab0e73b14ae0504c95c9d3e2c86d54911801f1 100644 --- a/clang/test/CodeGen/cfi-icall-cross-dso.c +++ b/clang/test/CodeGen/cfi-icall-cross-dso.c @@ -46,7 +46,7 @@ void caller(void (*f)()) { // Check that we emit both string and hash based type entries for static void g(), // and don't emit them for the declaration of h(). -// CHECK: define internal void @g({{.*}} !type [[TVOID:![0-9]+]] !type [[TVOID_ID:![0-9]+]] +// CHECK: define internal void @g({{.*}} !type [[TVOID:![0-9]+]] !type [[TVOID_GENERALIZED:![0-9]+]] !type [[TVOID_ID:![0-9]+]] static void g(void) {} // CHECK: declare void @h({{[^!]*$}} @@ -60,9 +60,9 @@ Fn h1() { return &h; } -// CHECK: define void @bar({{.*}} !type [[TNOPROTO:![0-9]+]] !type [[TNOPROTO_ID:![0-9]+]] +// CHECK: define void @bar({{.*}} !type [[TNOPROTO:![0-9]+]] !type [[TNOPROTO_GENERALIZED:![0-9]+]] !type [[TNOPROTO_ID:![0-9]+]] // ITANIUM: define available_externally void @foo({{[^!]*$}} -// MS: define linkonce_odr void @foo({{.*}} !type [[TNOPROTO]] !type [[TNOPROTO_ID]] +// MS: define linkonce_odr void @foo({{.*}} !type [[TNOPROTO]] !type [[TNOPROTO_GENERALIZED:![0-9]+]] !type [[TNOPROTO_ID]] inline void foo() {} void bar() { foo(); } @@ -71,11 +71,15 @@ void bar() { foo(); } // Check that the type entries are correct. // ITANIUM: [[TVOID]] = !{i64 0, !"_ZTSFvvE"} +// ITANIUM: [[TVOID_GENERALIZED]] = !{i64 0, !"_ZTSFvvE.generalized"} // ITANIUM: [[TVOID_ID]] = !{i64 0, i64 9080559750644022485} // ITANIUM: [[TNOPROTO]] = !{i64 0, !"_ZTSFvE"} +// ITANIUM: [[TNOPROTO_GENERALIZED]] = !{i64 0, !"_ZTSFvE.generalized"} // ITANIUM: [[TNOPROTO_ID]] = !{i64 0, i64 6588678392271548388} // MS: [[TVOID]] = !{i64 0, !"?6AXXZ"} +// MS: [[TVOID_GENERALIZED]] = !{i64 0, !"?6AXXZ.generalized"} // MS: [[TVOID_ID]] = !{i64 0, i64 5113650790573562461} // MS: [[TNOPROTO]] = !{i64 0, !"?6AX@Z"} +// MS: [[TNOPROTO_GENERALIZED]] = !{i64 0, !"?6AX@Z.generalized"} // MS: [[TNOPROTO_ID]] = !{i64 0, i64 4195979634929632483} diff --git a/clang/test/CodeGen/cfi-icall-generalize.c b/clang/test/CodeGen/cfi-icall-generalize.c new file mode 100644 index 0000000000000000000000000000000000000000..c7c7b30a7a218a1e2b852a508c3e2dc29529d17b --- /dev/null +++ b/clang/test/CodeGen/cfi-icall-generalize.c @@ -0,0 +1,19 @@ +// RUN: %clang_cc1 -triple x86_64-unknown-linux -fsanitize=cfi-icall -fsanitize-trap=cfi-icall -emit-llvm -o - %s | FileCheck --check-prefix=CHECK --check-prefix=UNGENERALIZED %s +// RUN: %clang_cc1 -triple x86_64-unknown-linux -fsanitize=cfi-icall -fsanitize-trap=cfi-icall -fsanitize-cfi-icall-generalize-pointers -emit-llvm -o - %s | FileCheck --check-prefix=CHECK --check-prefix=GENERALIZED %s + +// Test that const char* is generalized to const void* and that const char** is +// generalized to void* + +// CHECK: define i32** @f({{.*}} !type [[TYPE:![0-9]+]] !type [[TYPE_GENERALIZED:![0-9]+]] +int** f(const char *a, const char **b) { + return (int**)0; +} + +void g(int** (*fp)(const char *, const char **)) { + // UNGENERALIZED: call i1 @llvm.type.test(i8* {{.*}}, metadata !"_ZTSFPPiPKcPS2_E") + // GENERALIZED: call i1 @llvm.type.test(i8* {{.*}}, metadata !"_ZTSFPvPKvS_E.generalized") + fp(0, 0); +} + +// CHECK: [[TYPE]] = !{i64 0, !"_ZTSFPPiPKcPS2_E"} +// CHECK: [[TYPE_GENERALIZED]] = !{i64 0, !"_ZTSFPvPKvS_E.generalized"} diff --git a/clang/test/CodeGen/cfi-icall.c b/clang/test/CodeGen/cfi-icall.c index ed34f4f44beb6bdbb4790976fe29481fa507838c..5f346b66e81e5a0b0b645ad32e5578f4f6a2ae7a 100644 --- a/clang/test/CodeGen/cfi-icall.c +++ b/clang/test/CodeGen/cfi-icall.c @@ -3,22 +3,26 @@ // Tests that we assign appropriate identifiers to unprototyped functions. -// CHECK: define void @f({{.*}} !type [[TVOID:![0-9]+]] +// CHECK: define void @f({{.*}} !type [[TVOID:![0-9]+]] !type [[TVOID_GENERALIZED:![0-9]+]] void f() { } void xf(); -// CHECK: define void @g({{.*}} !type [[TINT:![0-9]+]] +// CHECK: define void @g({{.*}} !type [[TINT:![0-9]+]] !type [[TINT_GENERALIZED:![0-9]+]] void g(int b) { void (*fp)() = b ? f : xf; // ITANIUM: call i1 @llvm.type.test(i8* {{.*}}, metadata !"_ZTSFvE") fp(); } -// CHECK: declare !type [[TVOID:![0-9]+]] void @xf({{.*}} +// CHECK: declare !type [[TVOID]] !type [[TVOID_GENERALIZED]] void @xf({{.*}} // ITANIUM-DAG: [[TVOID]] = !{i64 0, !"_ZTSFvE"} +// ITANIUM-DAG: [[TVOID_GENERALIZED]] = !{i64 0, !"_ZTSFvE.generalized"} // ITANIUM-DAG: [[TINT]] = !{i64 0, !"_ZTSFviE"} +// ITANIUM-DAG: [[TINT_GENERALIZED]] = !{i64 0, !"_ZTSFviE.generalized"} // MS-DAG: [[TVOID]] = !{i64 0, !"?6AX@Z"} +// MS-DAG: [[TVOID_GENERALIZED]] = !{i64 0, !"?6AX@Z.generalized"} // MS-DAG: [[TINT]] = !{i64 0, !"?6AXH@Z"} +// MS-DAG: [[TINT_GENERALIZED]] = !{i64 0, !"?6AXH@Z.generalized"} diff --git a/clang/test/CodeGenCXX/cfi-icall.cpp b/clang/test/CodeGenCXX/cfi-icall.cpp index c3c6ed309cc6fb169a117cee2bde0e0b333166c4..5f5778fc1f7c015953201cccc114653718d42f97 100644 --- a/clang/test/CodeGenCXX/cfi-icall.cpp +++ b/clang/test/CodeGenCXX/cfi-icall.cpp @@ -8,19 +8,22 @@ namespace { struct S {}; -void f(S *s) { +void f(S s) { } } void g() { - void (*fp)(S *) = f; - // CHECK: call i1 @llvm.type.test(i8* {{.*}}, metadata [[VOIDS:![0-9]+]]) - fp(0); + struct S s; + void (*fp)(S) = f; + // CHECK: call i1 @llvm.type.test(i8* {{.*}}, metadata [[VOIDS1:![0-9]+]]) + fp(s); } -// ITANIUM: define internal void @_ZN12_GLOBAL__N_11fEPNS_1SE({{.*}} !type [[TS:![0-9]+]] -// MS: define internal void @"\01?f@?A@@YAXPEAUS@?A@@@Z"({{.*}} !type [[TS:![0-9]+]] +// ITANIUM: define internal void @_ZN12_GLOBAL__N_11fENS_1SE({{.*}} !type [[TS1:![0-9]+]] !type [[TS2:![0-9]+]] +// MS: define internal void @"\01?f@?A@@YAXUS@?A@@@Z"({{.*}} !type [[TS1:![0-9]+]] !type [[TS2:![0-9]+]] -// CHECK: [[VOIDS]] = distinct !{} -// CHECK: [[TS]] = !{i64 0, [[VOIDS]]} +// CHECK: [[VOIDS1]] = distinct !{} +// CHECK: [[TS1]] = !{i64 0, [[VOIDS1]]} +// CHECK: [[TS2]] = !{i64 0, [[VOIDS2:![0-9]+]]} +// CHECK: [[VOIDS2]] = distinct !{} diff --git a/clang/test/Driver/fsanitize.c b/clang/test/Driver/fsanitize.c index 92d5c5da72ccd06c3fb02db11e22d3533ed44f22..dcca96ff3a8974bfd2e93cc6e9cd5b2418371145 100644 --- a/clang/test/Driver/fsanitize.c +++ b/clang/test/Driver/fsanitize.c @@ -480,6 +480,14 @@ // CHECK-CFI-NO-CROSS-DSO: -emit-llvm-bc // CHECK-CFI-NO-CROSS-DSO-NOT: -fsanitize-cfi-cross-dso +// RUN: %clang -target x86_64-linux-gnu -fsanitize=cfi-icall -fsanitize-cfi-icall-generalize-pointers -fvisibility=hidden -flto -c %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-CFI-GENERALIZE-POINTERS +// RUN: %clang -target x86_64-linux-gnu -fsanitize=cfi-icall -fvisibility=hidden -flto -c %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-NO-CFI-GENERALIZE-POINTERS +// CHECK-CFI-GENERALIZE-POINTERS: -fsanitize-cfi-icall-generalize-pointers +// CHECK-NO-CFI-GENERALIZE-POINTERS-NOT: -fsanitize-cfi-icall-generalize-pointers + +// RUN: %clang -target x86_64-linux-gnu -fsanitize=cfi-icall -fsanitize-cfi-icall-generalize-pointers -fsanitize-cfi-cross-dso -fvisibility=hidden -flto -c %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-CFI-GENERALIZE-AND-CROSS-DSO +// CHECK-CFI-GENERALIZE-AND-CROSS-DSO: error: invalid argument '-fsanitize-cfi-cross-dso' not allowed with '-fsanitize-cfi-icall-generalize-pointers' + // RUN: %clang -target x86_64-linux-gnu -fsanitize=cfi -fsanitize-stats -flto -c %s -### 2>&1 | FileCheck %s --check-prefix=CHECK-CFI-STATS // CHECK-CFI-STATS: -fsanitize-stats