use crate::{BorrowHandle, GuestError, Region}; use std::collections::HashMap; use std::sync::Mutex; pub struct BorrowChecker { /// Unfortunately, since the terminology of std::cell and the problem domain of borrow checking /// overlap, the method calls on this member will be confusing. /// /// Also, unfortunately, for now this uses a `Mutex`. The reason for that is /// that this is shared as `&BorrowChecker` in a bunch of `GuestPtr` values. /// Through this sharing we still want each `GuestPtr` to be `Send` and the /// "naive" way to make `&T` `Send` with interior mutability is to use a /// `Mutex`. Fixing this will likely require rethinking `GuestPtr` one way /// or another. That needs to happen for other reasons as well (for example /// to allow for wasm calls to happen while `GuestPtr` values are active), /// so it's hoped that in a later rethinking of `GuestPtr` we can revisit /// this and remove this `Mutex`. bc: Mutex, } impl BorrowChecker { /// A `BorrowChecker` manages run-time validation of borrows from a /// `GuestMemory`. It keeps track of regions of guest memory which are /// possible to alias with Rust references (via the `GuestSlice` and /// `GuestStr` structs, which implement `std::ops::Deref` and /// `std::ops::DerefMut`. It also enforces that `GuestPtr::read` /// does not access memory with an outstanding mutable borrow, and /// `GuestPtr::write` does not access memory with an outstanding /// shared or mutable borrow. pub fn new() -> Self { BorrowChecker { bc: Mutex::new(InnerBorrowChecker::new()), } } /// Indicates whether any outstanding shared or mutable borrows are known /// to the `BorrowChecker`. This function must be `false` in order for it /// to be safe to recursively call into a WebAssembly module, or to /// manipulate the WebAssembly memory by any other means. pub fn has_outstanding_borrows(&self) -> bool { self.bc.lock().unwrap().has_outstanding_borrows() } pub fn shared_borrow(&self, r: Region) -> Result { self.bc.lock().unwrap().shared_borrow(r) } pub fn mut_borrow(&self, r: Region) -> Result { self.bc.lock().unwrap().mut_borrow(r) } pub fn shared_unborrow(&self, h: BorrowHandle) { self.bc.lock().unwrap().shared_unborrow(h) } pub fn mut_unborrow(&self, h: BorrowHandle) { self.bc.lock().unwrap().mut_unborrow(h) } pub fn is_shared_borrowed(&self, r: Region) -> bool { self.bc.lock().unwrap().is_shared_borrowed(r) } pub fn is_mut_borrowed(&self, r: Region) -> bool { self.bc.lock().unwrap().is_mut_borrowed(r) } } #[derive(Debug)] /// This is a pretty naive way to account for borrows. This datastructure /// could be made a lot more efficient with some effort. struct InnerBorrowChecker { /// Maps from handle to region borrowed. A HashMap is probably not ideal /// for this but it works. It would be more efficient if we could /// check `is_borrowed` without an O(n) iteration, by organizing borrows /// by an ordering of Region. shared_borrows: HashMap, mut_borrows: HashMap, /// Handle to give out for the next borrow. This is the bare minimum of /// bookkeeping of free handles, and in a pathological case we could run /// out, hence [`GuestError::BorrowCheckerOutOfHandles`] next_handle: BorrowHandle, } impl InnerBorrowChecker { fn new() -> Self { InnerBorrowChecker { shared_borrows: HashMap::new(), mut_borrows: HashMap::new(), next_handle: BorrowHandle(0), } } fn has_outstanding_borrows(&self) -> bool { !(self.shared_borrows.is_empty() && self.mut_borrows.is_empty()) } fn is_shared_borrowed(&self, r: Region) -> bool { self.shared_borrows.values().any(|b| b.overlaps(r)) } fn is_mut_borrowed(&self, r: Region) -> bool { self.mut_borrows.values().any(|b| b.overlaps(r)) } fn new_handle(&mut self) -> Result { // Reset handles to 0 if all handles have been returned. if self.shared_borrows.is_empty() && self.mut_borrows.is_empty() { self.next_handle = BorrowHandle(0); } let h = self.next_handle; // Get the next handle. Since we don't recycle handles until all of // them have been returned, there is a pathological case where a user // may make a Very Large (usize::MAX) number of valid borrows and // unborrows while always keeping at least one borrow outstanding, and // we will run out of borrow handles. self.next_handle = BorrowHandle( h.0.checked_add(1) .ok_or_else(|| GuestError::BorrowCheckerOutOfHandles)?, ); Ok(h) } fn shared_borrow(&mut self, r: Region) -> Result { if self.is_mut_borrowed(r) { return Err(GuestError::PtrBorrowed(r)); } let h = self.new_handle()?; self.shared_borrows.insert(h, r); Ok(h) } fn mut_borrow(&mut self, r: Region) -> Result { if self.is_shared_borrowed(r) || self.is_mut_borrowed(r) { return Err(GuestError::PtrBorrowed(r)); } let h = self.new_handle()?; self.mut_borrows.insert(h, r); Ok(h) } fn shared_unborrow(&mut self, h: BorrowHandle) { let removed = self.shared_borrows.remove(&h); debug_assert!(removed.is_some(), "double-freed shared borrow"); } fn mut_unborrow(&mut self, h: BorrowHandle) { let removed = self.mut_borrows.remove(&h); debug_assert!(removed.is_some(), "double-freed mut borrow"); } } #[cfg(test)] mod test { use super::*; #[test] fn nonoverlapping() { let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(0, 10); let r2 = Region::new(10, 10); assert!(!r1.overlaps(r2)); bs.mut_borrow(r1).expect("can borrow r1"); bs.mut_borrow(r2).expect("can borrow r2"); let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(10, 10); let r2 = Region::new(0, 10); assert!(!r1.overlaps(r2)); bs.mut_borrow(r1).expect("can borrow r1"); bs.mut_borrow(r2).expect("can borrow r2"); } #[test] fn overlapping() { let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(0, 10); let r2 = Region::new(9, 10); assert!(r1.overlaps(r2)); bs.shared_borrow(r1).expect("can borrow r1"); assert!(bs.mut_borrow(r2).is_err(), "cant mut borrow r2"); bs.shared_borrow(r2).expect("can shared borrow r2"); let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(0, 10); let r2 = Region::new(2, 5); assert!(r1.overlaps(r2)); bs.shared_borrow(r1).expect("can borrow r1"); assert!(bs.mut_borrow(r2).is_err(), "cant borrow r2"); bs.shared_borrow(r2).expect("can shared borrow r2"); let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(9, 10); let r2 = Region::new(0, 10); assert!(r1.overlaps(r2)); bs.shared_borrow(r1).expect("can borrow r1"); assert!(bs.mut_borrow(r2).is_err(), "cant borrow r2"); bs.shared_borrow(r2).expect("can shared borrow r2"); let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(2, 5); let r2 = Region::new(0, 10); assert!(r1.overlaps(r2)); bs.shared_borrow(r1).expect("can borrow r1"); assert!(bs.mut_borrow(r2).is_err(), "cant borrow r2"); bs.shared_borrow(r2).expect("can shared borrow r2"); let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(2, 5); let r2 = Region::new(10, 5); let r3 = Region::new(15, 5); let r4 = Region::new(0, 10); assert!(r1.overlaps(r4)); bs.shared_borrow(r1).expect("can borrow r1"); bs.shared_borrow(r2).expect("can borrow r2"); bs.shared_borrow(r3).expect("can borrow r3"); assert!(bs.mut_borrow(r4).is_err(), "cant mut borrow r4"); bs.shared_borrow(r4).expect("can shared borrow r4"); } #[test] fn unborrowing() { let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(0, 10); let r2 = Region::new(10, 10); assert!(!r1.overlaps(r2)); assert_eq!(bs.has_outstanding_borrows(), false, "start with no borrows"); let h1 = bs.mut_borrow(r1).expect("can borrow r1"); assert_eq!(bs.has_outstanding_borrows(), true, "h1 is outstanding"); let h2 = bs.mut_borrow(r2).expect("can borrow r2"); assert!(bs.mut_borrow(r2).is_err(), "can't borrow r2 twice"); bs.mut_unborrow(h2); assert_eq!( bs.has_outstanding_borrows(), true, "h1 is still outstanding" ); bs.mut_unborrow(h1); assert_eq!(bs.has_outstanding_borrows(), false, "no remaining borrows"); let _h3 = bs .mut_borrow(r2) .expect("can borrow r2 again now that its been unborrowed"); // Lets try again with shared: let mut bs = InnerBorrowChecker::new(); let r1 = Region::new(0, 10); let r2 = Region::new(10, 10); assert!(!r1.overlaps(r2)); assert_eq!(bs.has_outstanding_borrows(), false, "start with no borrows"); let h1 = bs.shared_borrow(r1).expect("can borrow r1"); assert_eq!(bs.has_outstanding_borrows(), true, "h1 is outstanding"); let h2 = bs.shared_borrow(r2).expect("can borrow r2"); let h3 = bs.shared_borrow(r2).expect("can shared borrow r2 twice"); bs.shared_unborrow(h2); assert_eq!( bs.has_outstanding_borrows(), true, "h1, h3 still outstanding" ); bs.shared_unborrow(h1); bs.shared_unborrow(h3); assert_eq!(bs.has_outstanding_borrows(), false, "no remaining borrows"); } }