1 //! Memory operation flags. 2 3 use super::TrapCode; 4 use core::fmt; 5 use core::str::FromStr; 6 7 #[cfg(feature = "enable-serde")] 8 use serde_derive::{Deserialize, Serialize}; 9 10 /// Endianness of a memory access. 11 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] 12 pub enum Endianness { 13 /// Little-endian 14 Little, 15 /// Big-endian 16 Big, 17 } 18 19 /// Which disjoint region of aliasing memory is accessed in this memory 20 /// operation. 21 #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] 22 #[repr(u8)] 23 #[allow(missing_docs)] 24 #[rustfmt::skip] 25 pub enum AliasRegion { 26 // None = 0b00; 27 Heap = 0b01, 28 Table = 0b10, 29 Vmctx = 0b11, 30 } 31 32 impl AliasRegion { 33 const fn from_bits(bits: u8) -> Option<Self> { 34 match bits { 35 0b00 => None, 36 0b01 => Some(Self::Heap), 37 0b10 => Some(Self::Table), 38 0b11 => Some(Self::Vmctx), 39 _ => panic!("invalid alias region bits"), 40 } 41 } 42 43 const fn to_bits(region: Option<Self>) -> u8 { 44 match region { 45 None => 0b00, 46 Some(r) => r as u8, 47 } 48 } 49 } 50 51 /// Flags for memory operations like load/store. 52 /// 53 /// Each of these flags introduce a limited form of undefined behavior. The flags each enable 54 /// certain optimizations that need to make additional assumptions. Generally, the semantics of a 55 /// program does not change when a flag is removed, but adding a flag will. 56 /// 57 /// In addition, the flags determine the endianness of the memory access. By default, 58 /// any memory access uses the native endianness determined by the target ISA. This can 59 /// be overridden for individual accesses by explicitly specifying little- or big-endian 60 /// semantics via the flags. 61 #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] 62 #[cfg_attr(feature = "enable-serde", derive(Serialize, Deserialize))] 63 pub struct MemFlags { 64 // Initialized to all zeros to have all flags have their default value. 65 // This is interpreted through various methods below. Currently the bits of 66 // this are defined as: 67 // 68 // * 0 - aligned flag 69 // * 1 - readonly flag 70 // * 2 - little endian flag 71 // * 3 - big endian flag 72 // * 4 - checked flag 73 // * 5/6 - alias region 74 // * 7/8/9/10 - trap code 75 // * 11/12/13/14/15 - unallocated 76 // 77 // Current properties upheld are: 78 // 79 // * only one of little/big endian is set 80 // * only one alias region can be set - once set it cannot be changed 81 bits: u16, 82 } 83 84 /// Guaranteed to use "natural alignment" for the given type. This 85 /// may enable better instruction selection. 86 const BIT_ALIGNED: u16 = 1 << 0; 87 88 /// A load that reads data in memory that does not change for the 89 /// duration of the function's execution. This may enable 90 /// additional optimizations to be performed. 91 const BIT_READONLY: u16 = 1 << 1; 92 93 /// Load multi-byte values from memory in a little-endian format. 94 const BIT_LITTLE_ENDIAN: u16 = 1 << 2; 95 96 /// Load multi-byte values from memory in a big-endian format. 97 const BIT_BIG_ENDIAN: u16 = 1 << 3; 98 99 /// Check this load or store for safety when using the 100 /// proof-carrying-code framework. The address must have a 101 /// `PointsTo` fact attached with a sufficiently large valid range 102 /// for the accessed size. 103 const BIT_CHECKED: u16 = 1 << 4; 104 105 /// Used for alias analysis, indicates which disjoint part of the abstract state 106 /// is being accessed. 107 const MASK_ALIAS_REGION: u16 = 0b11 << ALIAS_REGION_OFFSET; 108 const ALIAS_REGION_OFFSET: u16 = 5; 109 110 /// Trap code, if any, for this memory operation. 111 const MASK_TRAP_CODE: u16 = 0b1111 << TRAP_CODE_OFFSET; 112 const TRAP_CODE_OFFSET: u16 = 7; 113 114 impl MemFlags { 115 /// Create a new empty set of flags. 116 pub const fn new() -> Self { 117 Self { bits: 0 } 118 } 119 120 /// Create a set of flags representing an access from a "trusted" address, meaning it's 121 /// known to be aligned and non-trapping. 122 pub const fn trusted() -> Self { 123 Self::new().with_notrap().with_aligned() 124 } 125 126 /// Read a flag bit. 127 const fn read_bit(self, bit: u16) -> bool { 128 self.bits & bit != 0 129 } 130 131 /// Return a new `MemFlags` with this flag bit set. 132 const fn with_bit(mut self, bit: u16) -> Self { 133 self.bits |= bit; 134 self 135 } 136 137 /// Reads the alias region that this memory operation works with. 138 pub const fn alias_region(self) -> Option<AliasRegion> { 139 AliasRegion::from_bits(((self.bits & MASK_ALIAS_REGION) >> ALIAS_REGION_OFFSET) as u8) 140 } 141 142 /// Sets the alias region that this works on to the specified `region`. 143 pub const fn with_alias_region(mut self, region: Option<AliasRegion>) -> Self { 144 let bits = AliasRegion::to_bits(region); 145 self.bits &= !MASK_ALIAS_REGION; 146 self.bits |= (bits as u16) << ALIAS_REGION_OFFSET; 147 self 148 } 149 150 /// Sets the alias region that this works on to the specified `region`. 151 pub fn set_alias_region(&mut self, region: Option<AliasRegion>) { 152 *self = self.with_alias_region(region); 153 } 154 155 /// Set a flag bit by name. 156 /// 157 /// Returns true if the flag was found and set, false for an unknown flag 158 /// name. 159 /// 160 /// # Errors 161 /// 162 /// Returns an error message if the `name` is known but couldn't be applied 163 /// due to it being a semantic error. 164 pub fn set_by_name(&mut self, name: &str) -> Result<bool, &'static str> { 165 *self = match name { 166 "notrap" => self.with_trap_code(None), 167 "aligned" => self.with_aligned(), 168 "readonly" => self.with_readonly(), 169 "little" => { 170 if self.read_bit(BIT_BIG_ENDIAN) { 171 return Err("cannot set both big and little endian bits"); 172 } 173 self.with_endianness(Endianness::Little) 174 } 175 "big" => { 176 if self.read_bit(BIT_LITTLE_ENDIAN) { 177 return Err("cannot set both big and little endian bits"); 178 } 179 self.with_endianness(Endianness::Big) 180 } 181 "heap" => { 182 if self.alias_region().is_some() { 183 return Err("cannot set more than one alias region"); 184 } 185 self.with_alias_region(Some(AliasRegion::Heap)) 186 } 187 "table" => { 188 if self.alias_region().is_some() { 189 return Err("cannot set more than one alias region"); 190 } 191 self.with_alias_region(Some(AliasRegion::Table)) 192 } 193 "vmctx" => { 194 if self.alias_region().is_some() { 195 return Err("cannot set more than one alias region"); 196 } 197 self.with_alias_region(Some(AliasRegion::Vmctx)) 198 } 199 "checked" => self.with_checked(), 200 201 other => match TrapCode::from_str(other) { 202 Ok(TrapCode::User(_)) => return Err("cannot set user trap code on mem flags"), 203 Ok(code) => self.with_trap_code(Some(code)), 204 Err(()) => return Ok(false), 205 }, 206 }; 207 Ok(true) 208 } 209 210 /// Return endianness of the memory access. This will return the endianness 211 /// explicitly specified by the flags if any, and will default to the native 212 /// endianness otherwise. The native endianness has to be provided by the 213 /// caller since it is not explicitly encoded in CLIF IR -- this allows a 214 /// front end to create IR without having to know the target endianness. 215 pub const fn endianness(self, native_endianness: Endianness) -> Endianness { 216 if self.read_bit(BIT_LITTLE_ENDIAN) { 217 Endianness::Little 218 } else if self.read_bit(BIT_BIG_ENDIAN) { 219 Endianness::Big 220 } else { 221 native_endianness 222 } 223 } 224 225 /// Set endianness of the memory access. 226 pub fn set_endianness(&mut self, endianness: Endianness) { 227 *self = self.with_endianness(endianness); 228 } 229 230 /// Set endianness of the memory access, returning new flags. 231 pub const fn with_endianness(self, endianness: Endianness) -> Self { 232 let res = match endianness { 233 Endianness::Little => self.with_bit(BIT_LITTLE_ENDIAN), 234 Endianness::Big => self.with_bit(BIT_BIG_ENDIAN), 235 }; 236 assert!(!(res.read_bit(BIT_LITTLE_ENDIAN) && res.read_bit(BIT_BIG_ENDIAN))); 237 res 238 } 239 240 /// Test if this memory operation cannot trap. 241 /// 242 /// By default `MemFlags` will assume that any load/store can trap and is 243 /// associated with a `TrapCode::HeapOutOfBounds` code. If the trap code is 244 /// configured to `None` though then this method will return `true` and 245 /// indicates that the memory operation will not trap. 246 /// 247 /// If this returns `true` then the memory is *accessible*, which means 248 /// that accesses will not trap. This makes it possible to delete an unused 249 /// load or a dead store instruction. 250 pub const fn notrap(self) -> bool { 251 self.trap_code().is_none() 252 } 253 254 /// Sets the trap code for this `MemFlags` to `None`. 255 pub fn set_notrap(&mut self) { 256 *self = self.with_notrap(); 257 } 258 259 /// Sets the trap code for this `MemFlags` to `None`, returning the new 260 /// flags. 261 pub const fn with_notrap(self) -> Self { 262 self.with_trap_code(None) 263 } 264 265 /// Test if the `aligned` flag is set. 266 /// 267 /// By default, Cranelift memory instructions work with any unaligned effective address. If the 268 /// `aligned` flag is set, the instruction is permitted to trap or return a wrong result if the 269 /// effective address is misaligned. 270 pub const fn aligned(self) -> bool { 271 self.read_bit(BIT_ALIGNED) 272 } 273 274 /// Set the `aligned` flag. 275 pub fn set_aligned(&mut self) { 276 *self = self.with_aligned(); 277 } 278 279 /// Set the `aligned` flag, returning new flags. 280 pub const fn with_aligned(self) -> Self { 281 self.with_bit(BIT_ALIGNED) 282 } 283 284 /// Test if the `readonly` flag is set. 285 /// 286 /// Loads with this flag have no memory dependencies. 287 /// This results in undefined behavior if the dereferenced memory is mutated at any time 288 /// between when the function is called and when it is exited. 289 pub const fn readonly(self) -> bool { 290 self.read_bit(BIT_READONLY) 291 } 292 293 /// Set the `readonly` flag. 294 pub fn set_readonly(&mut self) { 295 *self = self.with_readonly(); 296 } 297 298 /// Set the `readonly` flag, returning new flags. 299 pub const fn with_readonly(self) -> Self { 300 self.with_bit(BIT_READONLY) 301 } 302 303 /// Test if the `checked` bit is set. 304 /// 305 /// Loads and stores with this flag are verified to access 306 /// pointers only with a validated `PointsTo` fact attached, and 307 /// with that fact validated, when using the proof-carrying-code 308 /// framework. If initial facts on program inputs are correct 309 /// (i.e., correctly denote the shape and types of data structures 310 /// in memory), and if PCC validates the compiled output, then all 311 /// `checked`-marked memory accesses are guaranteed (up to the 312 /// checker's correctness) to access valid memory. This can be 313 /// used to ensure memory safety and sandboxing. 314 pub const fn checked(self) -> bool { 315 self.read_bit(BIT_CHECKED) 316 } 317 318 /// Set the `checked` bit. 319 pub fn set_checked(&mut self) { 320 *self = self.with_checked(); 321 } 322 323 /// Set the `checked` bit, returning new flags. 324 pub const fn with_checked(self) -> Self { 325 self.with_bit(BIT_CHECKED) 326 } 327 328 /// Get the trap code to report if this memory access traps. 329 /// 330 /// A `None` trap code indicates that this memory access does not trap. 331 pub const fn trap_code(self) -> Option<TrapCode> { 332 // NB: keep this encoding in sync with `with_trap_code` below. 333 // 334 // Also note that the default, all zeros, is `HeapOutOfBounds`. It is 335 // intentionally not `None` so memory operations are all considered 336 // effect-ful by default. 337 match (self.bits & MASK_TRAP_CODE) >> TRAP_CODE_OFFSET { 338 0b0000 => Some(TrapCode::HeapOutOfBounds), 339 0b0001 => Some(TrapCode::StackOverflow), 340 0b0010 => Some(TrapCode::HeapMisaligned), 341 0b0011 => Some(TrapCode::TableOutOfBounds), 342 0b0100 => Some(TrapCode::IndirectCallToNull), 343 0b0101 => Some(TrapCode::BadSignature), 344 0b0110 => Some(TrapCode::IntegerOverflow), 345 0b0111 => Some(TrapCode::IntegerDivisionByZero), 346 0b1000 => Some(TrapCode::BadConversionToInteger), 347 0b1001 => Some(TrapCode::UnreachableCodeReached), 348 0b1010 => Some(TrapCode::Interrupt), 349 0b1011 => Some(TrapCode::NullReference), 350 0b1100 => Some(TrapCode::NullI31Ref), 351 // 0b1101 => {} not allocated 352 // 0b1110 => {} not allocated 353 0b1111 => None, 354 _ => unreachable!(), 355 } 356 } 357 358 /// Configures these flags with the specified trap code `code`. 359 /// 360 /// Note that `TrapCode::User(_)` cannot be set in `MemFlags`. A trap code 361 /// indicates that this memory operation cannot be optimized away and it 362 /// must "stay where it is" in the programs. Traps are considered side 363 /// effects, for example, and have meaning through the trap code that is 364 /// communicated and which instruction trapped. 365 pub const fn with_trap_code(mut self, code: Option<TrapCode>) -> Self { 366 let bits = match code { 367 Some(TrapCode::HeapOutOfBounds) => 0b0000, 368 Some(TrapCode::StackOverflow) => 0b0001, 369 Some(TrapCode::HeapMisaligned) => 0b0010, 370 Some(TrapCode::TableOutOfBounds) => 0b0011, 371 Some(TrapCode::IndirectCallToNull) => 0b0100, 372 Some(TrapCode::BadSignature) => 0b0101, 373 Some(TrapCode::IntegerOverflow) => 0b0110, 374 Some(TrapCode::IntegerDivisionByZero) => 0b0111, 375 Some(TrapCode::BadConversionToInteger) => 0b1000, 376 Some(TrapCode::UnreachableCodeReached) => 0b1001, 377 Some(TrapCode::Interrupt) => 0b1010, 378 Some(TrapCode::NullReference) => 0b1011, 379 Some(TrapCode::NullI31Ref) => 0b1100, 380 None => 0b1111, 381 382 Some(TrapCode::User(_)) => panic!("cannot set user trap code in mem flags"), 383 }; 384 self.bits &= !MASK_TRAP_CODE; 385 self.bits |= bits << TRAP_CODE_OFFSET; 386 self 387 } 388 } 389 390 impl fmt::Display for MemFlags { 391 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 392 match self.trap_code() { 393 None => write!(f, " notrap")?, 394 // This is the default trap code, so don't print anything extra 395 // for this. 396 Some(TrapCode::HeapOutOfBounds) => {} 397 Some(t) => write!(f, " {t}")?, 398 } 399 if self.aligned() { 400 write!(f, " aligned")?; 401 } 402 if self.readonly() { 403 write!(f, " readonly")?; 404 } 405 if self.read_bit(BIT_BIG_ENDIAN) { 406 write!(f, " big")?; 407 } 408 if self.read_bit(BIT_LITTLE_ENDIAN) { 409 write!(f, " little")?; 410 } 411 if self.checked() { 412 write!(f, " checked")?; 413 } 414 match self.alias_region() { 415 None => {} 416 Some(AliasRegion::Heap) => write!(f, " heap")?, 417 Some(AliasRegion::Table) => write!(f, " table")?, 418 Some(AliasRegion::Vmctx) => write!(f, " vmctx")?, 419 } 420 Ok(()) 421 } 422 } 423 424 #[cfg(test)] 425 mod tests { 426 use super::*; 427 428 #[test] 429 fn roundtrip_traps() { 430 for trap in TrapCode::non_user_traps().iter().copied() { 431 let flags = MemFlags::new().with_trap_code(Some(trap)); 432 assert_eq!(flags.trap_code(), Some(trap)); 433 } 434 let flags = MemFlags::new().with_trap_code(None); 435 assert_eq!(flags.trap_code(), None); 436 } 437 438 #[test] 439 fn cannot_set_big_and_little() { 440 let mut big = MemFlags::new().with_endianness(Endianness::Big); 441 assert!(big.set_by_name("little").is_err()); 442 443 let mut little = MemFlags::new().with_endianness(Endianness::Little); 444 assert!(little.set_by_name("big").is_err()); 445 } 446 447 #[test] 448 fn only_one_region() { 449 let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Heap)); 450 assert!(big.set_by_name("table").is_err()); 451 assert!(big.set_by_name("vmctx").is_err()); 452 453 let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Table)); 454 assert!(big.set_by_name("heap").is_err()); 455 assert!(big.set_by_name("vmctx").is_err()); 456 457 let mut big = MemFlags::new().with_alias_region(Some(AliasRegion::Vmctx)); 458 assert!(big.set_by_name("heap").is_err()); 459 assert!(big.set_by_name("table").is_err()); 460 } 461 } 462