diff --git a/Cargo.lock b/Cargo.lock index 1daa8ddcbc245..17b0f26be2791 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,8 +309,9 @@ dependencies = [ "cbindgen", "hashbrown 0.15.2", "libc", + "log", "rustc-hash 2.1.1", - "smallvec", + "test-log", ] [[package]] @@ -5282,6 +5283,28 @@ dependencies = [ "rayon", ] +[[package]] +name = "test-log" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f46083d221181166e5b6f6b1e5f1d499f3a76888826e6cb1d057554157cd0f" +dependencies = [ + "env_logger", + "test-log-macros", + "tracing-subscriber", +] + +[[package]] +name = "test-log-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "888d0c3c6db53c0fdab160d2ed5e12ba745383d3e85813f2ea0f2b1475ab553f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "thin-vec" version = "0.2.13" diff --git a/src/llvm-project b/src/llvm-project index 5c3457300084a..d5bfe98e7d2fe 160000 --- a/src/llvm-project +++ b/src/llvm-project @@ -1 +1 @@ -Subproject commit 5c3457300084a1c47d3d555b24984a7a55a2fcb1 +Subproject commit d5bfe98e7d2fe9df88ef49eb48cb40f621007b6b diff --git a/src/tools/bsan/bsan-rt/Cargo.toml b/src/tools/bsan/bsan-rt/Cargo.toml index 6c2b424c4e077..3fa4b06871d6e 100644 --- a/src/tools/bsan/bsan-rt/Cargo.toml +++ b/src/tools/bsan/bsan-rt/Cargo.toml @@ -4,10 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] +log = "0.4" libc = { version = "0.2.169", default-features = false } hashbrown = { version = "0.15.2", default-features = false, features = ["default-hasher", "nightly", "inline-more"] } rustc-hash = { version = "2.1.1", default-features = false } -smallvec = { version = "1.14.0" } + +[dev-dependencies] +test-log = "0.2.17" [lib] name = "bsan_rt" diff --git a/src/tools/bsan/bsan-rt/src/block.rs b/src/tools/bsan/bsan-rt/src/block.rs index 3b3974189d79f..06e42a083abbf 100644 --- a/src/tools/bsan/bsan-rt/src/block.rs +++ b/src/tools/bsan/bsan-rt/src/block.rs @@ -11,34 +11,65 @@ use crate::*; /// of a singly-linked list. For this implementation to be sound, /// the pointer that is returned must not be mutated concurrently. pub unsafe trait Linkable { - fn next(&self) -> *mut *mut T; + fn next(&mut self) -> *mut *mut T; } /// An mmap-ed chunk of memory that will munmap the chunk on drop. -#[derive(Debug)] pub struct Block { - pub size: NonZeroUsize, + pub num_elements: NonZeroUsize, pub base: NonNull, pub munmap: MUnmap, } impl Block { + /// The number of instances of T that can fit within the block. + #[inline] + fn len(&self) -> NonZeroUsize { + self.num_elements + } + + /// The byte width of the block. + #[inline] + fn byte_width(&self) -> NonZeroUsize { + #[cfg(test)] + let result = self.len().get().checked_mul(mem::size_of::()).unwrap(); + #[cfg(not(test))] + let result = unsafe { self.len().get().unchecked_mul(mem::size_of::()) }; + + unsafe { NonZeroUsize::new_unchecked(result) } + } + /// The last valid, addressable location within the block (at its high-end) + #[inline] fn last(&self) -> *mut T { - unsafe { self.base.as_ptr().add(self.size.get() - 1) } + unsafe { self.base.as_ptr().add(self.len().get() - 1) } } + /// The first valid, addressable location within the block (at its low-end) + #[inline] fn first(&self) -> *mut T { self.base.as_ptr() } } +impl fmt::Debug for Block { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Block") + .field("base", &self.base) + .field("first", &self.first()) + .field("last", &self.last()) + .field("reserved for num_elements", &self.num_elements) + .finish() + } +} + impl Drop for Block { fn drop(&mut self) { // SAFETY: our munmap pointer will be valid by construction of the GlobalCtx. // We can safely transmute it to c_void since that's what it was originally when // it was allocated by mmap - let success = unsafe { (self.munmap)(mem::transmute(self.base.as_ptr()), self.size.get()) }; + let success = + unsafe { (self.munmap)(mem::transmute(self.base.as_ptr()), self.byte_width().get()) }; if success != 0 { panic!("Failed to unmap block!"); } @@ -67,7 +98,7 @@ unsafe impl> Sync for BlockAllocator {} impl> BlockAllocator { /// Initializes a BlockAllocator for the given block. - fn new(block: Block) -> Self { + pub fn new(block: Block) -> Self { BlockAllocator { // we begin at the high-end of the block and decrement downward cursor: AtomicPtr::new(block.last() as *mut MaybeUninit), @@ -80,7 +111,7 @@ impl> BlockAllocator { /// Allocates a new instance from the block. /// If a prior allocation has been freed, it will be reused instead of /// incrementing the internal cursor. - fn alloc(&self) -> Option>> { + pub fn alloc(&self) -> Option>> { if !self.free_lock.swap(true, Ordering::Acquire) { let curr = unsafe { *self.free_list.get() }; let curr = if !curr.is_null() { @@ -139,6 +170,8 @@ mod test { use std::sync::Arc; use std::thread; + use test_log::test; + use super::*; use crate::global::test::TEST_HOOKS; use crate::*; @@ -147,7 +180,7 @@ mod test { } unsafe impl Linkable for Link { - fn next(&self) -> *mut *mut Link { + fn next(&mut self) -> *mut *mut Link { unsafe { mem::transmute(self.link.get()) } } } diff --git a/src/tools/bsan/bsan-rt/src/global.rs b/src/tools/bsan/bsan-rt/src/global.rs index 8373a164be09f..87e0b9f24f233 100644 --- a/src/tools/bsan/bsan-rt/src/global.rs +++ b/src/tools/bsan/bsan-rt/src/global.rs @@ -30,8 +30,11 @@ use crate::*; #[derive(Debug)] pub struct GlobalCtx { hooks: BsanHooks, + // TODO(obraunsdorf): Does the counter need to be AtomicU64, because it would weaken security + // with a counter that wraps around often if we are on fewer-bit architectures? next_alloc_id: AtomicUsize, next_thread_id: AtomicUsize, + alloc_metadata_map: BlockAllocator, } const BSAN_MMAP_PROT: i32 = libc::PROT_READ | libc::PROT_WRITE; @@ -41,36 +44,53 @@ impl GlobalCtx { /// Creates a new instance of `GlobalCtx` using the given `BsanHooks`. /// This function will also initialize our shadow heap fn new(hooks: BsanHooks) -> Self { + let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize; + let alloc_metadata_size = mem::size_of::(); + debug_assert!(0 < alloc_metadata_size && alloc_metadata_size <= page_size); + //let num_elements = NonZeroUsize::new(page_size / mem::size_of::()).unwrap(); + let num_elements = NonZeroUsize::new(1024).unwrap(); + let block = Self::generate_block(hooks.mmap, hooks.munmap, num_elements); Self { hooks, next_alloc_id: AtomicUsize::new(AllocId::min().get()), next_thread_id: AtomicUsize::new(0), + alloc_metadata_map: BlockAllocator::new(block), } } - pub fn new_block(&self, num_elements: NonZeroUsize) -> Block { + pub(crate) unsafe fn allocate_lock_location(&self) -> NonNull> { + match self.alloc_metadata_map.alloc() { + Some(a) => a, + None => { + log::error!("Failed to allocate lock location"); + panic!("Failed to allocate lock location"); + } + } + } + + fn generate_block(mmap: MMap, munmap: MUnmap, num_elements: NonZeroUsize) -> Block { let layout = Layout::array::(num_elements.into()).unwrap(); - let size = NonZeroUsize::new(layout.size()).unwrap(); + let size: NonZero = NonZeroUsize::new(layout.size()).unwrap(); + + debug_assert!(mmap as usize != 0); let base = unsafe { - (self.hooks.mmap)( - ptr::null_mut(), - layout.size(), - BSAN_MMAP_PROT, - BSAN_MMAP_FLAGS, - -1, - 0, - ) + (mmap)(ptr::null_mut(), layout.size(), BSAN_MMAP_PROT, BSAN_MMAP_FLAGS, -1, 0) }; if base.is_null() { panic!("Allocation failed"); } let base = unsafe { NonNull::new_unchecked(mem::transmute(base)) }; - let munmap = self.hooks.munmap; - Block { size, base, munmap } + let munmap = munmap; + + Block { num_elements, base, munmap } + } + + pub fn new_block(&self, num_elements: NonZeroUsize) -> Block { + Self::generate_block(self.hooks.mmap, self.hooks.munmap, num_elements) } - fn allocator(&self) -> BsanAllocHooks { + pub(crate) fn allocator(&self) -> BsanAllocHooks { self.hooks.alloc } @@ -281,7 +301,7 @@ pub unsafe fn global_ctx() -> *mut GlobalCtx { } #[cfg(test)] -pub mod test { +pub(crate) mod test { use crate::*; unsafe extern "C" fn test_print(ptr: *const c_char) { diff --git a/src/tools/bsan/bsan-rt/src/lib.rs b/src/tools/bsan/bsan-rt/src/lib.rs index 9e8f97b50bd55..aace0cc147707 100644 --- a/src/tools/bsan/bsan-rt/src/lib.rs +++ b/src/tools/bsan/bsan-rt/src/lib.rs @@ -8,6 +8,7 @@ #![allow(unused)] extern crate alloc; +use alloc::boxed::Box; use core::alloc::{AllocError, Allocator, GlobalAlloc, Layout}; use core::cell::UnsafeCell; use core::ffi::{c_char, c_ulonglong, c_void}; @@ -18,8 +19,17 @@ use core::panic::PanicInfo; use core::ptr::NonNull; use core::{fmt, mem, ptr}; +use block::Linkable; + mod global; -pub use global::*; + +use global::*; + +mod tree_borrows; +use log::debug; +use tree_borrows::tree_borrows_wrapper as TreeBorrows; + +use crate::ui_test; mod local; pub use local::*; @@ -27,7 +37,83 @@ pub use local::*; mod block; mod shadow; -pub type MMap = unsafe extern "C" fn(*mut c_void, usize, i32, i32, i32, c_ulonglong) -> *mut c_void; +// Atomic counter to assign unique IDs to each allocation +static ALLOC_COUNTER: core::sync::atomic::AtomicU64 = core::sync::atomic::AtomicU64::new(0); + +union FreeListAddrUnion { + free_list_next: *mut AllocMetadata, + base_addr: *const c_void, +} + +impl core::fmt::Debug for FreeListAddrUnion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + unsafe { write!(f, "{:?}", self.base_addr) } + } +} + +/// This is the metadata stored with each allocation +#[derive(Debug)] +pub struct AllocMetadata { + alloc_id: AllocId, + base_addr: FreeListAddrUnion, + // TODO(obraunsdorf) making tree_address a Box is benefitial internal memory-safety of our bsanrt library + // however, a Box with a non-zero-sized allocator takes up more space than just a raw pointer. + // We should consider making this a raw pointer or storing malloc/free pointers in a lazily initialized + // global variable (maybe using OnceCell/OnceLock), + // such that we can make BsanAllocHooks a zero-sized allocator that just uses the global malloc/free. + tree_address: Box, +} + +unsafe impl Linkable for AllocMetadata { + fn next(&mut self) -> *mut *mut AllocMetadata { + // we are re-using the space of base_addr to store the free list pointer + // SAFETY: this is safe because both union fields are raw pointers + unsafe { core::ptr::addr_of_mut!(self.base_addr.free_list_next) } + } +} + +impl AllocMetadata { + fn base_addr(&self) -> *const c_void { + // SAFETY: this is safe because both union fields are raw pointers + unsafe { self.base_addr.base_addr } + } + + fn dealloc(&mut self, borrow_tag: TreeBorrows::BorrowTag) { + self.alloc_id = AllocId::invalid(); + self.tree_address.deallocate(self.base_addr(), borrow_tag); + //TODO(obraunsdorf) free the allocation of the tree itself + } +} + +/// Pointers have provenance (RFC #3559). In Tree Borrows, this includes an allocation ID +/// and a borrow tag. We also include a pointer to the "lock" location for the allocation, +/// which contains all other metadata used to detect undefined behavior. + +#[repr(C)] +#[derive(Debug, Clone, Copy)] +struct Provenance { + pub alloc_id: AllocId, + pub borrow_tag: TreeBorrows::BorrowTag, + pub lock_address: *const AllocMetadata, +} +impl Provenance { + /// The default provenance value, which is assigned to dangling or invalid + /// pointers. + const fn null() -> Self { + Provenance { alloc_id: AllocId::invalid(), borrow_tag: 0, lock_address: ptr::null() } + } + + /// Pointers cast from integers receive a "wildcard" provenance value, which permits + /// any access. + const fn wildcard() -> Self { + Provenance { alloc_id: AllocId::wildcard(), borrow_tag: 0, lock_address: ptr::null() } + } +} + +//#[cfg(all(target_arch = "aarch64", target_os = "linux"))] +pub type MMap = unsafe extern "C" fn(*mut c_void, usize, i32, i32, i32, u64) -> *mut c_void; +//#[cfg(not(all(target_arch = "aarch64", target_os = "linux")))] +//pub type MMap = unsafe extern "C" fn(*mut c_void, usize, i32, i32, i32, c_ulonglong) -> *mut c_void; pub type MUnmap = unsafe extern "C" fn(*mut c_void, usize) -> i32; pub type Malloc = unsafe extern "C" fn(usize) -> *mut c_void; pub type Free = unsafe extern "C" fn(*mut c_void); @@ -84,7 +170,7 @@ impl AllocId { self.0 } /// An invalid allocation - pub const fn null() -> Self { + pub const fn invalid() -> Self { AllocId(0) } @@ -148,65 +234,6 @@ impl fmt::Debug for BorTag { /// of the actions that lead to an aliasing violation. pub type Span = usize; -/// Pointers have provenance (RFC #3559). In Tree Borrows, this includes an allocation ID -/// and a borrow tag. We also include a pointer to the "lock" location for the allocation, -/// which contains all other metadata used to detect undefined behavior. -#[repr(C)] -#[derive(Clone, Copy)] -pub struct Provenance { - pub alloc_id: AllocId, - pub bor_tag: BorTag, - pub alloc_info: *mut c_void, -} - -impl Provenance { - /// The default provenance value, which is assigned to dangling or invalid - /// pointers. - const fn null() -> Self { - Provenance { - alloc_id: AllocId::null(), - bor_tag: BorTag::new(0), - alloc_info: core::ptr::null_mut(), - } - } - - /// Pointers cast from integers receive a "wildcard" provenance value, which permits - /// any access. - const fn wildcard() -> Self { - Provenance { - alloc_id: AllocId::wildcard(), - bor_tag: BorTag::new(0), - alloc_info: core::ptr::null_mut(), - } - } -} - -/// Every allocation is associated with a "lock" object, which is an instance of `AllocInfo`. -/// Provenance is the "key" to this lock. To validate a memory access, we compare the allocation ID -/// of a pointer's provenance with the value stored in its corresponding `AllocInfo` object. If the values -/// do not match, then the access is invalid. If they do match, then we proceed to validate the access against -/// the tree for the allocation. -#[repr(C)] -struct AllocInfo { - pub alloc_id: AllocId, - pub base_addr: usize, - pub size: usize, - pub align: usize, - pub tree: *mut c_void, -} - -impl AllocInfo { - /// When we deallocate an allocation, we need to invalidate its metadata. - /// so that any uses-after-free are detectable. - fn dealloc(&mut self) { - self.alloc_id = AllocId::null(); - self.base_addr = 0; - self.size = 0; - self.align = 1; - // FIXME: free the tree - } -} - /// Initializes the global state of the runtime library. /// The safety of this library is entirely dependent on this /// function having been executed. We assume the global invariant that @@ -275,12 +302,37 @@ extern "C" fn bsan_push_frame(span: Span) {} #[no_mangle] extern "C" fn bsan_pop_frame(span: Span) {} -// Registers a heap allocation of size `size` +/// Creates metadata for a heap allocation of the application. +/// (out:) `prov` is a pointer for returning the provenance (pointer metadata) for this allocation. +/// `span` is a ID to trace back to the source code location of the allocation +/// `object_address` is the address of the allocated object +/// `alloc_size` is the size of the allocated object +/// # Safety +/// The caller must ensure that `bsan_aalloc()` is only called after `bsan_init()` has +/// been called to initialize the global context, esp. the allocator and mmap hooks. + #[no_mangle] -extern "C" fn bsan_alloc(span: Span, prov: *mut MaybeUninit, addr: usize) { - unsafe { - (*prov).write(Provenance::null()); - } +unsafe extern "C" fn bsan_alloc( + span: Span, + prov: *mut MaybeUninit, + object_address: *const c_void, + alloc_size: usize, +) { + debug_assert!(prov != ptr::null_mut()); + let ctx = &*global_ctx(); + let alloc_hooks = ctx.allocator(); + + let tree = Box::new_in(TreeBorrows::Tree::new(object_address, alloc_size), alloc_hooks); + let root_borrow_tag = tree.get_root_borrow_tag(); + let alloc_id = ctx.new_alloc_id(); + let mut lock_location = ctx.allocate_lock_location(); + let alloc_metadata = AllocMetadata { + alloc_id, + base_addr: FreeListAddrUnion { base_addr: object_address }, + tree_address: tree, + }; + let lock_address = lock_location.as_mut().write(alloc_metadata) as *const AllocMetadata; + (*prov).write(Provenance { alloc_id, borrow_tag: root_borrow_tag, lock_address }); } /// Registers a stack allocation of size `size`. @@ -291,9 +343,42 @@ extern "C" fn bsan_alloc_stack(span: Span, prov: *mut MaybeUninit, s } } +// FIXME(obraunsdorf) using thiserror for good error handling +// might be valuable in the future to also catch and report Tree Borrows errors +struct BsanDeallocError { + msg: &'static str, +} +unsafe fn __inner_bsan_dealloc(span: Span, prov: *mut Provenance) -> Result<(), BsanDeallocError> { + let prov = &mut *prov; + let ctx = &*global_ctx(); + let alloc_metadata = &mut *(prov.lock_address as *mut AllocMetadata); + if (alloc_metadata.alloc_id.get() != prov.alloc_id.get()) { + return Err(BsanDeallocError { + //TODO(obraunsdorf) format the id's into the error message for better error reporting + msg: "Allocation ID in pointer metadata does not match the one in the lock address", + }); + } + alloc_metadata.dealloc(prov.borrow_tag); + return Ok(()); +} + /// Deregisters a heap allocation +/// # Safety +/// Mutating alloc_metadata (i.e. deallocating the tree, and invalidating alloc_id) through the provencance +/// metadata (which is copy) is only thread-safe, if the application itself is thread-safe. +/// BSAN is not ensuring thread-safety here +/// if the #[no_mangle] -extern "C" fn bsan_dealloc(span: Span, prov: *mut Provenance) {} +unsafe extern "C" fn bsan_dealloc(span: Span, prov: *mut Provenance) { + let result = __inner_bsan_dealloc(span, prov); + match result { + Ok(_) => {} + Err(e) => { + //TODO(obraunsdorf) use error handling routing from the hooks to print the error message and exit + panic!("BSAN ERROR: {}", e.msg); + } + } +} /// Marks the borrow tag for `prov` as "exposed," allowing it to be resolved to /// validate accesses through "wildcard" pointers. @@ -305,3 +390,84 @@ extern "C" fn bsan_expose_tag(prov: *const Provenance) {} fn panic(info: &PanicInfo<'_>) -> ! { loop {} } + +#[cfg(test)] +mod test { + use core::alloc::{GlobalAlloc, Layout}; + use core::mem::MaybeUninit; + use core::ptr::NonNull; + + use test_log::test; + + use super::*; + use crate::global::test::TEST_HOOKS; + + fn init_bsan_with_test_hooks() { + let bsan_test_hooks = TEST_HOOKS.clone(); + unsafe { + bsan_init(bsan_test_hooks); + } + } + + fn create_metadata() -> Provenance { + let mut prov = MaybeUninit::::uninit(); + let prov_ptr = (&mut prov) as *mut _; + let span1 = 42; + unsafe { + bsan_alloc(span1, prov_ptr, 0xaaaaaaaa as *const c_void, 10); + prov.assume_init() + } + } + + #[test] + fn bsan_alloc_increasing_alloc_id() { + init_bsan_with_test_hooks(); + unsafe { + log::debug!("before bsan_alloc"); + let prov1 = create_metadata(); + log::debug!("directly after bsan_alloc"); + assert_eq!(prov1.alloc_id.get(), 3); + assert_eq!(AllocId::min().get(), 3); + let span2 = 43; + let prov2 = create_metadata(); + assert_eq!(prov2.alloc_id.get(), 4); + } + } + + #[test] + fn bsan_alloc_and_dealloc() { + init_bsan_with_test_hooks(); + unsafe { + let mut prov = create_metadata(); + bsan_dealloc(43, &mut prov as *mut _); + let alloc_metadata = &*(prov.lock_address as *const AllocMetadata); + assert_eq!(alloc_metadata.alloc_id.get(), AllocId::invalid().get()); + assert_eq!(alloc_metadata.alloc_id.get(), 0); + } + } + + #[test] + fn bsan_dealloc_detect_double_free() { + init_bsan_with_test_hooks(); + unsafe { + let mut prov = create_metadata(); + let span = 43; + let _ = __inner_bsan_dealloc(span, &mut prov as *mut _); + let result = __inner_bsan_dealloc(span, &mut prov as *mut _); + assert!(result.is_err()); + } + } + + #[test] + fn bsan_dealloc_detect_invalid_free() { + init_bsan_with_test_hooks(); + unsafe { + let span = 43; + let mut prov = create_metadata(); + let mut modified_prov = prov; + modified_prov.alloc_id = AllocId::new(99); + let result = __inner_bsan_dealloc(span, &mut modified_prov as *mut _); + assert!(result.is_err()); + } + } +} diff --git a/src/tools/bsan/bsan-rt/src/tree_borrows/mod.rs b/src/tools/bsan/bsan-rt/src/tree_borrows/mod.rs new file mode 100644 index 0000000000000..981fe59eb35e9 --- /dev/null +++ b/src/tools/bsan/bsan-rt/src/tree_borrows/mod.rs @@ -0,0 +1,42 @@ +pub(super) mod tree_borrows_wrapper { + use core::ffi::c_void; + pub type BorrowTag = u64; + use alloc::boxed::Box; + + use log::debug; + + #[derive(Debug)] + pub enum TreeBorrowsError { + InvalidAccess, + } + + #[derive(Debug)] + pub struct Tree {} + impl Tree { + pub fn new(object_address: *const c_void, alloc_size: usize) -> Self { + Self {} + } + + pub fn get_root_borrow_tag(&self) -> BorrowTag { + 0 + } + + pub fn deallocate( + &mut self, + object_address: *const c_void, + borrow_tag: BorrowTag, + ) -> Result<(), TreeBorrowsError> { + debug!( + "Invalidating tree for object allocation at {:p} with borrow tag {}", + object_address, borrow_tag + ); + Ok(()) + } + } + + impl Drop for Tree { + fn drop(&mut self) { + debug!("Dropping Tree"); + } + } +}