diff --git a/libresolve/src/lib.rs b/libresolve/src/lib.rs index 02fb6334..ff9c341e 100644 --- a/libresolve/src/lib.rs +++ b/libresolve/src/lib.rs @@ -2,6 +2,9 @@ // LGPL-3; See LICENSE.txt in the repo root for details. #![feature(btree_cursors)] +#![feature(macro_metavar_expr_concat)] +#![feature(test)] +extern crate test; mod remediate; mod shadowobjs; diff --git a/libresolve/src/remediate.rs b/libresolve/src/remediate.rs index 30c6b9c3..8a0d4bd1 100644 --- a/libresolve/src/remediate.rs +++ b/libresolve/src/remediate.rs @@ -4,7 +4,9 @@ use libc::{ c_char, c_void, calloc, free, malloc, realloc, strdup, strlen, strndup, strnlen, }; -use crate::shadowobjs::{ALIVE_OBJ_LIST, AllocType, FREED_OBJ_LIST, ShadowObject, Vaddr}; +use crate::shadowobjs::{ + ALIVE_OBJ_LIST, AllocType, FREED_OBJ_LIST, STACK_OBJ_LIST, ShadowObject, Vaddr, +}; use log::{error, info, trace, warn}; @@ -24,26 +26,55 @@ use log::{error, info, trace, warn}; #[unsafe(no_mangle)] pub extern "C" fn resolve_stack_obj(ptr: *mut c_void, size: usize) -> () { let base = ptr as Vaddr; - { - let mut obj_list = ALIVE_OBJ_LIST.lock(); - obj_list.add_shadow_object(AllocType::Stack, base, size); - } + + STACK_OBJ_LIST.with_borrow_mut(|l| { + l.add_shadow_object(AllocType::Stack, base, size); + }); info!("[STACK] Object allocated with size: {size}, address: 0x{base:x}"); } +/* #[unsafe(no_mangle)] +pub extern "C" fn resolve_invalidate_stack(base: *mut c_void) { pub extern "C" fn resolve_invalidate_stack(base: *mut c_void) { let base = base as Vaddr; - { - let mut obj_list = ALIVE_OBJ_LIST.lock(); - obj_list.invalidate_at(base); - } + STACK_OBJ_LIST.with_borrow_mut(|l| { + l.invalidate_at(base); + }); + + info!("[STACK] Free range 0x{base:x}"); +} +*/ + +macro_rules! invalidate_stacks { + ($name:ident; $($ns:literal),+) => { + +#[unsafe(no_mangle)] +pub extern "C" fn $name($(${concat(base_, $ns)}: *mut c_void, )+) { + + STACK_OBJ_LIST.with_borrow_mut(|l| { + $( + l.invalidate_at(${concat(base_, $ns)} as Vaddr); + info!("[STACK] Free address 0x{:x}", ${concat(base_, $ns)} as Vaddr); + )+ + }); + +} - info!("[STACK] Free addr 0x{base:x}"); + } } +// the x64 ABI allows up to 6 arguments to be passed via register, +// so provide that many functions as we cannot define a variadic +invalidate_stacks!(resolve_invalidate_stack; 1); +invalidate_stacks!(resolve_invalidate_stack_2; 1, 2); +invalidate_stacks!(resolve_invalidate_stack_3; 1, 2, 3); +invalidate_stacks!(resolve_invalidate_stack_4; 1, 2, 3, 4); +invalidate_stacks!(resolve_invalidate_stack_5; 1, 2, 3, 4, 5); +invalidate_stacks!(resolve_invalidate_stack_6; 1, 2, 3, 4, 5, 6); + /** * @brief - Allocator logging interface for malloc * @input - size of the allocation in bytes @@ -81,10 +112,37 @@ pub extern "C" fn resolve_malloc(size: usize) -> *mut c_void { */ #[unsafe(no_mangle)] -pub extern "C" fn resolve_gep(ptr: *mut c_void, derived: *mut c_void) -> *mut c_void { +pub extern "C" fn resolve_gep(ptr: *mut c_void, derived: *mut c_void, max_access: usize) -> *mut c_void { let base = ptr as Vaddr; let derived = derived as Vaddr; + + let contains_or_err = |obj: &ShadowObject| { + // If shadow object exists then check if the access is within bounds + if obj.contains(ShadowObject::limit(derived, max_access)) { + trace!( + "[GEP] ptr {max_access}x{derived:x} valid for base 0x{base:x}, obj: {}@0x{:x}", + obj.size(), + obj.base + ); + return derived as *mut c_void; + } + + error!( + "[GEP] ptr {max_access}x{derived:x} not valid for base 0x{base:x}, obj: {}@0x{:x}", + obj.size(), + obj.base + ); + + // Pointer known-invalid, return sentinel to indicate failure + 0 as *mut c_void + }; + + // Check the local stack list first since it is cheaper. + if let Some(sobj) = STACK_OBJ_LIST.with_borrow(|l| l.search_intersection(base).cloned()) { + return contains_or_err(&sobj); + }; + let sobj_table = ALIVE_OBJ_LIST.lock(); // Look up the shadow object corresponding to this access. @@ -101,24 +159,7 @@ pub extern "C" fn resolve_gep(ptr: *mut c_void, derived: *mut c_void) -> *mut c_ return derived as *mut c_void; }; - // If shadow object exists then check if the access is within bounds - if sobj.contains(derived as Vaddr) { - info!( - "[GEP] ptr 0x{derived:x} valid for base 0x{base:x}, obj: {}@0x{:x}", - sobj.size(), - sobj.base - ); - return derived as *mut c_void; - } - - error!( - "[GEP] ptr 0x{derived:x} not valid for base 0x{base:x}, obj: {}@0x{:x}", - sobj.size(), - sobj.base - ); - - // Return 1-past limit of allocation @ ptr - sobj.past_limit() as *mut c_void + contains_or_err(sobj) } @@ -317,27 +358,39 @@ pub extern "C" fn resolve_strndup(ptr: *mut c_char, size: usize) -> *mut c_char pub extern "C" fn resolve_check_bounds(base_ptr: *mut c_void, size: usize) -> bool { let base = base_ptr as Vaddr; - let sobj_table = ALIVE_OBJ_LIST.lock(); + // Nullptr clearly are not valid, and may be returned by resolve_gep if it is an invalid call. + if base == 0 { + return false; + } - // Look up the shadow object corresponding to this access - if let Some(sobj) = sobj_table.search_intersection(base) { + let contains_or_err = |obj: &ShadowObject| { // If shadow object exists then check if the access is within bounds - if sobj.contains(ShadowObject::limit(base, size)) { + if obj.contains(ShadowObject::limit(base, size)) { // Access in Bounds trace!( "[BOUNDS] Access allowed {size}@0x{base:x} for allocation {}@0x{:x}", - sobj.size(), - sobj.base + obj.size(), + obj.base ); return true; } else { error!( "[BOUNDS] OOB access at {size}@0x{base:x} too big for allocation {}@0x{:x}", - sobj.size(), - sobj.base + obj.size(), + obj.base ); return false; } + }; + + if let Some(sobj) = STACK_OBJ_LIST.with_borrow(|l| l.search_intersection(base).cloned()) { + return contains_or_err(&sobj); + } + + // Look up the shadow object corresponding to this access + let sobj_table = ALIVE_OBJ_LIST.lock(); + if let Some(sobj) = sobj_table.search_intersection(base) { + return contains_or_err(sobj); } // Check if this is an invalid pointer for one of the known shadow objects @@ -350,6 +403,8 @@ pub extern "C" fn resolve_check_bounds(base_ptr: *mut c_void, size: usize) -> bo return false; } + warn!("[BOUNDS] unknown pointer 0x:{base:x}"); + // Not a tracked pointer, assume good to avoid trapping on otherwise valid pointers // TODO: add a strict mode to reject here / add extra tracking. true @@ -359,13 +414,21 @@ pub extern "C" fn resolve_check_bounds(base_ptr: *mut c_void, size: usize) -> bo pub extern "C" fn resolve_obj_type(base_ptr: *mut c_void) -> AllocType { let base = base_ptr as Vaddr; - let find_in = |table: &crate::MutexWrap| { - let t = table.lock(); - t.search_intersection(base).map(|o| o.alloc_type) + let find_in = |table: &crate::shadowobjs::ShadowObjectTable| { + table.search_intersection(base).map(|o| o.alloc_type) }; // Why does this search freed before alive? - let alloc_type = find_in(&FREED_OBJ_LIST).or_else(|| find_in(&ALIVE_OBJ_LIST)); + let alloc_type = STACK_OBJ_LIST + .with_borrow(|l| find_in(l)) + .or_else(|| { + let l = FREED_OBJ_LIST.lock(); + find_in(&l) + }) + .or_else(|| { + let l = ALIVE_OBJ_LIST.lock(); + find_in(&l) + }); alloc_type.unwrap_or(AllocType::Unknown) } @@ -378,7 +441,7 @@ pub extern "C" fn resolve_obj_type(base_ptr: *mut c_void) -> AllocType { */ #[unsafe(no_mangle)] pub extern "C" fn resolve_report_sanitize_mem_inst_triggered(ptr: *mut c_void) { - info!( + error!( "[SANITIZE] Applying sanitizer to address 0x{:x}", ptr as Vaddr ); @@ -396,6 +459,7 @@ pub extern "C" fn resolve_report_sanitizer_triggered() -> () { mod tests { use super::*; use crate::{resolve_init, shadowobjs::AllocType}; + use test::Bencher; #[test] fn test_malloc_free() { @@ -479,4 +543,24 @@ mod tests { resolve_free(p); } } + + + #[bench] + fn bench_resolve_stack(b: &mut Bencher) { + resolve_init(); + + let addrs: Vec<_> = (0x7FFF_0000_0000_0000..0x7FFF_0000_0001_0000) + .map(|a: usize| a as *mut c_void) + .collect(); + + b.iter(|| { + addrs.iter().for_each(|a| resolve_stack_obj(*a, 1)); + + addrs.iter().for_each(|&a| { + let _ = resolve_gep(a, a, 1); + }); + + addrs.iter().for_each(|a| resolve_invalidate_stack(*a)); + }); + } } diff --git a/libresolve/src/shadowobjs.rs b/libresolve/src/shadowobjs.rs index 878a47d2..368a616a 100644 --- a/libresolve/src/shadowobjs.rs +++ b/libresolve/src/shadowobjs.rs @@ -2,6 +2,7 @@ // LGPL-3; See LICENSE.txt in the repo root for details. use crate::MutexWrap; +use std::cell::RefCell; use std::collections::BTreeMap; use std::ops::RangeInclusive; use std::ops::Bound::Included; @@ -131,6 +132,9 @@ impl ShadowObjectTable { } // static object lists to store all objects +thread_local! { + pub static STACK_OBJ_LIST: RefCell = RefCell::new(ShadowObjectTable::new()); +} pub static ALIVE_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); pub static FREED_OBJ_LIST: MutexWrap = MutexWrap::new(ShadowObjectTable::new()); diff --git a/llvm-plugin/Makefile b/llvm-plugin/Makefile index 8c74d06b..2d58f9f5 100644 --- a/llvm-plugin/Makefile +++ b/llvm-plugin/Makefile @@ -1,10 +1,11 @@ -all: build test -.PHONY: clangformat test +.PHONY: all build clangformat test + +all: build test build: src mkdir -p build - cd build && cmake ../src && make -j4 + cmake -S ./src -B ./build && cd build && make -j4 clangformat: src cd build && make clangformat diff --git a/llvm-plugin/src/CMakeLists.txt b/llvm-plugin/src/CMakeLists.txt index e6b73235..de5841a8 100644 --- a/llvm-plugin/src/CMakeLists.txt +++ b/llvm-plugin/src/CMakeLists.txt @@ -13,7 +13,7 @@ if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE RelWithDebInfo) endif() -# set(CMAKE_CXX_STANDARD 17 STRING "") +set(CMAKE_CXX_STANDARD 20 STRING "") # LLVM is normally built without RTTI. Be consistent with that. if(NOT LLVM_ENABLE_RTTI) diff --git a/llvm-plugin/src/CVEAssert/bounds_check.cpp b/llvm-plugin/src/CVEAssert/bounds_check.cpp index aa52c872..55535c2e 100644 --- a/llvm-plugin/src/CVEAssert/bounds_check.cpp +++ b/llvm-plugin/src/CVEAssert/bounds_check.cpp @@ -301,14 +301,9 @@ void instrumentAlloca(Function *F) { auto size_ty = Type::getInt64Ty(Ctx); auto void_ty = Type::getVoidTy(Ctx); - // Initialize list to store pointers to alloca and instructions + // // Initialize list to store pointers to alloca and instructions std::vector toFreeList; - auto invalidateFn = M->getOrInsertFunction( - "resolve_invalidate_stack", - FunctionType::get(void_ty, { ptr_ty }, false) - ); - auto handle_alloca = [&](auto* allocaInst) { bool hasStart = false; bool hasEnd = false; @@ -346,6 +341,7 @@ void instrumentAlloca(Function *F) { for (auto &BB: *F) { for (auto &instr: BB) { if (auto *inst = dyn_cast(&instr)) { + toFreeList.push_back(inst); handle_alloca(inst); } } @@ -356,6 +352,68 @@ void instrumentAlloca(Function *F) { return; } + auto invalidateFn = M->getOrInsertFunction( + "resolve_invalidate_stack", + FunctionType::get(void_ty, { ptr_ty }, false) + ); + + + auto invalidateFn2 = M->getOrInsertFunction( + "resolve_invalidate_stack_2", + FunctionType::get(void_ty, { ptr_ty, ptr_ty }, false) + ); + + auto invalidateFn3 = M->getOrInsertFunction( + "resolve_invalidate_stack_3", + FunctionType::get(void_ty, { ptr_ty, ptr_ty, ptr_ty }, false) + ); + + auto invalidateFn4 = M->getOrInsertFunction( + "resolve_invalidate_stack_4", + FunctionType::get(void_ty, { ptr_ty, ptr_ty, ptr_ty, ptr_ty }, false) + ); + + auto invalidateFn5 = M->getOrInsertFunction( + "resolve_invalidate_stack_5", + FunctionType::get(void_ty, { ptr_ty, ptr_ty, ptr_ty, ptr_ty, ptr_ty }, false) + ); + + auto invalidateFn6 = M->getOrInsertFunction( + "resolve_invalidate_stack_6", + FunctionType::get(void_ty, { ptr_ty, ptr_ty, ptr_ty, ptr_ty, ptr_ty, ptr_ty }, false) + ); + + + // Try to reduce the number of calls to invalidate each of the stack addrs. + // the x64 ABI allows us to pass up to 6 arguments in registers, so libresolve provides functions with up to arity 6. + auto invalidate_all_at = [&](auto* inst) { + builder.SetInsertPoint(inst); + auto size = toFreeList.size(); + for (auto i = 0; i < toFreeList.size(); i += 6) { + switch ((size - i) % 6) { + case 1: + builder.CreateCall(invalidateFn, { toFreeList[i] }); + break; + case 2: + builder.CreateCall(invalidateFn2, { toFreeList[i], toFreeList[i+1] }); + break; + case 3: + builder.CreateCall(invalidateFn3, { toFreeList[i], toFreeList[i+1], toFreeList[i+2] }); + break; + case 4: + builder.CreateCall(invalidateFn4, { toFreeList[i], toFreeList[i+1], toFreeList[i+2], toFreeList[i+3] }); + break; + case 5: + builder.CreateCall(invalidateFn5, { toFreeList[i], toFreeList[i+1], toFreeList[i+2], toFreeList[i+3], toFreeList[i+4] }); + break; + // 6 + case 0: + builder.CreateCall(invalidateFn6, { toFreeList[i], toFreeList[i+1], toFreeList[i+2], toFreeList[i+3], toFreeList[i+4], toFreeList[i+5] }); + break; + } + } + }; + // Stack grows down, so first allocation is high, last is low // Hmm.. compiler seems to be reordering the allocas in ways // that break this assumption @@ -364,11 +422,7 @@ void instrumentAlloca(Function *F) { for (auto &BB: *F) { for (auto &instr: BB) { if (auto *inst = dyn_cast(&instr)) { - builder.SetInsertPoint(inst); - // builder.CreateCall(invalidateFn, { low, high }); - for (auto *alloca: toFreeList) { - builder.CreateCall(invalidateFn, { alloca }); - } + invalidate_all_at(inst); } } } @@ -476,49 +530,67 @@ void instrumentGEP(Function *F) { const DataLayout &DL = M->getDataLayout(); std::vector gepList; auto ptr_ty = PointerType::get(Ctx, 0); + auto size_ty = Type::getInt64Ty(Ctx); FunctionType *resolveGEPFnTy = FunctionType::get( ptr_ty, - { ptr_ty, ptr_ty }, + { ptr_ty, ptr_ty, size_ty }, false ); - FunctionCallee resolveGEPFn = M->getOrInsertFunction( + FunctionCallee resolveGepFn = M->getOrInsertFunction( "resolve_gep", resolveGEPFnTy ); - for (auto &BB : *F) { - for (auto &inst: BB) { - if (auto *gep = dyn_cast(&inst)) { - gepList.push_back(gep); - } - } - } + std::unordered_set visitedGep; - for (auto GEPInst: gepList) { - builder.SetInsertPoint(GEPInst->getNextNode()); + //auto resolveGepFn = getOrCreateResolveGepSanitizer(M, Ctx, strategy); - // Get the pointer operand and offset from GEP - Value *basePtr = GEPInst->getPointerOperand(); - Value * derivedPtr = GEPInst; - - // Don't assume gep is inbounds, otherwise our remdiation risks being optimized away - GEPInst->setIsInBounds(false); + auto handle_gep = [&](auto* gep) { + + if (visitedGep.contains(gep)) { + return; + } - auto resolveGEPCall = builder.CreateCall(resolveGEPFn, { basePtr, derivedPtr }); + Value* basePtr = gep->getPointerOperand(); + GetElementPtrInst* derivedPtr = gep; + gep->setIsInBounds(false); + + // If we are chaining geps we don't need to check each individually, only the total range in the end. + while (derivedPtr->hasOneUser()) { + if (auto* gep2 = dyn_cast(derivedPtr->user_back())) { + gep2->setIsInBounds(false); + visitedGep.insert(gep2); + derivedPtr = gep2; + } else { + break; + } + } - // Collect users of gep instruction before mutation SmallVector gep_users; - for (User *U : GEPInst->users()) { + for (User *U : derivedPtr->users()) { gep_users.push_back(U); } + builder.SetInsertPoint(derivedPtr->getNextNode()); + auto resolveGepCall = builder.CreateCall(resolveGepFn, { basePtr, derivedPtr, ConstantExpr::getSizeOf(derivedPtr->getResultElementType()) }); + // Iterate over all the users of the gep instruction and - // replace there operands with resolve_gep result + // replace their operands with resolve_gep result for (User *U : gep_users) { - if (U != resolveGEPCall) { - U->replaceUsesOfWith(GEPInst, resolveGEPCall); + if (U != resolveGepCall) { + U->replaceUsesOfWith(derivedPtr, resolveGepCall); + } + } + + visitedGep.insert(gep); + }; + + for (auto &BB : *F) { + for (auto &inst: BB) { + if (auto *gep = dyn_cast(&inst)) { + handle_gep(gep); } } }