1 //! Implementation of Wasm to CLIF memory access translation.
2 //!
3 //! Given
4 //!
5 //! * a dynamic Wasm memory index operand,
6 //! * a static offset immediate, and
7 //! * a static access size,
8 //!
9 //! bounds check the memory access and translate it into a native memory access.
10 //!
11 //! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
12 //! !!!                                                                      !!!
13 //! !!!    THIS CODE IS VERY SUBTLE, HAS MANY SPECIAL CASES, AND IS ALSO     !!!
14 //! !!!   ABSOLUTELY CRITICAL FOR MAINTAINING THE SAFETY OF THE WASM HEAP    !!!
15 //! !!!                             SANDBOX.                                 !!!
16 //! !!!                                                                      !!!
17 //! !!!    A good rule of thumb is to get two reviews on any substantive     !!!
18 //! !!!                         changes in here.                             !!!
19 //! !!!                                                                      !!!
20 //! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
21 
22 use crate::{
23     Reachability,
24     func_environ::FuncEnvironment,
25     translate::{HeapData, TargetEnvironment},
26     trap::TranslateTrap,
27 };
28 use Reachability::*;
29 use cranelift_codegen::{
30     cursor::{Cursor, FuncCursor},
31     ir::{self, InstBuilder, RelSourceLoc, condcodes::IntCC},
32 };
33 use cranelift_frontend::FunctionBuilder;
34 
35 /// The kind of bounds check to perform when accessing a Wasm linear memory or
36 /// GC heap.
37 ///
38 /// Prefer `BoundsCheck::*WholeObject` over `BoundsCheck::Field` when possible,
39 /// as that approach allows the mid-end to deduplicate bounds checks across
40 /// multiple accesses to the same GC object.
41 #[derive(Debug)]
42 pub enum BoundsCheck {
43     /// Check that this one access in particular is in bounds:
44     ///
45     /// ```ignore
46     /// index + offset + access_size <= bound
47     /// ```
48     StaticOffset { offset: u32, access_size: u8 },
49 
50     /// Assuming the precondition `offset + access_size <= object_size`, check
51     /// that this whole object is in bounds:
52     ///
53     /// ```ignore
54     /// index + object_size <= bound
55     /// ```
56     #[cfg(feature = "gc")]
57     StaticObjectField {
58         offset: u32,
59         access_size: u8,
60         object_size: u32,
61     },
62 
63     /// Like `StaticWholeObject` but with dynamic offset and object size.
64     ///
65     /// It is *your* responsibility to ensure that the `offset + access_size <=
66     /// object_size` precondition holds.
67     #[cfg(feature = "gc")]
68     DynamicObjectField {
69         offset: ir::Value,
70         object_size: ir::Value,
71     },
72 }
73 
74 /// Helper used to emit bounds checks (as necessary) and compute the native
75 /// address of a heap access.
76 ///
77 /// Returns the `ir::Value` holding the native address of the heap access, or
78 /// `Reachability::Unreachable` if the heap access will unconditionally trap and
79 /// any subsequent code in this basic block is unreachable.
bounds_check_and_compute_addr( builder: &mut FunctionBuilder, env: &mut FuncEnvironment<'_>, heap: &HeapData, index: ir::Value, bounds_check: BoundsCheck, trap: ir::TrapCode, ) -> Reachability<ir::Value>80 pub fn bounds_check_and_compute_addr(
81     builder: &mut FunctionBuilder,
82     env: &mut FuncEnvironment<'_>,
83     heap: &HeapData,
84     index: ir::Value,
85     bounds_check: BoundsCheck,
86     trap: ir::TrapCode,
87 ) -> Reachability<ir::Value> {
88     match bounds_check {
89         BoundsCheck::StaticOffset {
90             offset,
91             access_size,
92         } => bounds_check_field_access(builder, env, heap, index, offset, access_size, trap),
93 
94         #[cfg(feature = "gc")]
95         BoundsCheck::StaticObjectField {
96             offset,
97             access_size,
98             object_size,
99         } => {
100             // Assert that the precondition holds.
101             let offset_and_access_size = offset.checked_add(access_size.into()).unwrap();
102             assert!(offset_and_access_size <= object_size);
103 
104             // When we can, pretend that we are doing one big access of the
105             // whole object all at once. This enables better GVN for repeated
106             // accesses of the same object.
107             if let Ok(object_size) = u8::try_from(object_size) {
108                 let obj_ptr = match bounds_check_field_access(
109                     builder,
110                     env,
111                     heap,
112                     index,
113                     0,
114                     object_size,
115                     trap,
116                 ) {
117                     Reachable(v) => v,
118                     u @ Unreachable => return u,
119                 };
120                 let offset = builder.ins().iconst(env.pointer_type(), i64::from(offset));
121                 let field_ptr = builder.ins().iadd(obj_ptr, offset);
122                 return Reachable(field_ptr);
123             }
124 
125             // Otherwise, bounds check just this one field's access.
126             bounds_check_field_access(builder, env, heap, index, offset, access_size, trap)
127         }
128 
129         // Compute the index of the end of the object, bounds check that and get
130         // a pointer to just after the object, and then reverse offset from that
131         // to get the pointer to the field being accessed.
132         #[cfg(feature = "gc")]
133         BoundsCheck::DynamicObjectField {
134             offset,
135             object_size,
136         } => {
137             assert_eq!(heap.index_type(), ir::types::I32);
138             assert_eq!(builder.func.dfg.value_type(index), ir::types::I32);
139             assert_eq!(builder.func.dfg.value_type(offset), ir::types::I32);
140             assert_eq!(builder.func.dfg.value_type(object_size), ir::types::I32);
141 
142             let index_and_object_size = builder.ins().uadd_overflow_trap(index, object_size, trap);
143             let ptr_just_after_obj = match bounds_check_field_access(
144                 builder,
145                 env,
146                 heap,
147                 index_and_object_size,
148                 0,
149                 0,
150                 trap,
151             ) {
152                 Reachable(v) => v,
153                 u @ Unreachable => return u,
154             };
155 
156             let backwards_offset = builder.ins().isub(object_size, offset);
157             let backwards_offset = cast_index_to_pointer_ty(
158                 backwards_offset,
159                 ir::types::I32,
160                 env.pointer_type(),
161                 &mut builder.cursor(),
162                 trap,
163             );
164 
165             let field_ptr = builder.ins().isub(ptr_just_after_obj, backwards_offset);
166             Reachable(field_ptr)
167         }
168     }
169 }
170 
bounds_check_field_access( builder: &mut FunctionBuilder, env: &mut FuncEnvironment<'_>, heap: &HeapData, index: ir::Value, offset: u32, access_size: u8, trap: ir::TrapCode, ) -> Reachability<ir::Value>171 fn bounds_check_field_access(
172     builder: &mut FunctionBuilder,
173     env: &mut FuncEnvironment<'_>,
174     heap: &HeapData,
175     index: ir::Value,
176     offset: u32,
177     access_size: u8,
178     trap: ir::TrapCode,
179 ) -> Reachability<ir::Value> {
180     let pointer_bit_width = u16::try_from(env.pointer_type().bits()).unwrap();
181 
182     let clif_memory_traps_enabled = env.clif_memory_traps_enabled();
183     let spectre_mitigations_enabled =
184         env.heap_access_spectre_mitigation() && clif_memory_traps_enabled;
185 
186     let host_page_size_log2 = env.target_config().page_size_align_log2;
187     let can_use_virtual_memory = heap
188         .memory
189         .can_use_virtual_memory(env.tunables(), host_page_size_log2)
190         && clif_memory_traps_enabled;
191     let can_elide_bounds_check = heap
192         .memory
193         .can_elide_bounds_check(env.tunables(), host_page_size_log2)
194         && clif_memory_traps_enabled;
195     let memory_guard_size = env.tunables().memory_guard_size;
196     let memory_reservation = env.tunables().memory_reservation;
197 
198     let offset_and_size = offset_plus_size(offset, access_size);
199     let statically_in_bounds = statically_in_bounds(&builder.func, heap, index, offset_and_size);
200 
201     let index = cast_index_to_pointer_ty(
202         index,
203         heap.index_type(),
204         env.pointer_type(),
205         &mut builder.cursor(),
206         trap,
207     );
208 
209     let oob_behavior = if spectre_mitigations_enabled {
210         OobBehavior::ConditionallyLoadFromZero {
211             select_spectre_guard: true,
212         }
213     } else if env.load_from_zero_allowed() {
214         OobBehavior::ConditionallyLoadFromZero {
215             select_spectre_guard: false,
216         }
217     } else {
218         OobBehavior::ExplicitTrap
219     };
220 
221     let make_compare =
222         |builder: &mut FunctionBuilder, compare_kind: IntCC, lhs: ir::Value, rhs: ir::Value| {
223             builder.ins().icmp(compare_kind, lhs, rhs)
224         };
225 
226     // We need to emit code that will trap (or compute an address that will trap
227     // when accessed) if
228     //
229     //     index + offset + access_size > bound
230     //
231     // or if the `index + offset + access_size` addition overflows.
232     //
233     // Note that we ultimately want a 64-bit integer (we only target 64-bit
234     // architectures at the moment) and that `offset` is a `u32` and
235     // `access_size` is a `u8`. This means that we can add the latter together
236     // as `u64`s without fear of overflow, and we only have to be concerned with
237     // whether adding in `index` will overflow.
238     //
239     // Finally, the following if/else chains do have a little
240     // bit of duplicated code across them, but I think writing it this way is
241     // worth it for readability and seeing very clearly each of our cases for
242     // different bounds checks and optimizations of those bounds checks. It is
243     // intentionally written in a straightforward case-matching style that will
244     // hopefully make it easy to port to ISLE one day.
245     if offset_and_size > heap.memory.maximum_byte_size().unwrap_or(u64::MAX) {
246         // Special case: trap immediately if `offset + access_size >
247         // max_memory_size`, since we will end up being out-of-bounds regardless
248         // of the given `index`.
249         env.before_unconditionally_trapping_memory_access(builder);
250         env.trap(builder, trap);
251         return Unreachable;
252     }
253 
254     // Special case: if this is a 32-bit platform and the `offset_and_size`
255     // overflows the 32-bit address space then there's no hope of this ever
256     // being in-bounds. We can't represent `offset_and_size` in CLIF as the
257     // native pointer type anyway, so this is an unconditional trap.
258     if pointer_bit_width < 64 && offset_and_size >= (1 << pointer_bit_width) {
259         env.before_unconditionally_trapping_memory_access(builder);
260         env.trap(builder, trap);
261         return Unreachable;
262     }
263 
264     // Special case for when we can completely omit explicit
265     // bounds checks for 32-bit memories.
266     //
267     // First, let's rewrite our comparison to move all of the constants
268     // to one side:
269     //
270     //         index + offset + access_size > bound
271     //     ==> index > bound - (offset + access_size)
272     //
273     // We know the subtraction on the right-hand side won't wrap because
274     // we didn't hit the unconditional trap case above.
275     //
276     // Additionally, we add our guard pages (if any) to the right-hand
277     // side, since we can rely on the virtual memory subsystem at runtime
278     // to catch out-of-bound accesses within the range `bound .. bound +
279     // guard_size`. So now we are dealing with
280     //
281     //     index > bound + guard_size - (offset + access_size)
282     //
283     // Note that `bound + guard_size` cannot overflow for
284     // correctly-configured heaps, as otherwise the heap wouldn't fit in
285     // a 64-bit memory space.
286     //
287     // The complement of our should-this-trap comparison expression is
288     // the should-this-not-trap comparison expression:
289     //
290     //     index <= bound + guard_size - (offset + access_size)
291     //
292     // If we know the right-hand side is greater than or equal to
293     // `u32::MAX`, then
294     //
295     //     index <= u32::MAX <= bound + guard_size - (offset + access_size)
296     //
297     // This expression is always true when the heap is indexed with
298     // 32-bit integers because `index` cannot be larger than
299     // `u32::MAX`. This means that `index` is always either in bounds or
300     // within the guard page region, neither of which require emitting an
301     // explicit bounds check.
302     if can_elide_bounds_check
303         && u64::from(u32::MAX) <= memory_reservation + memory_guard_size - offset_and_size
304     {
305         assert!(heap.index_type() == ir::types::I32);
306         assert!(
307             can_use_virtual_memory,
308             "static memories require the ability to use virtual memory"
309         );
310         return Reachable(compute_addr(
311             &mut builder.cursor(),
312             heap,
313             env.pointer_type(),
314             index,
315             offset,
316         ));
317     }
318 
319     // Special case when the `index` is a constant and statically known to be
320     // in-bounds on this memory, no bounds checks necessary.
321     if statically_in_bounds {
322         return Reachable(compute_addr(
323             &mut builder.cursor(),
324             heap,
325             env.pointer_type(),
326             index,
327             offset,
328         ));
329     }
330 
331     // Special case for when we can rely on virtual memory, the minimum
332     // byte size of this memory fits within the memory reservation, and
333     // memory isn't allowed to move. In this situation we know that
334     // memory will statically not grow beyond `memory_reservation` so we
335     // and we know that memory from 0 to that limit is guaranteed to be
336     // valid or trap. Here we effectively assume that the dynamic size
337     // of linear memory is its maximal value, `memory_reservation`, and
338     // we can avoid loading the actual length of memory.
339     //
340     // We have to explicitly test whether
341     //
342     //     index > bound - (offset + access_size)
343     //
344     // and trap if so.
345     //
346     // Since we have to emit explicit bounds checks, we might as well be
347     // precise, not rely on the virtual memory subsystem at all, and not
348     // factor in the guard pages here.
349     if can_use_virtual_memory
350         && heap.memory.minimum_byte_size().unwrap_or(u64::MAX) <= memory_reservation
351         && !heap.memory.memory_may_move(env.tunables())
352         && memory_reservation >= offset_and_size
353     {
354         let adjusted_bound = memory_reservation.checked_sub(offset_and_size).unwrap();
355         let adjusted_bound_value = builder
356             .ins()
357             .iconst(env.pointer_type(), adjusted_bound as i64);
358         let oob = make_compare(
359             builder,
360             IntCC::UnsignedGreaterThan,
361             index,
362             adjusted_bound_value,
363         );
364         return Reachable(explicit_check_oob_condition_and_compute_addr(
365             env,
366             builder,
367             heap,
368             index,
369             offset,
370             oob_behavior,
371             oob,
372             trap,
373         ));
374     }
375 
376     // Special case for when `offset + access_size == 1`:
377     //
378     //         index + 1 > bound
379     //     ==> index >= bound
380     //
381     // Note that this special case is skipped for Pulley targets to assist with
382     // pattern-matching bounds checks into single instructions. Otherwise more
383     // patterns/instructions would have to be added to match this. In the end
384     // the goal is to emit one instruction anyway, so this optimization is
385     // largely only applicable for native platforms.
386     if offset_and_size == 1 && !env.is_pulley() {
387         let bound = get_dynamic_heap_bound(builder, env, heap);
388         let oob = make_compare(builder, IntCC::UnsignedGreaterThanOrEqual, index, bound);
389         return Reachable(explicit_check_oob_condition_and_compute_addr(
390             env,
391             builder,
392             heap,
393             index,
394             offset,
395             oob_behavior,
396             oob,
397             trap,
398         ));
399     }
400 
401     // Special case for when we know that there are enough guard
402     // pages to cover the offset and access size.
403     //
404     // The precise should-we-trap condition is
405     //
406     //     index + offset + access_size > bound
407     //
408     // However, if we instead check only the partial condition
409     //
410     //     index > bound
411     //
412     // then the most out of bounds that the access can be, while that
413     // partial check still succeeds, is `offset + access_size`.
414     //
415     // However, when we have a guard region that is at least as large as
416     // `offset + access_size`, we can rely on the virtual memory
417     // subsystem handling these out-of-bounds errors at
418     // runtime. Therefore, the partial `index > bound` check is
419     // sufficient for this heap configuration.
420     //
421     // Additionally, this has the advantage that a series of Wasm loads
422     // that use the same dynamic index operand but different static
423     // offset immediates -- which is a common code pattern when accessing
424     // multiple fields in the same struct that is in linear memory --
425     // will all emit the same `index > bound` check, which we can GVN.
426     if can_use_virtual_memory && offset_and_size <= memory_guard_size {
427         let bound = get_dynamic_heap_bound(builder, env, heap);
428         let oob = make_compare(builder, IntCC::UnsignedGreaterThan, index, bound);
429         return Reachable(explicit_check_oob_condition_and_compute_addr(
430             env,
431             builder,
432             heap,
433             index,
434             offset,
435             oob_behavior,
436             oob,
437             trap,
438         ));
439     }
440 
441     // Special case for when `offset + access_size <= min_size`.
442     //
443     // We know that `bound >= min_size`, so we can do the following
444     // comparison, without fear of the right-hand side wrapping around:
445     //
446     //         index + offset + access_size > bound
447     //     ==> index > bound - (offset + access_size)
448     if offset_and_size <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX) {
449         let bound = get_dynamic_heap_bound(builder, env, heap);
450         let adjustment = offset_and_size as i64;
451         let adjustment_value = builder.ins().iconst(env.pointer_type(), adjustment);
452         let adjusted_bound = builder.ins().isub(bound, adjustment_value);
453         let oob = make_compare(builder, IntCC::UnsignedGreaterThan, index, adjusted_bound);
454         return Reachable(explicit_check_oob_condition_and_compute_addr(
455             env,
456             builder,
457             heap,
458             index,
459             offset,
460             oob_behavior,
461             oob,
462             trap,
463         ));
464     }
465 
466     // General case for dynamic bounds checks:
467     //
468     //     index + offset + access_size > bound
469     //
470     // And we have to handle the overflow case in the left-hand side.
471     let access_size_val = builder
472         .ins()
473         // Explicit cast from u64 to i64: we just want the raw
474         // bits, and iconst takes an `Imm64`.
475         .iconst(env.pointer_type(), offset_and_size as i64);
476     let adjusted_index = env.uadd_overflow_trap(builder, index, access_size_val, trap);
477     let bound = get_dynamic_heap_bound(builder, env, heap);
478     let oob = make_compare(builder, IntCC::UnsignedGreaterThan, adjusted_index, bound);
479     Reachable(explicit_check_oob_condition_and_compute_addr(
480         env,
481         builder,
482         heap,
483         index,
484         offset,
485         oob_behavior,
486         oob,
487         trap,
488     ))
489 }
490 
491 /// Get the bound of a dynamic heap as an `ir::Value`.
get_dynamic_heap_bound( builder: &mut FunctionBuilder, env: &mut FuncEnvironment<'_>, heap: &HeapData, ) -> ir::Value492 fn get_dynamic_heap_bound(
493     builder: &mut FunctionBuilder,
494     env: &mut FuncEnvironment<'_>,
495     heap: &HeapData,
496 ) -> ir::Value {
497     match heap.memory.static_heap_size() {
498         // The heap has a constant size, no need to actually load the
499         // bound.
500         Some(max_size) => builder.ins().iconst(env.pointer_type(), max_size as i64),
501 
502         // Load the heap bound from its global variable.
503         _ => builder.ins().global_value(env.pointer_type(), heap.bound),
504     }
505 }
506 
cast_index_to_pointer_ty( index: ir::Value, index_ty: ir::Type, pointer_ty: ir::Type, pos: &mut FuncCursor, trap: ir::TrapCode, ) -> ir::Value507 fn cast_index_to_pointer_ty(
508     index: ir::Value,
509     index_ty: ir::Type,
510     pointer_ty: ir::Type,
511     pos: &mut FuncCursor,
512     trap: ir::TrapCode,
513 ) -> ir::Value {
514     if index_ty == pointer_ty {
515         return index;
516     }
517 
518     // If the index size is larger than the pointer, that means that this is a
519     // 32-bit host platform with a 64-bit wasm linear memory. If the index is
520     // larger than 2**32 then that's guaranteed to be out-of-bounds, otherwise we
521     // `ireduce` the index.
522     //
523     // Also note that at this time this branch doesn't support the
524     // value-label-ranges of the below path.
525     //
526     // Finally, note that the returned `low_bits` here are still subject to an
527     // explicit bounds check in wasm so in terms of Spectre speculation on
528     // either side of the `trapnz` should be ok.
529     if index_ty.bits() > pointer_ty.bits() {
530         assert_eq!(index_ty, ir::types::I64);
531         assert_eq!(pointer_ty, ir::types::I32);
532         let low_bits = pos.ins().ireduce(pointer_ty, index);
533         let c32 = pos.ins().iconst(pointer_ty, 32);
534         let high_bits = pos.ins().ushr(index, c32);
535         let high_bits = pos.ins().ireduce(pointer_ty, high_bits);
536         pos.ins().trapnz(high_bits, trap);
537         return low_bits;
538     }
539 
540     // Convert `index` to `addr_ty`.
541     let extended_index = pos.ins().uextend(pointer_ty, index);
542 
543     // Add debug value-label alias so that debuginfo can name the extended
544     // value as the address
545     let loc = pos.srcloc();
546     let loc = RelSourceLoc::from_base_offset(pos.func.params.base_srcloc(), loc);
547     pos.func
548         .stencil
549         .dfg
550         .add_value_label_alias(extended_index, loc, index);
551 
552     extended_index
553 }
554 
555 /// What to do on out-of-bounds for the
556 /// `explicit_check_oob_condition_and_compute_addr` function below.
557 enum OobBehavior {
558     /// An explicit `trapnz` instruction should be used.
559     ExplicitTrap,
560     /// A load from NULL should be issued if the address is out-of-bounds.
561     ConditionallyLoadFromZero {
562         /// Whether or not to use `select_spectre_guard` to choose the address
563         /// to load from. If `false` then a normal `select` is used.
564         select_spectre_guard: bool,
565     },
566 }
567 
568 /// Emit explicit checks on the given out-of-bounds condition for the Wasm
569 /// address and return the native address.
570 ///
571 /// This function deduplicates explicit bounds checks and Spectre mitigations
572 /// that inherently also implement bounds checking.
explicit_check_oob_condition_and_compute_addr( env: &mut FuncEnvironment<'_>, builder: &mut FunctionBuilder, heap: &HeapData, index: ir::Value, offset: u32, oob_behavior: OobBehavior, oob_condition: ir::Value, trap: ir::TrapCode, ) -> ir::Value573 fn explicit_check_oob_condition_and_compute_addr(
574     env: &mut FuncEnvironment<'_>,
575     builder: &mut FunctionBuilder,
576     heap: &HeapData,
577     index: ir::Value,
578     offset: u32,
579     oob_behavior: OobBehavior,
580     // The `i8` boolean value that is non-zero when the heap access is out of
581     // bounds (and therefore we should trap) and is zero when the heap access is
582     // in bounds (and therefore we can proceed).
583     oob_condition: ir::Value,
584     trap: ir::TrapCode,
585 ) -> ir::Value {
586     if let OobBehavior::ExplicitTrap = oob_behavior {
587         env.trapnz(builder, oob_condition, trap);
588     }
589     let addr_ty = env.pointer_type();
590 
591     let mut addr = compute_addr(&mut builder.cursor(), heap, addr_ty, index, offset);
592 
593     if let OobBehavior::ConditionallyLoadFromZero {
594         select_spectre_guard,
595     } = oob_behavior
596     {
597         // These mitigations rely on trapping when loading from NULL so
598         // CLIF memory instruction traps must be allowed for this to be
599         // generated.
600         assert!(env.load_from_zero_allowed());
601         let null = builder.ins().iconst(addr_ty, 0);
602         addr = if select_spectre_guard {
603             builder
604                 .ins()
605                 .select_spectre_guard(oob_condition, null, addr)
606         } else {
607             builder.ins().select(oob_condition, null, addr)
608         };
609     }
610 
611     addr
612 }
613 
614 /// Emit code for the native address computation of a Wasm address,
615 /// without any bounds checks or overflow checks.
616 ///
617 /// It is the caller's responsibility to ensure that any necessary bounds and
618 /// overflow checks are emitted, and that the resulting address is never used
619 /// unless they succeed.
compute_addr( pos: &mut FuncCursor, heap: &HeapData, addr_ty: ir::Type, index: ir::Value, offset: u32, ) -> ir::Value620 fn compute_addr(
621     pos: &mut FuncCursor,
622     heap: &HeapData,
623     addr_ty: ir::Type,
624     index: ir::Value,
625     offset: u32,
626 ) -> ir::Value {
627     debug_assert_eq!(pos.func.dfg.value_type(index), addr_ty);
628 
629     let heap_base = pos.ins().global_value(addr_ty, heap.base);
630     let base_and_index = pos.ins().iadd(heap_base, index);
631 
632     if offset == 0 {
633         base_and_index
634     } else {
635         // NB: The addition of the offset immediate must happen *before* the
636         // `select_spectre_guard`, if any. If it happens after, then we
637         // potentially are letting speculative execution read the whole first
638         // 4GiB of memory.
639         let offset_val = pos.ins().iconst(addr_ty, i64::from(offset));
640         pos.ins().iadd(base_and_index, offset_val)
641     }
642 }
643 
644 #[inline]
offset_plus_size(offset: u32, size: u8) -> u64645 fn offset_plus_size(offset: u32, size: u8) -> u64 {
646     // Cannot overflow because we are widening to `u64`.
647     offset as u64 + size as u64
648 }
649 
650 /// Returns whether `index` is statically in-bounds with respect to this
651 /// `heap`'s configuration.
652 ///
653 /// This is `true` when `index` is a constant and when the offset/size are added
654 /// in it's all still less than the minimum byte size of the heap.
655 ///
656 /// The `offset_and_size` here are the static offset that was listed on the wasm
657 /// instruction plus the size of the access being made.
statically_in_bounds( func: &ir::Function, heap: &HeapData, index: ir::Value, offset_and_size: u64, ) -> bool658 fn statically_in_bounds(
659     func: &ir::Function,
660     heap: &HeapData,
661     index: ir::Value,
662     offset_and_size: u64,
663 ) -> bool {
664     func.dfg
665         .value_def(index)
666         .inst()
667         .and_then(|i| {
668             let imm = match func.dfg.insts[i] {
669                 ir::InstructionData::UnaryImm {
670                     opcode: ir::Opcode::Iconst,
671                     imm,
672                 } => imm,
673                 _ => return None,
674             };
675             let ty = func.dfg.value_type(index);
676             let index = imm.zero_extend_from_width(ty.bits()).bits().cast_unsigned();
677             let final_addr = index.checked_add(offset_and_size)?;
678             Some(final_addr <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX))
679         })
680         .unwrap_or(false)
681 }
682