Index: test/CMakeLists.txt =================================================================== --- test/CMakeLists.txt +++ test/CMakeLists.txt @@ -51,6 +51,7 @@ if(COMPILER_RT_HAS_UBSAN) add_subdirectory(ubsan) endif() + add_subdirectory(cfi) endif() if(COMPILER_RT_STANDALONE_BUILD) Index: test/cfi/CMakeLists.txt =================================================================== --- /dev/null +++ test/cfi/CMakeLists.txt @@ -0,0 +1,19 @@ +configure_lit_site_cfg( + ${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.in + ${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg + ) + +set(CFI_TEST_DEPS) +if(NOT COMPILER_RT_STANDALONE_BUILD) + list(APPEND CFI_TEST_DEPS + FileCheck + clang + LLVMgold + not + ) +endif() + +add_lit_testsuite(check-cfi "Running the cfi regression tests" + ${CMAKE_CURRENT_BINARY_DIR} + DEPENDS ${CFI_TEST_DEPS}) +set_target_properties(check-cfi PROPERTIES FOLDER "Tests") Index: test/cfi/anon-namespace.cpp =================================================================== --- /dev/null +++ test/cfi/anon-namespace.cpp @@ -0,0 +1,61 @@ +// RUN: %clangxx_cfi -c -DTU1 -o %t1.o %s +// RUN: %clangxx_cfi -c -DTU2 -o %t2.o %S/../cfi/anon-namespace.cpp +// RUN: %clangxx_cfi -o %t %t1.o %t2.o +// RUN: not --crash %t 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -c -DTU1 -o %t1.o %s +// RUN: %clangxx -c -DTU2 -o %t2.o %S/../cfi/anon-namespace.cpp +// RUN: %clangxx -o %t %t1.o %t2.o +// RUN: %t 2>&1 | FileCheck --check-prefix=NCFI %s + +// Tests that the CFI mechanism treats classes in the anonymous namespace in +// different translation units as having distinct identities. This is done by +// compiling two translation units TU1 and TU2 containing a class named B in an +// anonymous namespace, and testing that the program crashes if TU2 attempts to +// use a TU1 B as a TU2 B. + +// FIXME: This test should not require that the paths supplied to the compiler +// are different. It currently does so because bitset names have global scope +// so we have to mangle the file path into the bitset name. + +#include + +struct A { + virtual void f() = 0; +}; + +namespace { + +struct B : A { + virtual void f() {} +}; + +} + +A *mkb(); + +#ifdef TU1 + +A *mkb() { + return new B; +} + +#endif // TU1 + +#ifdef TU2 + +int main() { + A *a = mkb(); + + // CFI: 1 + // NCFI: 1 + fprintf(stderr, "1\n"); + + ((B *)a)->f(); // UB here + + // CFI-NOT: 2 + // NCFI: 2 + fprintf(stderr, "2\n"); +} + +#endif // TU2 Index: test/cfi/lit.cfg =================================================================== --- /dev/null +++ test/cfi/lit.cfg @@ -0,0 +1,39 @@ +import lit.formats +import os +import subprocess +import sys + +config.name = 'cfi' +config.suffixes = ['.cpp'] +config.test_format = lit.formats.ShTest() +config.test_source_root = config.compiler_rt_src_root + '/test/cfi' +config.test_exec_root = config.compiler_rt_obj_root + '/test/cfi' +config.excludes = ['Inputs'] + +def is_darwin_lto_supported(): + return os.path.exists(os.path.join(config.llvm_shlib_dir, 'libLTO.dylib')) + +def is_linux_lto_supported(): + if not os.path.exists(os.path.join(config.llvm_shlib_dir, 'LLVMgold.so')): + return False + + ld_cmd = subprocess.Popen([config.gold_executable, '--help'], stdout = subprocess.PIPE) + ld_out = ld_cmd.stdout.read().decode() + ld_cmd.wait() + + if not '-plugin' in ld_out: + return False + + return True + +config.substitutions.append((r"%clangxx ", config.llvm_obj_root + '/bin/clang++ ')) + +if sys.platform == 'darwin' and is_darwin_lto_supported(): + config.substitutions.append((r"%clangxx_cfi ", 'env DYLD_LIBRARY_PATH=' + config.llvm_shlib_dir + ' ' + config.llvm_obj_root + '/bin/clang++ -fsanitize=cfi ')) +elif sys.platform.startswith('linux') and is_linux_lto_supported(): + config.substitutions.append((r"%clangxx_cfi ", config.llvm_obj_root + '/bin/clang++ -fuse-ld=gold -fsanitize=cfi ')) +else: + config.unsupported = True + +config.substitutions.append((r"\bFileCheck\b", config.llvm_obj_root + '/bin/FileCheck')) +config.substitutions.append((r"\bnot\b", config.llvm_obj_root + '/bin/not')) Index: test/cfi/lit.site.cfg.in =================================================================== --- /dev/null +++ test/cfi/lit.site.cfg.in @@ -0,0 +1,2 @@ +lit_config.load_config(config, "@COMPILER_RT_BINARY_DIR@/test/lit.common.configured") +lit_config.load_config(config, "@CMAKE_CURRENT_SOURCE_DIR@/lit.cfg") Index: test/cfi/multiple-inheritance.cpp =================================================================== --- /dev/null +++ test/cfi/multiple-inheritance.cpp @@ -0,0 +1,47 @@ +// RUN: %clangxx_cfi -o %t %s +// RUN: not --crash %t 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: not --crash %t x 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -o %t %s +// RUN: %t 2>&1 | FileCheck --check-prefix=NCFI %s +// RUN: %t x 2>&1 | FileCheck --check-prefix=NCFI %s + +// Tests that the CFI mechanism is sensitive to multiple inheritance and only +// permits calls via virtual tables for the correct base class. + +#include + +struct A { + virtual void f() = 0; +}; + +struct B { + virtual void g() = 0; +}; + +struct C : A, B { + virtual void f(), g(); +}; + +void C::f() {} +void C::g() {} + +int main(int argc, char **argv) { + C *c = new C; + + // CFI: 1 + // NCFI: 1 + fprintf(stderr, "1\n"); + + if (argc > 1) { + A *a = c; + ((B *)a)->g(); // UB here + } else { + B *b = c; + ((A *)b)->f(); // UB here + } + + // CFI-NOT: 2 + // NCFI: 2 + fprintf(stderr, "2\n"); +} Index: test/cfi/overwrite.cpp =================================================================== --- /dev/null +++ test/cfi/overwrite.cpp @@ -0,0 +1,41 @@ +// RUN: %clangxx_cfi -o %t %s +// RUN: not --crash %t 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -o %t %s +// RUN: %t 2>&1 | FileCheck --check-prefix=NCFI %s + +// Tests that the CFI mechanism crashes the program when a virtual table is +// replaced with a compatible table of function pointers that does not belong to +// any class, by manually overwriting the virtual table of an object and +// attempting to make a call through it. + +#include + +struct A { + virtual void f(); +}; + +void A::f() {} + +void foo() { + fprintf(stderr, "foo\n"); +} + +void *fake_vtable[] = { (void *)&foo }; + +int main() { + A *a = new A; + *((void **)a) = fake_vtable; // UB here + + // CFI: 1 + // NCFI: 1 + fprintf(stderr, "1\n"); + + // CFI-NOT: foo + // NCFI: foo + a->f(); + + // CFI-NOT: 2 + // NCFI: 2 + fprintf(stderr, "2\n"); +} Index: test/cfi/simple-fail.cpp =================================================================== --- /dev/null +++ test/cfi/simple-fail.cpp @@ -0,0 +1,37 @@ +// RUN: %clangxx_cfi -o %t %s +// RUN: not --crash %t 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -o %t %s +// RUN: %t 2>&1 | FileCheck --check-prefix=NCFI %s + +// Tests that the CFI mechanism crashes the program when making a virtual call +// to an object of the wrong class but with a compatible vtable, by casting a +// pointer to such an object and attempting to make a call through it. + +#include + +struct A { + virtual void f(); +}; + +void A::f() {} + +struct B { + virtual void f(); +}; + +void B::f() {} + +int main() { + A *a = new A; + + // CFI: 1 + // NCFI: 1 + fprintf(stderr, "1\n"); + + ((B *)a)->f(); // UB here + + // CFI-NOT: 2 + // NCFI: 2 + fprintf(stderr, "2\n"); +} Index: test/cfi/simple-pass.cpp =================================================================== --- /dev/null +++ test/cfi/simple-pass.cpp @@ -0,0 +1,99 @@ +// RUN: %clangxx_cfi -o %t %s +// RUN: %t + +// Tests that the CFI mechanism does not crash the program when making various +// kinds of valid calls involving classes with various different linkages and +// types of inheritance. + +inline void break_optimization(void *arg) { + __asm__ __volatile__("" : : "r" (arg) : "memory"); +} + +struct A { + virtual void f(); +}; + +void A::f() {} + +struct A2 : A { + virtual void f(); +}; + +void A2::f() {} + +struct B { + virtual void f() {} +}; + +struct B2 : B { + virtual void f() {} +}; + +namespace { + +struct C { + virtual void f(); +}; + +void C::f() {} + +struct C2 : C { + virtual void f(); +}; + +void C2::f() {} + +struct D { + virtual void f() {} +}; + +struct D2 : D { + virtual void f() {} +}; + +} + +struct E { + virtual void f() {} +}; + +struct E2 : virtual E { + virtual void f() {} +}; + +int main() { + A *a = new A; + break_optimization(a); + a->f(); + a = new A2; + break_optimization(a); + a->f(); + + B *b = new B; + break_optimization(b); + b->f(); + b = new B2; + break_optimization(b); + b->f(); + + C *c = new C; + break_optimization(c); + c->f(); + c = new C2; + break_optimization(c); + c->f(); + + D *d = new D; + break_optimization(d); + d->f(); + d = new D2; + break_optimization(d); + d->f(); + + E *e = new E; + break_optimization(e); + e->f(); + e = new E2; + break_optimization(e); + e->f(); +} Index: test/cfi/vdtor.cpp =================================================================== --- /dev/null +++ test/cfi/vdtor.cpp @@ -0,0 +1,36 @@ +// RUN: %clangxx_cfi -o %t %s +// RUN: not --crash %t 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -o %t %s +// RUN: %t 2>&1 | FileCheck --check-prefix=NCFI %s + +// Tests that the CFI enforcement also applies to virtual destructor calls made +// via 'delete'. + +#include + +struct A { + virtual ~A(); +}; + +A::~A() {} + +struct B { + virtual ~B(); +}; + +B::~B() {} + +int main() { + A *a = new A; + + // CFI: 1 + // NCFI: 1 + fprintf(stderr, "1\n"); + + delete (B *)a; // UB here + + // CFI-NOT: 2 + // NCFI: 2 + fprintf(stderr, "2\n"); +} Index: test/lit.common.configured.in =================================================================== --- test/lit.common.configured.in +++ test/lit.common.configured.in @@ -18,6 +18,8 @@ set_default("compiler_rt_src_root", "@COMPILER_RT_SOURCE_DIR@") set_default("compiler_rt_obj_root", "@COMPILER_RT_BINARY_DIR@") set_default("llvm_tools_dir", "@LLVM_TOOLS_DIR@") +set_default("llvm_shlib_dir", "@SHLIBDIR@") +set_default("gold_executable", "@GOLD_EXECUTABLE@") set_default("clang", "@COMPILER_RT_TEST_COMPILER@") set_default("compiler_id", "@COMPILER_RT_TEST_COMPILER_ID@") set_default("compiler_rt_arch", "@COMPILER_RT_SUPPORTED_ARCH@")