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