Index: lib/cfi/cfi.cc =================================================================== --- lib/cfi/cfi.cc +++ lib/cfi/cfi.cc @@ -28,6 +28,7 @@ #include "interception/interception.h" #include "sanitizer_common/sanitizer_common.h" #include "sanitizer_common/sanitizer_flag_parser.h" +#include "sanitizer_common/sanitizer_platform_limits_posix.h" #include "ubsan/ubsan_init.h" #include "ubsan/ubsan_flags.h" @@ -103,12 +104,12 @@ // 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) { +static uptr find_cfi_check_in_dso(uptr base, const char *name, + const ElfW(Phdr) * phdr, int phnum) { 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); + for (int i = 0; i < phnum; ++i) { + if (phdr[i].p_type == PT_DYNAMIC) { + dynamic = (const ElfW(Dyn) *)(base + phdr[i].p_vaddr); break; } } @@ -129,16 +130,16 @@ // 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; + for (phdr_idx = 0; phdr_idx < phnum; phdr_idx++) { + const Elf_Phdr *p = &phdr[phdr_idx]; + if (p->p_type == PT_LOAD) { + uptr beg = base + p->p_vaddr; + uptr end = beg + p->p_memsz; if (strtab >= beg && strtab < end && symtab >= beg && symtab < end) break; } } - if (phdr_idx == info->dlpi_phnum) { + if (phdr_idx == 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); @@ -150,36 +151,50 @@ 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; + uptr addr = base + 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); +static void update_shadow_for_dso(uptr base, const char *name, + const ElfW(Phdr) * phdr, int phnum, + bool load) { + uptr cfi_check; + if (load) { + cfi_check = find_cfi_check_in_dso(base, name, phdr, phnum); + if (cfi_check) + VReport(1, "Module '%s' __cfi_check %zx\n", 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) { + for (int i = 0; i < phnum; i++) { + const Elf_Phdr *p = &phdr[i]; + if (p->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)); + uptr cur_beg = base + p->p_vaddr; + uptr cur_end = cur_beg + p->p_memsz; + if (load) { + 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, kUncheckedShadow); + } } else { - fill_shadow_constant(cur_beg, cur_end, kUncheckedShadow); + fill_shadow_constant(cur_beg, cur_end, kInvalidShadow); } } } +} + +static int dl_iterate_phdr_cb(dl_phdr_info *info, size_t size, void *data) { + update_shadow_for_dso(info->dlpi_addr, info->dlpi_name, info->dlpi_phdr, + info->dlpi_phnum, true /*load*/); return 0; } @@ -188,16 +203,52 @@ dl_iterate_phdr(dl_iterate_phdr_cb, nullptr); } +// Setup shadow for dlopen()ed libraries. +// The actual shadow setup happens after dlopen() returns, which means that +// a library can not be a target of any CFI checks while its constructors are +// running. It's unclear how to fix this without some extra help from libc. +// In glibc, mmap inside dlopen is not interceptable. +// Maybe a seccomp-bpf filter? +// We could insert a high-priority constructor into the library, but that would +// not help with the uninstrumented libraries. +INTERCEPTOR(void*, dlopen, const char *filename, int flag) { + void *handle = REAL(dlopen)(filename, flag); + if (handle) { + link_map *map = GET_LINK_MAP_BY_DLOPEN_HANDLE(handle); + uptr base = map->l_addr; + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)base; + uptr phdr = base + ehdr->e_phoff; + int phnum = ehdr->e_phnum; + update_shadow_for_dso(base, filename, (const ElfW(Phdr) *)phdr, phnum, + true /*load*/); + } + return handle; +} + +INTERCEPTOR(int, dlclose, void *handle) { + if (handle) { + link_map *map = GET_LINK_MAP_BY_DLOPEN_HANDLE(handle); + uptr base = map->l_addr; + ElfW(Ehdr) *ehdr = (ElfW(Ehdr) *)base; + uptr phdr = base + ehdr->e_phoff; + int phnum = ehdr->e_phnum; + update_shadow_for_dso(base, nullptr, (const ElfW(Phdr) *)phdr, phnum, + false /*load*/); + } + int res = REAL(dlclose)(handle); + return res; +} + extern "C" SANITIZER_INTERFACE_ATTRIBUTE 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()) { + if (UNLIKELY(sv.is_invalid())) { VReport(2, "CFI: invalid memory region for a function pointer (shadow==0): %p\n", Ptr); - Die(); + __builtin_trap(); } - if (sv.is_unchecked()) { + if (UNLIKELY(sv.is_unchecked())) { VReport(2, "CFI: unchecked call (shadow=FFFF): %p\n", Ptr); return; } @@ -255,9 +306,14 @@ __cfi_shadow = (uptr)shadow; init_shadow(); + INTERCEPT_FUNCTION(dlopen); + INTERCEPT_FUNCTION(dlclose); + #ifdef CFI_ENABLE_DIAG __ubsan::InitAsPlugin(); #endif + + } #if SANITIZER_CAN_USE_PREINIT_ARRAY Index: test/cfi/cross-dso/dlopen.cpp =================================================================== --- /dev/null +++ test/cfi/cross-dso/dlopen.cpp @@ -0,0 +1,146 @@ +// RUN: %clangxx_cfi_dso -DSHARED_LIB %s -fPIC -shared -o %t1-so.so +// RUN: %clangxx_cfi_dso %s -o %t1 +// RUN: %expect_crash %t1 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t1 cast 2>&1 | FileCheck --check-prefix=CFI-CAST %s +// RUN: %expect_crash %t1 dlclose 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 +// RUN: %expect_crash %t2 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t2 cast 2>&1 | FileCheck --check-prefix=CFI-CAST %s +// RUN: %expect_crash %t2 dlclose 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 +// RUN: %expect_crash %t3 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t3 cast 2>&1 | FileCheck --check-prefix=CFI-CAST %s +// RUN: %expect_crash %t3 dlclose 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 +// RUN: %expect_crash %t4 2>&1 | FileCheck --check-prefix=CFI %s +// RUN: %expect_crash %t4 cast 2>&1 | FileCheck --check-prefix=CFI-CAST %s +// RUN: %expect_crash %t4 dlclose 2>&1 | FileCheck --check-prefix=CFI %s + +// RUN: %clangxx -g -DBM -DSHARED_LIB -DNOCFI %s -fPIC -shared -o %t5-so.so +// RUN: %clangxx -g -DBM -DNOCFI %s -ldl -o %t5 +// RUN: %t5 2>&1 | FileCheck --check-prefix=NCFI %s +// RUN: %t5 cast 2>&1 | FileCheck --check-prefix=NCFI %s +// RUN: %t5 dlclose 2>&1 | FileCheck --check-prefix=NCFI %s + +// Test that calls to uninstrumented library are unchecked. +// RUN: %clangxx -DBM -DSHARED_LIB %s -fPIC -shared -o %t6-so.so +// RUN: %clangxx_cfi_dso -DBM %s -o %t6 +// RUN: %t6 2>&1 | FileCheck --check-prefix=NCFI %s +// RUN: %t6 cast 2>&1 | FileCheck --check-prefix=NCFI %s + +// Call-after-dlclose is checked on the caller side. +// RUN: %expect_crash %t6 dlclose 2>&1 | FileCheck --check-prefix=CFI %s + +// Tests calls into dlopen-ed library. +// REQUIRES: cxxabi + +#include +#include +#include +#include +#include +#include + +struct A { + virtual void f(); +}; + +#ifdef SHARED_LIB + +#include "../utils.h" +struct B { + virtual void f(); +}; +void B::f() {} + +extern "C" void *create_B() { + create_derivers(); + return (void *)(new B()); +} + +extern "C" void do_nothing() {} + +#else + +void A::f() {} + +static const int kCodeAlign = 4096; +static const int kCodeSize = 8192; +static char saved_code[kCodeSize]; +static char *real_start; + +static void save_code(char *p) { + real_start = (char *)(((uintptr_t)p) & ~(kCodeAlign - 1)); + memcpy(saved_code, real_start, kCodeSize); +} + +static void restore_code() { + char *code = (char *)mmap(real_start, kCodeSize, PROT_WRITE | PROT_EXEC, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, 0, 0); + assert(code == real_start); + memcpy(code, saved_code, kCodeSize); +} + +int main(int argc, char *argv[]) { + const bool test_cast = argc > 1 && strcmp(argv[1], "cast") == 0; + const bool test_dlclose = argc > 1 && strcmp(argv[1], "dlclose") == 0; + + char name[100]; + snprintf(name, sizeof(name), "%s-so.so", argv[0]); + void *handle = dlopen(name, RTLD_NOW); + assert(handle); + void *(*create_B)() = (void *(*)())dlsym(handle, "create_B"); + assert(create_B); + + void *p = create_B(); + A *a; + + // CFI: =0= + // CFI-CAST: =0= + // NCFI: =0= + fprintf(stderr, "=0=\n"); + + if (test_cast) { + // 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"); + + if (test_dlclose) { + // Imitate an attacker sneaking in an executable page where a dlclose()d + // library was loaded. This needs to pass w/o CFI, so for the testing + // purpose, we just copy the bytes of a "void f() {}" function back and + // forth. + void (*do_nothing)() = (void (*)())dlsym(handle, "do_nothing"); + assert(do_nothing); + save_code((char *)do_nothing); + + int res = dlclose(handle); + assert(res == 0); + + restore_code(); + + do_nothing(); // UB here + } else { + a->f(); // UB here + } + + // CFI-NOT: =2= + // CFI-CAST-NOT: =2= + // NCFI: =2= + fprintf(stderr, "=2=\n"); +} +#endif Index: test/cfi/cross-dso/simple-fail.cpp =================================================================== --- test/cfi/cross-dso/simple-fail.cpp +++ test/cfi/cross-dso/simple-fail.cpp @@ -19,12 +19,12 @@ // 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: %clangxx_cfi_dso -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 // RUN: %clangxx -DBM -DSHARED_LIB %s -fPIC -shared -o %t6-so.so -// RUN: %clangxx_cfi_dso -DBM %s -o %t6 %t6-so.so +// RUN: %clangxx -DBM %s -o %t6 %t6-so.so // RUN: %t6 2>&1 | FileCheck --check-prefix=NCFI %s // RUN: %t6 x 2>&1 | FileCheck --check-prefix=NCFI %s