wasmtime_internal_cranelift/
bounds_checks.rs

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
22use crate::{
23    Reachability,
24    func_environ::FuncEnvironment,
25    translate::{HeapData, TargetEnvironment},
26};
27use Reachability::*;
28use cranelift_codegen::{
29    cursor::{Cursor, FuncCursor},
30    ir::{self, InstBuilder, RelSourceLoc, condcodes::IntCC},
31    ir::{Expr, Fact},
32};
33use 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)]
42pub 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.
80pub 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                false,
162                &mut builder.cursor(),
163                trap,
164            );
165
166            let field_ptr = builder.ins().isub(ptr_just_after_obj, backwards_offset);
167            Reachable(field_ptr)
168        }
169    }
170}
171
172fn bounds_check_field_access(
173    builder: &mut FunctionBuilder,
174    env: &mut FuncEnvironment<'_>,
175    heap: &HeapData,
176    index: ir::Value,
177    offset: u32,
178    access_size: u8,
179    trap: ir::TrapCode,
180) -> Reachability<ir::Value> {
181    let pointer_bit_width = u16::try_from(env.pointer_type().bits()).unwrap();
182    let bound_gv = heap.bound;
183    let orig_index = index;
184    let clif_memory_traps_enabled = env.clif_memory_traps_enabled();
185    let spectre_mitigations_enabled =
186        env.heap_access_spectre_mitigation() && clif_memory_traps_enabled;
187    let pcc = env.proof_carrying_code();
188
189    let host_page_size_log2 = env.target_config().page_size_align_log2;
190    let can_use_virtual_memory = heap
191        .memory
192        .can_use_virtual_memory(env.tunables(), host_page_size_log2)
193        && clif_memory_traps_enabled;
194    let can_elide_bounds_check = heap
195        .memory
196        .can_elide_bounds_check(env.tunables(), host_page_size_log2)
197        && clif_memory_traps_enabled;
198    let memory_guard_size = env.tunables().memory_guard_size;
199    let memory_reservation = env.tunables().memory_reservation;
200
201    let offset_and_size = offset_plus_size(offset, access_size);
202    let statically_in_bounds = statically_in_bounds(&builder.func, heap, index, offset_and_size);
203
204    let index = cast_index_to_pointer_ty(
205        index,
206        heap.index_type(),
207        env.pointer_type(),
208        heap.pcc_memory_type.is_some(),
209        &mut builder.cursor(),
210        trap,
211    );
212
213    let oob_behavior = if spectre_mitigations_enabled {
214        OobBehavior::ConditionallyLoadFromZero {
215            select_spectre_guard: true,
216        }
217    } else if env.load_from_zero_allowed() {
218        OobBehavior::ConditionallyLoadFromZero {
219            select_spectre_guard: false,
220        }
221    } else {
222        OobBehavior::ExplicitTrap
223    };
224
225    let make_compare = |builder: &mut FunctionBuilder,
226                        compare_kind: IntCC,
227                        lhs: ir::Value,
228                        lhs_off: Option<i64>,
229                        rhs: ir::Value,
230                        rhs_off: Option<i64>| {
231        let result = builder.ins().icmp(compare_kind, lhs, rhs);
232        if pcc {
233            // Name the original value as a def of the SSA value;
234            // if the value was extended, name that as well with a
235            // dynamic range, overwriting the basic full-range
236            // fact that we previously put on the uextend.
237            builder.func.dfg.facts[orig_index] = Some(Fact::Def { value: orig_index });
238            if index != orig_index {
239                builder.func.dfg.facts[index] = Some(Fact::value(pointer_bit_width, orig_index));
240            }
241
242            // Create a fact on the LHS that is a "trivial symbolic
243            // fact": v1 has range v1+LHS_off..=v1+LHS_off
244            builder.func.dfg.facts[lhs] = Some(Fact::value_offset(
245                pointer_bit_width,
246                orig_index,
247                lhs_off.unwrap(),
248            ));
249            // If the RHS is a symbolic value (v1 or gv1), we can
250            // emit a Compare fact.
251            if let Some(rhs) = builder.func.dfg.facts[rhs]
252                .as_ref()
253                .and_then(|f| f.as_symbol())
254            {
255                builder.func.dfg.facts[result] = Some(Fact::Compare {
256                    kind: compare_kind,
257                    lhs: Expr::offset(&Expr::value(orig_index), lhs_off.unwrap()).unwrap(),
258                    rhs: Expr::offset(rhs, rhs_off.unwrap()).unwrap(),
259                });
260            }
261            // Likewise, if the RHS is a constant, we can emit a
262            // Compare fact.
263            if let Some(k) = builder.func.dfg.facts[rhs]
264                .as_ref()
265                .and_then(|f| f.as_const(pointer_bit_width))
266            {
267                builder.func.dfg.facts[result] = Some(Fact::Compare {
268                    kind: compare_kind,
269                    lhs: Expr::offset(&Expr::value(orig_index), lhs_off.unwrap()).unwrap(),
270                    rhs: Expr::constant((k as i64).checked_add(rhs_off.unwrap()).unwrap()),
271                });
272            }
273        }
274        result
275    };
276
277    // We need to emit code that will trap (or compute an address that will trap
278    // when accessed) if
279    //
280    //     index + offset + access_size > bound
281    //
282    // or if the `index + offset + access_size` addition overflows.
283    //
284    // Note that we ultimately want a 64-bit integer (we only target 64-bit
285    // architectures at the moment) and that `offset` is a `u32` and
286    // `access_size` is a `u8`. This means that we can add the latter together
287    // as `u64`s without fear of overflow, and we only have to be concerned with
288    // whether adding in `index` will overflow.
289    //
290    // Finally, the following if/else chains do have a little
291    // bit of duplicated code across them, but I think writing it this way is
292    // worth it for readability and seeing very clearly each of our cases for
293    // different bounds checks and optimizations of those bounds checks. It is
294    // intentionally written in a straightforward case-matching style that will
295    // hopefully make it easy to port to ISLE one day.
296    if offset_and_size > heap.memory.maximum_byte_size().unwrap_or(u64::MAX) {
297        // Special case: trap immediately if `offset + access_size >
298        // max_memory_size`, since we will end up being out-of-bounds regardless
299        // of the given `index`.
300        env.before_unconditionally_trapping_memory_access(builder);
301        env.trap(builder, trap);
302        return Unreachable;
303    }
304
305    // Special case: if this is a 32-bit platform and the `offset_and_size`
306    // overflows the 32-bit address space then there's no hope of this ever
307    // being in-bounds. We can't represent `offset_and_size` in CLIF as the
308    // native pointer type anyway, so this is an unconditional trap.
309    if pointer_bit_width < 64 && offset_and_size >= (1 << pointer_bit_width) {
310        env.before_unconditionally_trapping_memory_access(builder);
311        env.trap(builder, trap);
312        return Unreachable;
313    }
314
315    // Special case for when we can completely omit explicit
316    // bounds checks for 32-bit memories.
317    //
318    // First, let's rewrite our comparison to move all of the constants
319    // to one side:
320    //
321    //         index + offset + access_size > bound
322    //     ==> index > bound - (offset + access_size)
323    //
324    // We know the subtraction on the right-hand side won't wrap because
325    // we didn't hit the unconditional trap case above.
326    //
327    // Additionally, we add our guard pages (if any) to the right-hand
328    // side, since we can rely on the virtual memory subsystem at runtime
329    // to catch out-of-bound accesses within the range `bound .. bound +
330    // guard_size`. So now we are dealing with
331    //
332    //     index > bound + guard_size - (offset + access_size)
333    //
334    // Note that `bound + guard_size` cannot overflow for
335    // correctly-configured heaps, as otherwise the heap wouldn't fit in
336    // a 64-bit memory space.
337    //
338    // The complement of our should-this-trap comparison expression is
339    // the should-this-not-trap comparison expression:
340    //
341    //     index <= bound + guard_size - (offset + access_size)
342    //
343    // If we know the right-hand side is greater than or equal to
344    // `u32::MAX`, then
345    //
346    //     index <= u32::MAX <= bound + guard_size - (offset + access_size)
347    //
348    // This expression is always true when the heap is indexed with
349    // 32-bit integers because `index` cannot be larger than
350    // `u32::MAX`. This means that `index` is always either in bounds or
351    // within the guard page region, neither of which require emitting an
352    // explicit bounds check.
353    if can_elide_bounds_check
354        && u64::from(u32::MAX) <= memory_reservation + memory_guard_size - offset_and_size
355    {
356        assert!(heap.index_type() == ir::types::I32);
357        assert!(
358            can_use_virtual_memory,
359            "static memories require the ability to use virtual memory"
360        );
361        return Reachable(compute_addr(
362            &mut builder.cursor(),
363            heap,
364            env.pointer_type(),
365            index,
366            offset,
367            AddrPcc::static32(heap.pcc_memory_type, memory_reservation + memory_guard_size),
368        ));
369    }
370
371    // Special case when the `index` is a constant and statically known to be
372    // in-bounds on this memory, no bounds checks necessary.
373    if statically_in_bounds {
374        return Reachable(compute_addr(
375            &mut builder.cursor(),
376            heap,
377            env.pointer_type(),
378            index,
379            offset,
380            AddrPcc::static32(heap.pcc_memory_type, memory_reservation + memory_guard_size),
381        ));
382    }
383
384    // Special case for when we can rely on virtual memory, the minimum
385    // byte size of this memory fits within the memory reservation, and
386    // memory isn't allowed to move. In this situation we know that
387    // memory will statically not grow beyond `memory_reservation` so we
388    // and we know that memory from 0 to that limit is guaranteed to be
389    // valid or trap. Here we effectively assume that the dynamic size
390    // of linear memory is its maximal value, `memory_reservation`, and
391    // we can avoid loading the actual length of memory.
392    //
393    // We have to explicitly test whether
394    //
395    //     index > bound - (offset + access_size)
396    //
397    // and trap if so.
398    //
399    // Since we have to emit explicit bounds checks, we might as well be
400    // precise, not rely on the virtual memory subsystem at all, and not
401    // factor in the guard pages here.
402    if can_use_virtual_memory
403        && heap.memory.minimum_byte_size().unwrap_or(u64::MAX) <= memory_reservation
404        && !heap.memory.memory_may_move(env.tunables())
405    {
406        let adjusted_bound = memory_reservation.checked_sub(offset_and_size).unwrap();
407        let adjusted_bound_value = builder
408            .ins()
409            .iconst(env.pointer_type(), adjusted_bound as i64);
410        if pcc {
411            builder.func.dfg.facts[adjusted_bound_value] =
412                Some(Fact::constant(pointer_bit_width, adjusted_bound));
413        }
414        let oob = make_compare(
415            builder,
416            IntCC::UnsignedGreaterThan,
417            index,
418            Some(0),
419            adjusted_bound_value,
420            Some(0),
421        );
422        return Reachable(explicit_check_oob_condition_and_compute_addr(
423            env,
424            builder,
425            heap,
426            index,
427            offset,
428            access_size,
429            oob_behavior,
430            AddrPcc::static32(heap.pcc_memory_type, memory_reservation),
431            oob,
432            trap,
433        ));
434    }
435
436    // Special case for when `offset + access_size == 1`:
437    //
438    //         index + 1 > bound
439    //     ==> index >= bound
440    //
441    // Note that this special case is skipped for Pulley targets to assist with
442    // pattern-matching bounds checks into single instructions. Otherwise more
443    // patterns/instructions would have to be added to match this. In the end
444    // the goal is to emit one instruction anyway, so this optimization is
445    // largely only applicable for native platforms.
446    if offset_and_size == 1 && !env.is_pulley() {
447        let bound = get_dynamic_heap_bound(builder, env, heap);
448        let oob = make_compare(
449            builder,
450            IntCC::UnsignedGreaterThanOrEqual,
451            index,
452            Some(0),
453            bound,
454            Some(0),
455        );
456        return Reachable(explicit_check_oob_condition_and_compute_addr(
457            env,
458            builder,
459            heap,
460            index,
461            offset,
462            access_size,
463            oob_behavior,
464            AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
465            oob,
466            trap,
467        ));
468    }
469
470    // Special case for when we know that there are enough guard
471    // pages to cover the offset and access size.
472    //
473    // The precise should-we-trap condition is
474    //
475    //     index + offset + access_size > bound
476    //
477    // However, if we instead check only the partial condition
478    //
479    //     index > bound
480    //
481    // then the most out of bounds that the access can be, while that
482    // partial check still succeeds, is `offset + access_size`.
483    //
484    // However, when we have a guard region that is at least as large as
485    // `offset + access_size`, we can rely on the virtual memory
486    // subsystem handling these out-of-bounds errors at
487    // runtime. Therefore, the partial `index > bound` check is
488    // sufficient for this heap configuration.
489    //
490    // Additionally, this has the advantage that a series of Wasm loads
491    // that use the same dynamic index operand but different static
492    // offset immediates -- which is a common code pattern when accessing
493    // multiple fields in the same struct that is in linear memory --
494    // will all emit the same `index > bound` check, which we can GVN.
495    if can_use_virtual_memory && offset_and_size <= memory_guard_size {
496        let bound = get_dynamic_heap_bound(builder, env, heap);
497        let oob = make_compare(
498            builder,
499            IntCC::UnsignedGreaterThan,
500            index,
501            Some(0),
502            bound,
503            Some(0),
504        );
505        return Reachable(explicit_check_oob_condition_and_compute_addr(
506            env,
507            builder,
508            heap,
509            index,
510            offset,
511            access_size,
512            oob_behavior,
513            AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
514            oob,
515            trap,
516        ));
517    }
518
519    // Special case for when `offset + access_size <= min_size`.
520    //
521    // We know that `bound >= min_size`, so we can do the following
522    // comparison, without fear of the right-hand side wrapping around:
523    //
524    //         index + offset + access_size > bound
525    //     ==> index > bound - (offset + access_size)
526    if offset_and_size <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX) {
527        let bound = get_dynamic_heap_bound(builder, env, heap);
528        let adjustment = offset_and_size as i64;
529        let adjustment_value = builder.ins().iconst(env.pointer_type(), adjustment);
530        if pcc {
531            builder.func.dfg.facts[adjustment_value] =
532                Some(Fact::constant(pointer_bit_width, offset_and_size));
533        }
534        let adjusted_bound = builder.ins().isub(bound, adjustment_value);
535        if pcc {
536            builder.func.dfg.facts[adjusted_bound] = Some(Fact::global_value_offset(
537                pointer_bit_width,
538                bound_gv,
539                -adjustment,
540            ));
541        }
542        let oob = make_compare(
543            builder,
544            IntCC::UnsignedGreaterThan,
545            index,
546            Some(0),
547            adjusted_bound,
548            Some(adjustment),
549        );
550        return Reachable(explicit_check_oob_condition_and_compute_addr(
551            env,
552            builder,
553            heap,
554            index,
555            offset,
556            access_size,
557            oob_behavior,
558            AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
559            oob,
560            trap,
561        ));
562    }
563
564    // General case for dynamic bounds checks:
565    //
566    //     index + offset + access_size > bound
567    //
568    // And we have to handle the overflow case in the left-hand side.
569    let access_size_val = builder
570        .ins()
571        // Explicit cast from u64 to i64: we just want the raw
572        // bits, and iconst takes an `Imm64`.
573        .iconst(env.pointer_type(), offset_and_size as i64);
574    if pcc {
575        builder.func.dfg.facts[access_size_val] =
576            Some(Fact::constant(pointer_bit_width, offset_and_size));
577    }
578    let adjusted_index = env.uadd_overflow_trap(builder, index, access_size_val, trap);
579    if pcc {
580        builder.func.dfg.facts[adjusted_index] = Some(Fact::value_offset(
581            pointer_bit_width,
582            index,
583            i64::try_from(offset_and_size).unwrap(),
584        ));
585    }
586    let bound = get_dynamic_heap_bound(builder, env, heap);
587    let oob = make_compare(
588        builder,
589        IntCC::UnsignedGreaterThan,
590        adjusted_index,
591        i64::try_from(offset_and_size).ok(),
592        bound,
593        Some(0),
594    );
595    Reachable(explicit_check_oob_condition_and_compute_addr(
596        env,
597        builder,
598        heap,
599        index,
600        offset,
601        access_size,
602        oob_behavior,
603        AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
604        oob,
605        trap,
606    ))
607}
608
609/// Get the bound of a dynamic heap as an `ir::Value`.
610fn get_dynamic_heap_bound(
611    builder: &mut FunctionBuilder,
612    env: &mut FuncEnvironment<'_>,
613    heap: &HeapData,
614) -> ir::Value {
615    let enable_pcc = heap.pcc_memory_type.is_some();
616
617    let (value, gv) = match heap.memory.static_heap_size() {
618        // The heap has a constant size, no need to actually load the
619        // bound.  TODO: this is currently disabled for PCC because we
620        // can't easily prove that the GV load indeed results in a
621        // constant (that information is lost in the CLIF). We'll want
622        // to create an `iconst` GV expression kind to reify this fact
623        // in the GV, then re-enable this opt. (Or, alternately,
624        // compile such memories with a static-bound memtype and
625        // facts.)
626        Some(max_size) if !enable_pcc => (
627            builder.ins().iconst(env.pointer_type(), max_size as i64),
628            heap.bound,
629        ),
630
631        // Load the heap bound from its global variable.
632        _ => (
633            builder.ins().global_value(env.pointer_type(), heap.bound),
634            heap.bound,
635        ),
636    };
637
638    // If proof-carrying code is enabled, apply a fact to the range to
639    // tie it to the GV.
640    if enable_pcc {
641        builder.func.dfg.facts[value] = Some(Fact::global_value(
642            u16::try_from(env.pointer_type().bits()).unwrap(),
643            gv,
644        ));
645    }
646
647    value
648}
649
650fn cast_index_to_pointer_ty(
651    index: ir::Value,
652    index_ty: ir::Type,
653    pointer_ty: ir::Type,
654    pcc: bool,
655    pos: &mut FuncCursor,
656    trap: ir::TrapCode,
657) -> ir::Value {
658    if index_ty == pointer_ty {
659        return index;
660    }
661
662    // If the index size is larger than the pointer, that means that this is a
663    // 32-bit host platform with a 64-bit wasm linear memory. If the index is
664    // larger than 2**32 then that's guaranteed to be out-of-bounds, otherwise we
665    // `ireduce` the index.
666    //
667    // Also note that at this time this branch doesn't support pcc nor the
668    // value-label-ranges of the below path.
669    //
670    // Finally, note that the returned `low_bits` here are still subject to an
671    // explicit bounds check in wasm so in terms of Spectre speculation on
672    // either side of the `trapnz` should be ok.
673    if index_ty.bits() > pointer_ty.bits() {
674        assert_eq!(index_ty, ir::types::I64);
675        assert_eq!(pointer_ty, ir::types::I32);
676        let low_bits = pos.ins().ireduce(pointer_ty, index);
677        let c32 = pos.ins().iconst(pointer_ty, 32);
678        let high_bits = pos.ins().ushr(index, c32);
679        let high_bits = pos.ins().ireduce(pointer_ty, high_bits);
680        pos.ins().trapnz(high_bits, trap);
681        return low_bits;
682    }
683
684    // Convert `index` to `addr_ty`.
685    let extended_index = pos.ins().uextend(pointer_ty, index);
686
687    // Add a range fact on the extended value.
688    if pcc {
689        pos.func.dfg.facts[extended_index] = Some(Fact::max_range_for_width_extended(
690            u16::try_from(index_ty.bits()).unwrap(),
691            u16::try_from(pointer_ty.bits()).unwrap(),
692        ));
693    }
694
695    // Add debug value-label alias so that debuginfo can name the extended
696    // value as the address
697    let loc = pos.srcloc();
698    let loc = RelSourceLoc::from_base_offset(pos.func.params.base_srcloc(), loc);
699    pos.func
700        .stencil
701        .dfg
702        .add_value_label_alias(extended_index, loc, index);
703
704    extended_index
705}
706
707/// Which facts do we want to emit for proof-carrying code, if any, on
708/// address computations?
709#[derive(Clone, Copy, Debug)]
710enum AddrPcc {
711    /// A 32-bit static memory with the given size.
712    Static32(ir::MemoryType, u64),
713    /// Dynamic bounds-check, with actual memory size (the `GlobalValue`)
714    /// expressed symbolically.
715    Dynamic(ir::MemoryType, ir::GlobalValue),
716}
717impl AddrPcc {
718    fn static32(memory_type: Option<ir::MemoryType>, size: u64) -> Option<Self> {
719        memory_type.map(|ty| AddrPcc::Static32(ty, size))
720    }
721    fn dynamic(memory_type: Option<ir::MemoryType>, bound: ir::GlobalValue) -> Option<Self> {
722        memory_type.map(|ty| AddrPcc::Dynamic(ty, bound))
723    }
724}
725
726/// What to do on out-of-bounds for the
727/// `explicit_check_oob_condition_and_compute_addr` function below.
728enum OobBehavior {
729    /// An explicit `trapnz` instruction should be used.
730    ExplicitTrap,
731    /// A load from NULL should be issued if the address is out-of-bounds.
732    ConditionallyLoadFromZero {
733        /// Whether or not to use `select_spectre_guard` to choose the address
734        /// to load from. If `false` then a normal `select` is used.
735        select_spectre_guard: bool,
736    },
737}
738
739/// Emit explicit checks on the given out-of-bounds condition for the Wasm
740/// address and return the native address.
741///
742/// This function deduplicates explicit bounds checks and Spectre mitigations
743/// that inherently also implement bounds checking.
744fn explicit_check_oob_condition_and_compute_addr(
745    env: &mut FuncEnvironment<'_>,
746    builder: &mut FunctionBuilder,
747    heap: &HeapData,
748    index: ir::Value,
749    offset: u32,
750    access_size: u8,
751    oob_behavior: OobBehavior,
752    // Whether we're emitting PCC facts.
753    pcc: Option<AddrPcc>,
754    // The `i8` boolean value that is non-zero when the heap access is out of
755    // bounds (and therefore we should trap) and is zero when the heap access is
756    // in bounds (and therefore we can proceed).
757    oob_condition: ir::Value,
758    trap: ir::TrapCode,
759) -> ir::Value {
760    if let OobBehavior::ExplicitTrap = oob_behavior {
761        env.trapnz(builder, oob_condition, trap);
762    }
763    let addr_ty = env.pointer_type();
764
765    let mut addr = compute_addr(&mut builder.cursor(), heap, addr_ty, index, offset, pcc);
766
767    if let OobBehavior::ConditionallyLoadFromZero {
768        select_spectre_guard,
769    } = oob_behavior
770    {
771        // These mitigations rely on trapping when loading from NULL so
772        // CLIF memory instruction traps must be allowed for this to be
773        // generated.
774        assert!(env.load_from_zero_allowed());
775        let null = builder.ins().iconst(addr_ty, 0);
776        addr = if select_spectre_guard {
777            builder
778                .ins()
779                .select_spectre_guard(oob_condition, null, addr)
780        } else {
781            builder.ins().select(oob_condition, null, addr)
782        };
783
784        match pcc {
785            None => {}
786            Some(AddrPcc::Static32(ty, size)) => {
787                builder.func.dfg.facts[null] =
788                    Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
789                builder.func.dfg.facts[addr] = Some(Fact::Mem {
790                    ty,
791                    min_offset: 0,
792                    max_offset: size.checked_sub(u64::from(access_size)).unwrap(),
793                    nullable: true,
794                });
795            }
796            Some(AddrPcc::Dynamic(ty, gv)) => {
797                builder.func.dfg.facts[null] =
798                    Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
799                builder.func.dfg.facts[addr] = Some(Fact::DynamicMem {
800                    ty,
801                    min: Expr::constant(0),
802                    max: Expr::offset(
803                        &Expr::global_value(gv),
804                        i64::try_from(env.tunables().memory_guard_size)
805                            .unwrap()
806                            .checked_sub(i64::from(access_size))
807                            .unwrap(),
808                    )
809                    .unwrap(),
810                    nullable: true,
811                });
812            }
813        }
814    }
815
816    addr
817}
818
819/// Emit code for the native address computation of a Wasm address,
820/// without any bounds checks or overflow checks.
821///
822/// It is the caller's responsibility to ensure that any necessary bounds and
823/// overflow checks are emitted, and that the resulting address is never used
824/// unless they succeed.
825fn compute_addr(
826    pos: &mut FuncCursor,
827    heap: &HeapData,
828    addr_ty: ir::Type,
829    index: ir::Value,
830    offset: u32,
831    pcc: Option<AddrPcc>,
832) -> ir::Value {
833    debug_assert_eq!(pos.func.dfg.value_type(index), addr_ty);
834
835    let heap_base = pos.ins().global_value(addr_ty, heap.base);
836
837    match pcc {
838        None => {}
839        Some(AddrPcc::Static32(ty, _size)) => {
840            pos.func.dfg.facts[heap_base] = Some(Fact::Mem {
841                ty,
842                min_offset: 0,
843                max_offset: 0,
844                nullable: false,
845            });
846        }
847        Some(AddrPcc::Dynamic(ty, _limit)) => {
848            pos.func.dfg.facts[heap_base] = Some(Fact::dynamic_base_ptr(ty));
849        }
850    }
851
852    let base_and_index = pos.ins().iadd(heap_base, index);
853
854    match pcc {
855        None => {}
856        Some(AddrPcc::Static32(ty, _) | AddrPcc::Dynamic(ty, _)) => {
857            if let Some(idx) = pos.func.dfg.facts[index]
858                .as_ref()
859                .and_then(|f| f.as_symbol())
860                .cloned()
861            {
862                pos.func.dfg.facts[base_and_index] = Some(Fact::DynamicMem {
863                    ty,
864                    min: idx.clone(),
865                    max: idx,
866                    nullable: false,
867                });
868            } else {
869                pos.func.dfg.facts[base_and_index] = Some(Fact::Mem {
870                    ty,
871                    min_offset: 0,
872                    max_offset: u64::from(u32::MAX),
873                    nullable: false,
874                });
875            }
876        }
877    }
878
879    if offset == 0 {
880        base_and_index
881    } else {
882        // NB: The addition of the offset immediate must happen *before* the
883        // `select_spectre_guard`, if any. If it happens after, then we
884        // potentially are letting speculative execution read the whole first
885        // 4GiB of memory.
886        let offset_val = pos.ins().iconst(addr_ty, i64::from(offset));
887
888        if pcc.is_some() {
889            pos.func.dfg.facts[offset_val] = Some(Fact::constant(
890                u16::try_from(addr_ty.bits()).unwrap(),
891                u64::from(offset),
892            ));
893        }
894
895        let result = pos.ins().iadd(base_and_index, offset_val);
896
897        match pcc {
898            None => {}
899            Some(AddrPcc::Static32(ty, _) | AddrPcc::Dynamic(ty, _)) => {
900                if let Some(idx) = pos.func.dfg.facts[index]
901                    .as_ref()
902                    .and_then(|f| f.as_symbol())
903                {
904                    pos.func.dfg.facts[result] = Some(Fact::DynamicMem {
905                        ty,
906                        min: idx.clone(),
907                        // Safety: adding an offset to an expression with
908                        // zero offset -- add cannot wrap, so `unwrap()`
909                        // cannot fail.
910                        max: Expr::offset(idx, i64::from(offset)).unwrap(),
911                        nullable: false,
912                    });
913                } else {
914                    pos.func.dfg.facts[result] = Some(Fact::Mem {
915                        ty,
916                        min_offset: u64::from(offset),
917                        // Safety: can't overflow -- two u32s summed in a
918                        // 64-bit add. TODO: when memory64 is supported here,
919                        // `u32::MAX` is no longer true, and we'll need to
920                        // handle overflow here.
921                        max_offset: u64::from(u32::MAX) + u64::from(offset),
922                        nullable: false,
923                    });
924                }
925            }
926        }
927        result
928    }
929}
930
931#[inline]
932fn offset_plus_size(offset: u32, size: u8) -> u64 {
933    // Cannot overflow because we are widening to `u64`.
934    offset as u64 + size as u64
935}
936
937/// Returns whether `index` is statically in-bounds with respect to this
938/// `heap`'s configuration.
939///
940/// This is `true` when `index` is a constant and when the offset/size are added
941/// in it's all still less than the minimum byte size of the heap.
942///
943/// The `offset_and_size` here are the static offset that was listed on the wasm
944/// instruction plus the size of the access being made.
945fn statically_in_bounds(
946    func: &ir::Function,
947    heap: &HeapData,
948    index: ir::Value,
949    offset_and_size: u64,
950) -> bool {
951    func.dfg
952        .value_def(index)
953        .inst()
954        .and_then(|i| {
955            let imm = match func.dfg.insts[i] {
956                ir::InstructionData::UnaryImm {
957                    opcode: ir::Opcode::Iconst,
958                    imm,
959                } => imm,
960                _ => return None,
961            };
962            let ty = func.dfg.value_type(index);
963            let index = imm.zero_extend_from_width(ty.bits()).bits().cast_unsigned();
964            let final_addr = index.checked_add(offset_and_size)?;
965            Some(final_addr <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX))
966        })
967        .unwrap_or(false)
968}