|
| 1 | +use core::{cell::UnsafeCell, mem}; |
| 2 | + |
| 3 | +use alloc::vec::Vec; |
| 4 | + |
| 5 | +use crate::{core::little_endian::LittleEndianBytes, spinlock::RwSpinLock, RuntimeError}; |
| 6 | + |
| 7 | +/// Implementation of the linear memory suitable for concurrent access |
| 8 | +/// |
| 9 | +/// This linear memory implementation internally relies on a `Vec<UnsafeCell<u8>>`. Thus, the atomic unit of information for it is a byte (`u8`). All access to the linear memory internally occurs through pointers, avoiding the creation of shared and mut refs to the internal data completely. This avoids undefined behavior, except for the race-condition inherent to concurrent writes. Because of this, the [`LinearMemory::store`] function does not require `&mut self` -- `&self` suffices. |
| 10 | +/// |
| 11 | +/// # Notes on locking |
| 12 | +/// |
| 13 | +/// The internal data vector of the [`LinearMemory`] is wrapped in a [`RwLock`]. Despite the |
| 14 | +/// name, writes to the linear memory do not require an acquisition of a write lock. Writes are |
| 15 | +/// implemented through a shared ref to the internal vector, with an `UnsafeCell` to achieve |
| 16 | +/// interior mutability. |
| 17 | +/// |
| 18 | +/// However, linear memory can grow. As the linear memory is implemented via a [`Vec`], a grow can |
| 19 | +/// result in the vector's internal data buffer to be copied over to a bigger, fresh allocation. |
| 20 | +/// The old buffer is then freed. Combined with concurrent mutable access, this can cause |
| 21 | +/// use-after-free. To avoid this, a grow operation of the linear memory acquires a write lock, |
| 22 | +/// blocking all read/write to the linear memory inbetween. |
| 23 | +/// |
| 24 | +/// # Unsafe Note |
| 25 | +/// |
| 26 | +/// Raw pointer access it required, because concurent mutation of the linear memory might happen |
| 27 | +/// (consider the threading proposal for WASM, where mutliple WASM threads access the same linear |
| 28 | +/// memory at the same time). The inherent race condition results in UB w/r/t the state of the `u8`s |
| 29 | +/// in the inner data. However, this is tolerable, e.g. avoiding race conditions on the state of the |
| 30 | +/// linear memory can not be the task of the interpreter, but has to be fulfilled by the interpreted |
| 31 | +/// bytecode itself. |
| 32 | +// TODO if a memmap like operation is available, the linear memory implementation can be optimized brutally. Out-of-bound access can be mapped to userspace handled page-faults, e.g. the MMU takes over that responsibility of catching out of bounds. Grow can happen without copying of data, by mapping new pages consecutively after the current final page of the linear memory. |
| 33 | +pub struct LinearMemory<const PAGE_SIZE: usize = { 1 << 16 }> { |
| 34 | + inner_data: RwSpinLock<Vec<UnsafeCell<u8>>>, |
| 35 | +} |
| 36 | + |
| 37 | +impl<const PAGE_SIZE: usize> LinearMemory<PAGE_SIZE> { |
| 38 | + const PAGE_SIZE: usize = PAGE_SIZE; |
| 39 | + |
| 40 | + /// Create a new, empty [`LinearMemory`] |
| 41 | + pub fn new() -> Self { |
| 42 | + Self { |
| 43 | + inner_data: RwSpinLock::new(Vec::new()), |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + /// Create a new, empty [`LinearMemory`] |
| 48 | + pub fn new_with_capacity(pages: u16) -> Self { |
| 49 | + let size_bytes = Self::PAGE_SIZE * pages as usize; |
| 50 | + let mut data = Vec::with_capacity(size_bytes); |
| 51 | + data.resize_with(size_bytes, || UnsafeCell::new(0)); |
| 52 | + |
| 53 | + Self { |
| 54 | + inner_data: RwSpinLock::new(data), |
| 55 | + } |
| 56 | + } |
| 57 | + |
| 58 | + /// Grow the [`LinearMemory`] by a number of pages |
| 59 | + pub fn grow(&self, pages_to_add: i16) { |
| 60 | + let mut lock_guard = self.inner_data.write(); |
| 61 | + let prior_length_bytes = lock_guard.len(); |
| 62 | + let new_length_bytes = prior_length_bytes + Self::PAGE_SIZE * pages_to_add as usize; |
| 63 | + lock_guard.resize_with(new_length_bytes, || UnsafeCell::new(0)); |
| 64 | + } |
| 65 | + |
| 66 | + /// Get the number of pages currently allocated to this [`LinearMemory`] |
| 67 | + pub fn pages(&self) -> u16 { |
| 68 | + u16::try_from(self.inner_data.read().len() / PAGE_SIZE).unwrap() |
| 69 | + } |
| 70 | + |
| 71 | + /// At a given index, store a datum in the [`LinearMemory`] |
| 72 | + pub fn store<const N: usize, T: LittleEndianBytes<N>>( |
| 73 | + &self, |
| 74 | + index: i32, |
| 75 | + value: T, |
| 76 | + ) -> Result<(), RuntimeError> { |
| 77 | + let value_size = mem::size_of::<T>(); |
| 78 | + |
| 79 | + let lock_guard = self.inner_data.read(); |
| 80 | + let ptr = lock_guard.get(index as usize).unwrap().get(); |
| 81 | + let bytes = value.to_le_bytes(); // |
| 82 | + |
| 83 | + // A value must fit into the linear memory |
| 84 | + assert!( |
| 85 | + value_size <= lock_guard.len(), |
| 86 | + "value does not fit into linear memory" |
| 87 | + ); |
| 88 | + |
| 89 | + // The following statement must be true |
| 90 | + // `index + value_size <= lock_guard.len()` |
| 91 | + // This check verifies it, while avoiding the possible overflow. The subtraction can not |
| 92 | + // underflow because of the previous assert. |
| 93 | + assert!( |
| 94 | + (index as usize) <= lock_guard.len() - value_size, |
| 95 | + "value write would extend beyond the end of the linear_memory" |
| 96 | + ); |
| 97 | + |
| 98 | + // Safety argument: |
| 99 | + // |
| 100 | + // - nonoverlapping is guaranteed, because `src` is a pointer to a stack allocated array, |
| 101 | + // while the destination is heap allocated Vec |
| 102 | + // - the first assert above guarantee that `src` fits into the destination |
| 103 | + // - the second assert above guarantees that even with the offset in `index`, `src` does not |
| 104 | + // extend beyond the destinations last `UnsafeCell<u8>` |
| 105 | + // - the use of `UnsafeCell` avoids any `&` or `&mut` to ever be created on any of the `u8`s |
| 106 | + // contained in the `UnsafeCell`s, so no UB is created through the existence of unsound |
| 107 | + // references |
| 108 | + unsafe { ptr.copy_from_nonoverlapping(bytes.as_ref().as_ptr(), value_size) } |
| 109 | + |
| 110 | + Ok(()) |
| 111 | + } |
| 112 | + |
| 113 | + /// From a given index, load a datum in the [`LinearMemory`] |
| 114 | + pub fn load<const N: usize, T: LittleEndianBytes<N>>( |
| 115 | + &self, |
| 116 | + index: i32, |
| 117 | + ) -> Result<T, RuntimeError> { |
| 118 | + let value_size = mem::size_of::<T>(); |
| 119 | + assert_eq!(value_size, N, "value size must match const generic N"); |
| 120 | + |
| 121 | + let lock_guard = self.inner_data.read(); |
| 122 | + let ptr = lock_guard.get(index as usize).unwrap().get(); |
| 123 | + let mut bytes = [0; N]; |
| 124 | + |
| 125 | + // A value must fit into the linear memory |
| 126 | + assert!( |
| 127 | + value_size <= lock_guard.len(), |
| 128 | + "value does not fit into linear memory" |
| 129 | + ); |
| 130 | + |
| 131 | + // The following statement must be true |
| 132 | + // `index + value_size <= lock_guard.len()` |
| 133 | + // This check verifies it, while avoiding the possible overflow. The subtraction can not |
| 134 | + // underflow because of the previous assert. |
| 135 | + assert!( |
| 136 | + (index as usize) <= lock_guard.len() - value_size, |
| 137 | + "value read would extend beyond the end of the linear_memory" |
| 138 | + ); |
| 139 | + |
| 140 | + // Safety argument: |
| 141 | + // |
| 142 | + // - nonoverlapping is guaranteed, because `dest` is a pointer to a stack allocated array, |
| 143 | + // while the source is heap allocated Vec |
| 144 | + // - the first assert above guarantee that source is bigger than `dest` |
| 145 | + // - the second assert above guarantees that even with the offset in `index`, `dest` does |
| 146 | + // not extend beyond the destinations last `UnsafeCell<u8>` in source |
| 147 | + // - the use of `UnsafeCell` avoids any `&` or `&mut` to ever be created on any of the `u8`s |
| 148 | + // contained in the `UnsafeCell`s, so no UB is created through the existence of unsound |
| 149 | + // references |
| 150 | + unsafe { ptr.copy_to_nonoverlapping(bytes.as_mut_ptr(), bytes.len()) }; |
| 151 | + Ok(T::from_le_bytes(bytes)) |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +impl<const PAGE_SIZE: usize> core::fmt::Debug for LinearMemory<PAGE_SIZE> { |
| 156 | + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| 157 | + write!(f, "LinearMemory {{ inner_data: [ ")?; |
| 158 | + let lock_guard = self.inner_data.read(); |
| 159 | + let mut iter = lock_guard.iter(); |
| 160 | + |
| 161 | + if let Some(first_byte_uc) = iter.next() { |
| 162 | + write!(f, "{}", unsafe { *first_byte_uc.get() })?; |
| 163 | + } |
| 164 | + |
| 165 | + for uc in iter { |
| 166 | + // Safety argument: |
| 167 | + // |
| 168 | + // TODO |
| 169 | + let byte = unsafe { *uc.get() }; |
| 170 | + |
| 171 | + write!(f, ", {byte}")?; |
| 172 | + } |
| 173 | + write!(f, " ] }}") |
| 174 | + } |
| 175 | +} |
| 176 | + |
| 177 | +impl<const PAGE_SIZE: usize> Default for LinearMemory<PAGE_SIZE> { |
| 178 | + fn default() -> Self { |
| 179 | + Self::new() |
| 180 | + } |
| 181 | +} |
| 182 | + |
| 183 | +#[cfg(test)] |
| 184 | +mod test { |
| 185 | + use alloc::format; |
| 186 | + |
| 187 | + use super::*; |
| 188 | + |
| 189 | + const PAGE_SIZE: usize = 1 << 8; |
| 190 | + const PAGES: u16 = 2; |
| 191 | + |
| 192 | + #[test] |
| 193 | + fn new_constructor() { |
| 194 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new(); |
| 195 | + assert_eq!(lin_mem.pages(), 0); |
| 196 | + } |
| 197 | + |
| 198 | + #[test] |
| 199 | + fn new_grow() { |
| 200 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new(); |
| 201 | + lin_mem.grow(1); |
| 202 | + assert_eq!(lin_mem.pages(), 1); |
| 203 | + } |
| 204 | + |
| 205 | + #[test] |
| 206 | + fn debug_print() { |
| 207 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(1); |
| 208 | + assert_eq!(lin_mem.pages(), 1); |
| 209 | + |
| 210 | + let expected_length = "LinearMemory { inner_data: [ ] }".len() + PAGE_SIZE * "0, ".len(); |
| 211 | + let tol = 2; |
| 212 | + |
| 213 | + let debug_repr = format!("{lin_mem:?}"); |
| 214 | + let lower_bound = expected_length - tol; |
| 215 | + let upper_bound = expected_length + tol; |
| 216 | + assert!((lower_bound..upper_bound).contains(&debug_repr.len())); |
| 217 | + } |
| 218 | + |
| 219 | + #[test] |
| 220 | + fn roundtrip_normal_range_i8_neg127() { |
| 221 | + let x: i8 = -127; |
| 222 | + let highest_legal_offset = PAGE_SIZE - mem::size_of::<i8>(); |
| 223 | + for offset in 0..i32::try_from(highest_legal_offset).unwrap() { |
| 224 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(PAGES); |
| 225 | + |
| 226 | + lin_mem.store(offset, x).unwrap(); |
| 227 | + |
| 228 | + assert_eq!( |
| 229 | + lin_mem |
| 230 | + .load::<{ core::mem::size_of::<i8>() }, i8>(offset) |
| 231 | + .unwrap(), |
| 232 | + x, |
| 233 | + "load store roundtrip for {x:?} failed!" |
| 234 | + ); |
| 235 | + } |
| 236 | + } |
| 237 | + |
| 238 | + #[test] |
| 239 | + fn roundtrip_normal_range_f32_13() { |
| 240 | + let x: f32 = 13.0; |
| 241 | + let highest_legal_offset = PAGE_SIZE - mem::size_of::<f32>(); |
| 242 | + for offset in 0..i32::try_from(highest_legal_offset).unwrap() { |
| 243 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(PAGES); |
| 244 | + |
| 245 | + lin_mem.store(offset, x).unwrap(); |
| 246 | + |
| 247 | + assert_eq!( |
| 248 | + lin_mem |
| 249 | + .load::<{ core::mem::size_of::<f32>() }, f32>(offset) |
| 250 | + .unwrap(), |
| 251 | + x, |
| 252 | + "load store roundtrip for {x:?} failed!" |
| 253 | + ); |
| 254 | + } |
| 255 | + } |
| 256 | + |
| 257 | + #[test] |
| 258 | + fn roundtrip_normal_range_f64_min() { |
| 259 | + let x: f64 = f64::MIN; |
| 260 | + let highest_legal_offset = PAGE_SIZE - mem::size_of::<f64>(); |
| 261 | + for offset in 0..i32::try_from(highest_legal_offset).unwrap() { |
| 262 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(PAGES); |
| 263 | + |
| 264 | + lin_mem.store(offset, x).unwrap(); |
| 265 | + |
| 266 | + assert_eq!( |
| 267 | + lin_mem |
| 268 | + .load::<{ core::mem::size_of::<f64>() }, f64>(offset) |
| 269 | + .unwrap(), |
| 270 | + x, |
| 271 | + "load store roundtrip for {x:?} failed!" |
| 272 | + ); |
| 273 | + } |
| 274 | + } |
| 275 | + |
| 276 | + #[test] |
| 277 | + fn roundtrip_normal_range_f64_nan() { |
| 278 | + let x: f64 = f64::NAN; |
| 279 | + let highest_legal_offset = PAGE_SIZE - mem::size_of::<f64>(); |
| 280 | + for offset in 0..i32::try_from(highest_legal_offset).unwrap() { |
| 281 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(PAGES); |
| 282 | + |
| 283 | + lin_mem.store(offset, x).unwrap(); |
| 284 | + |
| 285 | + assert!( |
| 286 | + lin_mem |
| 287 | + .load::<{ core::mem::size_of::<f64>() }, f64>(offset) |
| 288 | + .unwrap() |
| 289 | + .is_nan(), |
| 290 | + "load store roundtrip for {x:?} failed!" |
| 291 | + ); |
| 292 | + } |
| 293 | + } |
| 294 | + |
| 295 | + #[test] |
| 296 | + #[should_panic(expected = "value write would extend beyond the end of the linear_memory")] |
| 297 | + fn store_out_of_range_u128_max() { |
| 298 | + let x: u128 = u128::MAX; |
| 299 | + let pages = 1; |
| 300 | + let lowest_illegal_offset = PAGE_SIZE - mem::size_of::<u128>() + 1; |
| 301 | + let lowest_illegal_offset = i32::try_from(lowest_illegal_offset).unwrap(); |
| 302 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(pages); |
| 303 | + |
| 304 | + lin_mem.store(lowest_illegal_offset, x).unwrap(); |
| 305 | + } |
| 306 | + |
| 307 | + #[test] |
| 308 | + #[should_panic(expected = "called `Option::unwrap()` on a `None` value")] |
| 309 | + fn store_empty_lineaer_memory_u8() { |
| 310 | + let x: u8 = u8::MAX; |
| 311 | + let pages = 0; |
| 312 | + let lowest_illegal_offset = PAGE_SIZE - mem::size_of::<u8>() + 1; |
| 313 | + let lowest_illegal_offset = i32::try_from(lowest_illegal_offset).unwrap(); |
| 314 | + let lin_mem = LinearMemory::<PAGE_SIZE>::new_with_capacity(pages); |
| 315 | + |
| 316 | + lin_mem.store(lowest_illegal_offset, x).unwrap(); |
| 317 | + } |
| 318 | +} |
0 commit comments