Index: cmake/config-ix.cmake =================================================================== --- cmake/config-ix.cmake +++ cmake/config-ix.cmake @@ -282,6 +282,7 @@ set(ALL_UBSAN_SUPPORTED_ARCH ${X86} ${X86_64} ${ARM32} ${ARM64} ${MIPS32} ${MIPS64} ${PPC64}) set(ALL_SAFESTACK_SUPPORTED_ARCH ${X86} ${X86_64} ${ARM64}) +set(ALL_CFI_SUPPORTED_ARCH ${X86} ${X86_64}) if(APPLE) include(CompilerRTDarwinUtils) @@ -471,6 +472,9 @@ list_union(SAFESTACK_SUPPORTED_ARCH ALL_SAFESTACK_SUPPORTED_ARCH SANITIZER_COMMON_SUPPORTED_ARCH) + list_union(CFI_SUPPORTED_ARCH + ALL_CFI_SUPPORTED_ARCH + SANITIZER_COMMON_SUPPORTED_ARCH) else() # Architectures supported by compiler-rt libraries. filter_available_targets(BUILTIN_SUPPORTED_ARCH @@ -492,6 +496,7 @@ filter_available_targets(UBSAN_SUPPORTED_ARCH ${ALL_UBSAN_SUPPORTED_ARCH}) filter_available_targets(SAFESTACK_SUPPORTED_ARCH ${ALL_SAFESTACK_SUPPORTED_ARCH}) + filter_available_targets(CFI_SUPPORTED_ARCH ${ALL_CFI_SUPPORTED_ARCH}) endif() message(STATUS "Compiler-RT supported architectures: ${COMPILER_RT_SUPPORTED_ARCH}") @@ -580,3 +585,10 @@ else() set(COMPILER_RT_HAS_SAFESTACK FALSE) endif() + +if (COMPILER_RT_HAS_SANITIZER_COMMON AND CFI_SUPPORTED_ARCH AND + OS_NAME MATCHES "Linux") + set(COMPILER_RT_HAS_CFI TRUE) +else() + set(COMPILER_RT_HAS_CFI FALSE) +endif() Index: lib/CMakeLists.txt =================================================================== --- lib/CMakeLists.txt +++ lib/CMakeLists.txt @@ -19,8 +19,6 @@ add_subdirectory(ubsan) endif() - add_subdirectory(cfi) - if(COMPILER_RT_HAS_ASAN) add_subdirectory(asan) endif() @@ -45,4 +43,8 @@ if(COMPILER_RT_HAS_SAFESTACK) add_subdirectory(safestack) endif() + + if(COMPILER_RT_HAS_CFI) + add_subdirectory(cfi) + endif() endif() Index: lib/cfi/CMakeLists.txt =================================================================== --- lib/cfi/CMakeLists.txt +++ lib/cfi/CMakeLists.txt @@ -1,4 +1,27 @@ add_custom_target(cfi) + +set(CFI_SOURCES cfi.cc) + +include_directories(..) + +set(CFI_CFLAGS + ${SANITIZER_COMMON_CFLAGS} +) + +foreach(arch ${CFI_SUPPORTED_ARCH}) + add_compiler_rt_runtime(clang_rt.cfi + STATIC + ARCHS ${arch} + SOURCES ${CFI_SOURCES} + OBJECT_LIBS RTInterception + RTSanitizerCommon + RTSanitizerCommonLibc + RTUbsan + RTUbsan_cxx + CFLAGS ${CFI_CFLAGS} + PARENT_TARGET cfi) +endforeach() + add_compiler_rt_resource_file(cfi_blacklist cfi_blacklist.txt) add_dependencies(cfi cfi_blacklist) add_dependencies(compiler-rt cfi) Index: lib/cfi/cfi.cc =================================================================== --- /dev/null +++ lib/cfi/cfi.cc @@ -0,0 +1,265 @@ +//===-------- cfi.cc ------------------------------------------------------===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// +// This file implements the runtime support for the cross-DSO CFI. +// +//===----------------------------------------------------------------------===// + +// FIXME: Intercept dlopen/dlclose. +// FIXME: Support diagnostic mode. +// FIXME: Harden: +// * mprotect shadow, use mremap for updates +// * something else equally important + +#include +#include +#include +#include + +typedef ElfW(Phdr) Elf_Phdr; +typedef ElfW(Ehdr) Elf_Ehdr; + +#include "interception/interception.h" +#include "sanitizer_common/sanitizer_common.h" +#include "sanitizer_common/sanitizer_flag_parser.h" +#include "ubsan/ubsan_init.h" +#include "ubsan/ubsan_flags.h" + +static uptr __cfi_shadow; +static constexpr uptr kShadowGranularity = 12; +static constexpr uptr kShadowAlign = 1UL << kShadowGranularity; // 4096 + +static constexpr uint16_t kInvalidShadow = 0; +static constexpr uint16_t kUncheckedShadow = 0xFFFFU; + +static uint16_t *mem_to_shadow(uptr x) { + return (uint16_t *)(__cfi_shadow + ((x >> kShadowGranularity) << 1)); +} + +typedef int (*CFICheckFn)(uptr, void *); + +class ShadowValue { + uptr addr; + uint16_t v; + explicit ShadowValue(uptr addr, uint16_t v) : addr(addr), v(v) {} + +public: + bool is_invalid() const { return v == kInvalidShadow; } + + bool is_unchecked() const { return v == kUncheckedShadow; } + + CFICheckFn get_cfi_check() const { + assert(!is_invalid() && !is_unchecked()); + uptr aligned_addr = addr & ~(kShadowAlign - 1); + uptr p = aligned_addr - (((uptr)v - 1) << kShadowGranularity); + return reinterpret_cast(p); + } + + // Load a shadow valud for the given application memory address. + static const ShadowValue load(uptr addr) { + return ShadowValue(addr, *mem_to_shadow(addr)); + } +}; + +static void fill_shadow_constant(uptr begin, uptr end, uint16_t v) { + assert(v == kInvalidShadow || v == kUncheckedShadow); + uint16_t *shadow_begin = mem_to_shadow(begin); + uint16_t *shadow_end = mem_to_shadow(end - 1) + 1; + memset(shadow_begin, v, (shadow_end - shadow_begin) * sizeof(*shadow_begin)); +} + +static void fill_shadow(uptr begin, uptr end, uptr cfi_check) { + assert((cfi_check & (kShadowAlign - 1)) == 0); + + // Don't fill anything below cfi_check. We can not represent those addresses + // in the shadow, and must make sure at codegen to place all valid call + // targets above cfi_check. + uptr p = Max(begin, cfi_check); + uint16_t *s = mem_to_shadow(p); + uint16_t *s_end = mem_to_shadow(end - 1) + 1; + uint16_t sv = ((p - cfi_check) >> kShadowGranularity) + 1; + for (; s < s_end; s++, sv++) + *s = sv; + + // Sanity checks. + for (; p < end; p += kShadowAlign) { + assert((uptr)ShadowValue::load(p).get_cfi_check() == cfi_check); + assert((uptr)ShadowValue::load(p + kShadowAlign / 2).get_cfi_check() == + cfi_check); + assert((uptr)ShadowValue::load(p + kShadowAlign - 1).get_cfi_check() == + cfi_check); + } +} + +// This is a workaround for a glibc bug: +// https://sourceware.org/bugzilla/show_bug.cgi?id=15199 +// Other platforms can, hopefully, just do +// dlopen(RTLD_NOLOAD | RTLD_LAZY) +// dlsym("__cfi_check"). +static uptr find_cfi_check_in_dso(dl_phdr_info *info) { + const ElfW(Dyn) *dynamic = nullptr; + for (int i = 0; i < info->dlpi_phnum; ++i) { + if (info->dlpi_phdr[i].p_type == PT_DYNAMIC) { + dynamic = + (const ElfW(Dyn) *)(info->dlpi_addr + info->dlpi_phdr[i].p_vaddr); + break; + } + } + if (!dynamic) return 0; + uptr strtab = 0, symtab = 0; + for (const ElfW(Dyn) *p = dynamic; p->d_tag != PT_NULL; ++p) { + if (p->d_tag == DT_SYMTAB) + symtab = p->d_un.d_ptr; + else if (p->d_tag == DT_STRTAB) + strtab = p->d_un.d_ptr; + } + + if (symtab > strtab) { + VReport(1, "Can not handle: symtab > strtab (%p > %zx)\n", symtab, strtab); + return 0; + } + + // Verify that strtab and symtab are inside of the same LOAD segment. + // This excludes VDSO, which has (very high) bogus strtab and symtab pointers. + int phdr_idx; + for (phdr_idx = 0; phdr_idx < info->dlpi_phnum; phdr_idx++) { + const Elf_Phdr *phdr = &info->dlpi_phdr[phdr_idx]; + if (phdr->p_type == PT_LOAD) { + uptr beg = info->dlpi_addr + phdr->p_vaddr; + uptr end = beg + phdr->p_memsz; + if (strtab >= beg && strtab < end && symtab >= beg && symtab < end) + break; + } + } + if (phdr_idx == info->dlpi_phnum) { + // Nope, either different segments or just bogus pointers. + // Can not handle this. + VReport(1, "Can not handle: symtab %p, strtab %zx\n", symtab, strtab); + return 0; + } + + for (const ElfW(Sym) *p = (const ElfW(Sym) *)symtab; (ElfW(Addr))p < strtab; + ++p) { + char *name = (char*)(strtab + p->st_name); + if (strcmp(name, "__cfi_check") == 0) { + assert(p->st_info == ELF32_ST_INFO(STB_GLOBAL, STT_FUNC)); + uptr addr = info->dlpi_addr + p->st_value; + return addr; + } + } + return 0; +} + +static int dl_iterate_phdr_cb(dl_phdr_info *info, size_t size, void *data) { + uptr cfi_check = find_cfi_check_in_dso(info); + if (cfi_check) + VReport(1, "Module '%s' __cfi_check %zx\n", info->dlpi_name, cfi_check); + + for (int i = 0; i < info->dlpi_phnum; i++) { + const Elf_Phdr *phdr = &info->dlpi_phdr[i]; + if (phdr->p_type == PT_LOAD) { + // Jump tables are in the executable segment. + // VTables are in the non-executable one. + // Need to fill shadow for both. + // FIXME: reject writable if vtables are in the r/o segment. Depend on + // PT_RELRO? + uptr cur_beg = info->dlpi_addr + phdr->p_vaddr; + uptr cur_end = cur_beg + phdr->p_memsz; + if (cfi_check) { + VReport(1, " %zx .. %zx\n", cur_beg, cur_end); + fill_shadow(cur_beg, cur_end, cfi_check ? cfi_check : (uptr)(-1)); + } else { + fill_shadow_constant(cur_beg, cur_end, kInvalidShadow); + } + } + } + return 0; +} + +// Fill shadow for the initial libraries. +static void init_shadow() { + dl_iterate_phdr(dl_iterate_phdr_cb, nullptr); +} + +SANITIZER_INTERFACE_ATTRIBUTE extern "C" +void __cfi_slowpath(uptr CallSiteTypeId, void *Ptr) { + uptr Addr = (uptr)Ptr; + VReport(3, "__cfi_slowpath: %zx, %p\n", CallSiteTypeId, Ptr); + ShadowValue sv = ShadowValue::load(Addr); + if (sv.is_invalid()) { + VReport(2, "CFI: invalid memory region for a function pointer (shadow==0): %p\n", Ptr); + Die(); + } + if (sv.is_unchecked()) { + VReport(2, "CFI: unchecked call (shadow=FFFF): %p\n", Ptr); + return; + } + CFICheckFn cfi_check = sv.get_cfi_check(); + VReport(2, "__cfi_check at %p\n", cfi_check); + cfi_check(CallSiteTypeId, Ptr); +} + +static void InitializeFlags() { + SetCommonFlagsDefaults(); + __ubsan::Flags *uf = __ubsan::flags(); + uf->SetDefaults(); + + FlagParser cfi_parser; + RegisterCommonFlags(&cfi_parser); + + FlagParser ubsan_parser; + __ubsan::RegisterUbsanFlags(&ubsan_parser, uf); + RegisterCommonFlags(&ubsan_parser); + + const char *ubsan_default_options = __ubsan::MaybeCallUbsanDefaultOptions(); + ubsan_parser.ParseString(ubsan_default_options); + + cfi_parser.ParseString(GetEnv("CFI_OPTIONS")); + ubsan_parser.ParseString(GetEnv("UBSAN_OPTIONS")); + + SetVerbosity(common_flags()->verbosity); + + if (Verbosity()) ReportUnrecognizedFlags(); + + if (common_flags()->help) { + cfi_parser.PrintFlagDescriptions(); + } +} + +extern "C" __attribute__((visibility("default"))) +#if !SANITIZER_CAN_USE_PREINIT_ARRAY +// On ELF platforms, the constructor is invoked using .preinit_array (see below) +__attribute__((constructor(0))) +#endif +void __cfi_init() { + SanitizerToolName = "CFI"; + InitializeFlags(); + + uptr vma = GetMaxVirtualAddress(); + // Shadow is 2 -> 2**kShadowGranularity. + uptr shadow_size = (vma >> (kShadowGranularity - 1)) + 1; + VReport(1, "CFI: VMA size %zx, shadow size %zx\n", vma, shadow_size); + void *shadow = MmapNoReserveOrDie(shadow_size, "CFI shadow"); + VReport(1, "CFI: shadow at %zx .. %zx\n", shadow, + reinterpret_cast(shadow) + shadow_size); + __cfi_shadow = (uptr)shadow; + init_shadow(); + + __ubsan::InitAsPlugin(); +} + +#if SANITIZER_CAN_USE_PREINIT_ARRAY +// On ELF platforms, run cfi initialization before any other constructors. +// On other platforms we use the constructor attribute to arrange to run our +// initialization early. +extern "C" { +__attribute__((section(".preinit_array"), + used)) void (*__cfi_preinit)(void) = __cfi_init; +} +#endif Index: test/cfi/CMakeLists.txt =================================================================== --- test/cfi/CMakeLists.txt +++ test/cfi/CMakeLists.txt @@ -6,6 +6,7 @@ set(CFI_TEST_DEPS ${SANITIZER_COMMON_LIT_TEST_DEPS}) if(NOT COMPILER_RT_STANDALONE_BUILD) list(APPEND CFI_TEST_DEPS + cfi opt ubsan ) Index: test/cfi/cross-dso/icall/icall-from-dso.cpp =================================================================== --- /dev/null +++ test/cfi/cross-dso/icall/icall-from-dso.cpp @@ -0,0 +1,26 @@ +// RUN: %clangxx_cfi_dso -DSHARED_LIB %s -fPIC -shared -o %t-so.so +// RUN: %clangxx_cfi_dso %s -o %t %t-so.so && %expect_crash %t 2>&1 | FileCheck %s + +#include + +#ifdef SHARED_LIB +void g(); +void f() { + // CHECK: =1= + fprintf(stderr, "=1=\n"); + ((void (*)(void))g)(); + // CHECK: =2= + fprintf(stderr, "=2=\n"); + ((void (*)(int))g)(42); // UB here + // CHECK-NOT: =3= + fprintf(stderr, "=3=\n"); +} +#else +void f(); +void g() { +} + +int main() { + f(); +} +#endif Index: test/cfi/cross-dso/icall/icall.cpp =================================================================== --- /dev/null +++ test/cfi/cross-dso/icall/icall.cpp @@ -0,0 +1,21 @@ +// RUN: %clangxx_cfi_dso -DSHARED_LIB %s -fPIC -shared -o %t-so.so +// RUN: %clangxx_cfi_dso %s -o %t %t-so.so && %expect_crash %t 2>&1 | FileCheck %s + +#include + +#ifdef SHARED_LIB +void f() { +} +#else +void f(); +int main() { + // CHECK: =1= + fprintf(stderr, "=1=\n"); + ((void (*)(void))f)(); + // CHECK: =2= + fprintf(stderr, "=2=\n"); + ((void (*)(int))f)(42); // UB here + // CHECK-NOT: =3= + fprintf(stderr, "=3=\n"); +} +#endif Index: test/cfi/cross-dso/icall/lit.local.cfg =================================================================== --- /dev/null +++ test/cfi/cross-dso/icall/lit.local.cfg @@ -0,0 +1,3 @@ +# The cfi-icall checker is only supported on x86 and x86_64 for now. +if config.root.host_arch not in ['x86', 'x86_64']: + config.unsupported = True Index: test/cfi/cross-dso/simple-fail.cpp =================================================================== --- /dev/null +++ test/cfi/cross-dso/simple-fail.cpp @@ -0,0 +1,87 @@ +// RUN: %clangxx_cfi_dso -DSHARED_LIB %s -fPIC -shared -o %t1-so.so +// RUN: %clangxx_cfi_dso %s -o %t1 %t1-so.so +// RUN: %expect_crash %t1 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t1 x 2>&1 | FileCheck --check-prefix=CFI-CAST %s + +// RUN: %clangxx_cfi_dso -DB32 -DSHARED_LIB %s -fPIC -shared -o %t2-so.so +// RUN: %clangxx_cfi_dso -DB32 %s -o %t2 %t2-so.so +// RUN: %expect_crash %t2 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t2 x 2>&1 | FileCheck --check-prefix=CFI-CAST %s + +// RUN: %clangxx_cfi_dso -DB64 -DSHARED_LIB %s -fPIC -shared -o %t3-so.so +// RUN: %clangxx_cfi_dso -DB64 %s -o %t3 %t3-so.so +// RUN: %expect_crash %t3 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t3 x 2>&1 | FileCheck --check-prefix=CFI-CAST %s + +// RUN: %clangxx_cfi_dso -DBM -DSHARED_LIB %s -fPIC -shared -o %t4-so.so +// RUN: %clangxx_cfi_dso -DBM %s -o %t4 %t4-so.so +// RUN: %expect_crash %t4 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t4 x 2>&1 | FileCheck --check-prefix=CFI-CAST %s + +// RUN: %clangxx -DBM -DSHARED_LIB %s -fPIC -shared -o %t5-so.so +// RUN: %clangxx -DBM %s -o %t5 %t5-so.so +// RUN: %t5 2>&1 | FileCheck --check-prefix=NCFI %s +// RUN: %t5 x 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. + +// REQUIRES: cxxabi + +#include +#include + +struct A { + virtual void f(); +}; + +void *create_B(); + +#ifdef SHARED_LIB + +#include "../utils.h" +struct B { + virtual void f(); +}; +void B::f() {} + +void *create_B() { + create_derivers(); + return (void *)(new B()); +} + +#else + +void A::f() {} + +int main(int argc, char *argv[]) { + void *p = create_B(); + A *a; + + // CFI: =0= + // CFI-CAST: =0= + // NCFI: =0= + fprintf(stderr, "=0=\n"); + + if (argc > 1 && argv[1][0] == 'x') { + // Test cast. BOOM. + a = (A*)p; + } else { + // Invisible to CFI. Test virtual call later. + memcpy(&a, &p, sizeof(a)); + } + + // CFI: =1= + // CFI-CAST-NOT: =1= + // NCFI: =1= + fprintf(stderr, "=1=\n"); + + a->f(); // UB here + + // CFI-NOT: =2= + // CFI-CAST-NOT: =2= + // NCFI: =2= + fprintf(stderr, "=2=\n"); +} +#endif Index: test/cfi/cross-dso/simple-pass.cpp =================================================================== --- /dev/null +++ test/cfi/cross-dso/simple-pass.cpp @@ -0,0 +1,65 @@ +// RUN: %clangxx_cfi_dso -DSHARED_LIB %s -fPIC -shared -o %t1-so.so +// RUN: %clangxx_cfi_dso %s -o %t1 %t1-so.so +// RUN: %t1 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx_cfi_dso -DB32 -DSHARED_LIB %s -fPIC -shared -o %t2-so.so +// RUN: %clangxx_cfi_dso -DB32 %s -o %t2 %t2-so.so +// RUN: %t2 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx_cfi_dso -DB64 -DSHARED_LIB %s -fPIC -shared -o %t3-so.so +// RUN: %clangxx_cfi_dso -DB64 %s -o %t3 %t3-so.so +// RUN: %t3 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx_cfi_dso -DBM -DSHARED_LIB %s -fPIC -shared -o %t4-so.so +// RUN: %clangxx_cfi_dso -DBM %s -o %t4 %t4-so.so +// RUN: %t4 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -DBM -DSHARED_LIB %s -fPIC -shared -o %t5-so.so +// RUN: %clangxx -DBM %s -o %t5 %t5-so.so +// RUN: %t5 2>&1 | FileCheck --check-prefix=NCFI %s +// RUN: %t5 x 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. + +// REQUIRES: cxxabi + +#include +#include + +struct A { + virtual void f(); +}; + +A *create_B(); + +#ifdef SHARED_LIB + +#include "../utils.h" +struct B : public A { + virtual void f(); +}; +void B::f() {} + +A *create_B() { + create_derivers(); + return new B(); +} + +#else + +void A::f() {} + +int main(int argc, char *argv[]) { + A *a = create_B(); + + // CFI: =1= + // NCFI: =1= + fprintf(stderr, "=1=\n"); + a->f(); // OK + // CFI: =2= + // NCFI: =2= + fprintf(stderr, "=2=\n"); +} +#endif Index: test/cfi/lit.cfg =================================================================== --- test/cfi/lit.cfg +++ test/cfi/lit.cfg @@ -10,8 +10,11 @@ config.substitutions.append((r"%clangxx ", clangxx + ' ')) if config.lto_supported: clangxx_cfi = ' '.join(config.lto_launch + [clangxx] + config.lto_flags + ['-flto -fsanitize=cfi ']) + clangxx_cfi_diag = clangxx_cfi + '-fno-sanitize-trap=cfi -fsanitize-recover=cfi ' config.substitutions.append((r"%clangxx_cfi ", clangxx_cfi)) - config.substitutions.append((r"%clangxx_cfi_diag ", clangxx_cfi + '-fno-sanitize-trap=cfi -fsanitize-recover=cfi ')) + config.substitutions.append((r"%clangxx_cfi_diag ", clangxx_cfi_diag)) + config.substitutions.append((r"%clangxx_cfi_dso ", clangxx_cfi + '-fsanitize-cfi-cross-dso ')) + config.substitutions.append((r"%clangxx_cfi_dso_diag ", clangxx_cfi_diag + '-fsanitize-cfi-cross-dso ')) else: config.unsupported = True