Skip to main content

wasmer_compiler_cranelift/translator/code_translator/
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 super::Reachability;
23use crate::{
24    func_environ::FuncEnvironment,
25    heap::{HeapData, HeapStyle},
26    translator::materialize_global_value,
27};
28use Reachability::*;
29use cranelift_codegen::{
30    cursor::{Cursor, FuncCursor},
31    ir::{self, InstBuilder, RelSourceLoc, condcodes::IntCC},
32};
33use cranelift_frontend::FunctionBuilder;
34use wasmer_types::WasmResult;
35
36/// Helper used to emit bounds checks (as necessary) and compute the native
37/// address of a heap access.
38///
39/// Returns the `ir::Value` holding the native address of the heap access, or
40/// `None` if the heap access will unconditionally trap.
41pub fn bounds_check_and_compute_addr(
42    builder: &mut FunctionBuilder,
43    env: &mut FuncEnvironment<'_>,
44    heap: &HeapData,
45    // Dynamic operand indexing into the heap.
46    index: ir::Value,
47    // Static immediate added to the index.
48    offset: u32,
49    // Static size of the heap access.
50    access_size: u8,
51) -> WasmResult<Reachability<ir::Value>> {
52    let index = cast_index_to_pointer_ty(
53        index,
54        heap.index_type,
55        env.pointer_type(),
56        &mut builder.cursor(),
57    );
58    let offset_and_size = offset_plus_size(offset, access_size);
59    let spectre_mitigations_enabled = env.heap_access_spectre_mitigation();
60
61    let host_page_size_log2 = env.target_config().page_size_align_log2;
62    let can_use_virtual_memory = heap.page_size_log2 >= host_page_size_log2;
63
64    let make_compare =
65        |builder: &mut FunctionBuilder, compare_kind: IntCC, lhs: ir::Value, rhs: ir::Value| {
66            builder.ins().icmp(compare_kind, lhs, rhs)
67        };
68
69    // We need to emit code that will trap (or compute an address that will trap
70    // when accessed) if
71    //
72    //     index + offset + access_size > bound
73    //
74    // or if the `index + offset + access_size` addition overflows.
75    //
76    // Note that we ultimately want a 64-bit integer (we only target 64-bit
77    // architectures at the moment) and that `offset` is a `u32` and
78    // `access_size` is a `u8`. This means that we can add the latter together
79    // as `u64`s without fear of overflow, and we only have to be concerned with
80    // whether adding in `index` will overflow.
81    //
82    // Finally, the following right-hand sides of the matches do have a little
83    // bit of duplicated code across them, but I think writing it this way is
84    // worth it for readability and seeing very clearly each of our cases for
85    // different bounds checks and optimizations of those bounds checks. It is
86    // intentionally written in a straightforward case-matching style that will
87    // hopefully make it easy to port to ISLE one day.
88    Ok(match heap.style {
89        // ====== Dynamic Memories ======
90        //
91        // 1. First special case for when `offset + access_size == 1`:
92        //
93        //            index + 1 > bound
94        //        ==> index >= bound
95        HeapStyle::Dynamic { .. } if offset_and_size == 1 => {
96            let bound = get_dynamic_heap_bound(builder, env, heap);
97            let oob = make_compare(builder, IntCC::UnsignedGreaterThanOrEqual, index, bound);
98            Reachable(explicit_check_oob_condition_and_compute_addr(
99                &mut builder.cursor(),
100                heap,
101                env.pointer_type(),
102                index,
103                offset,
104                spectre_mitigations_enabled,
105                oob,
106            ))
107        }
108
109        // 2. Second special case for when we know that there are enough guard
110        //    pages to cover the offset and access size.
111        //
112        //    The precise should-we-trap condition is
113        //
114        //        index + offset + access_size > bound
115        //
116        //    However, if we instead check only the partial condition
117        //
118        //        index > bound
119        //
120        //    then the most out of bounds that the access can be, while that
121        //    partial check still succeeds, is `offset + access_size`.
122        //
123        //    However, when we have a guard region that is at least as large as
124        //    `offset + access_size`, we can rely on the virtual memory
125        //    subsystem handling these out-of-bounds errors at
126        //    runtime. Therefore, the partial `index > bound` check is
127        //    sufficient for this heap configuration.
128        //
129        //    Additionally, this has the advantage that a series of Wasm loads
130        //    that use the same dynamic index operand but different static
131        //    offset immediates -- which is a common code pattern when accessing
132        //    multiple fields in the same struct that is in linear memory --
133        //    will all emit the same `index > bound` check, which we can GVN.
134        HeapStyle::Dynamic { .. }
135            if can_use_virtual_memory && offset_and_size <= heap.offset_guard_size =>
136        {
137            let bound = get_dynamic_heap_bound(builder, env, heap);
138            let oob = make_compare(builder, IntCC::UnsignedGreaterThan, index, bound);
139            Reachable(explicit_check_oob_condition_and_compute_addr(
140                &mut builder.cursor(),
141                heap,
142                env.pointer_type(),
143                index,
144                offset,
145                spectre_mitigations_enabled,
146                oob,
147            ))
148        }
149
150        // 3. Third special case for when `offset + access_size <= min_size`.
151        //
152        //    We know that `bound >= min_size`, so we can do the following
153        //    comparison, without fear of the right-hand side wrapping around:
154        //
155        //            index + offset + access_size > bound
156        //        ==> index > bound - (offset + access_size)
157        HeapStyle::Dynamic { .. } if offset_and_size <= heap.min_size => {
158            let bound = get_dynamic_heap_bound(builder, env, heap);
159            let adjustment = offset_and_size as i64;
160            let adjustment_value = builder.ins().iconst(env.pointer_type(), adjustment);
161            let adjusted_bound = builder.ins().isub(bound, adjustment_value);
162            let oob = make_compare(builder, IntCC::UnsignedGreaterThan, index, adjusted_bound);
163            Reachable(explicit_check_oob_condition_and_compute_addr(
164                &mut builder.cursor(),
165                heap,
166                env.pointer_type(),
167                index,
168                offset,
169                spectre_mitigations_enabled,
170                oob,
171            ))
172        }
173
174        // 4. General case for dynamic memories:
175        //
176        //        index + offset + access_size > bound
177        //
178        //    And we have to handle the overflow case in the left-hand side.
179        HeapStyle::Dynamic { .. } => {
180            let access_size_val = builder
181                .ins()
182                // Explicit cast from u64 to i64: we just want the raw
183                // bits, and iconst takes an `Imm64`.
184                .iconst(env.pointer_type(), offset_and_size as i64);
185            let adjusted_index = builder.ins().uadd_overflow_trap(
186                index,
187                access_size_val,
188                ir::TrapCode::HEAP_OUT_OF_BOUNDS,
189            );
190            let bound = get_dynamic_heap_bound(builder, env, heap);
191            let oob = make_compare(builder, IntCC::UnsignedGreaterThan, adjusted_index, bound);
192            Reachable(explicit_check_oob_condition_and_compute_addr(
193                &mut builder.cursor(),
194                heap,
195                env.pointer_type(),
196                index,
197                offset,
198                spectre_mitigations_enabled,
199                oob,
200            ))
201        }
202
203        // ====== Static Memories ======
204        //
205        // Static memories reserve the full wasm32 address space plus the offset
206        // guard up front: omit explicit bounds checks and rely on virtual memory
207        // protection to trap out-of-bounds accesses.
208        HeapStyle::Static => {
209            assert!(
210                can_use_virtual_memory,
211                "static memories require the ability to use virtual memory"
212            );
213            Reachable(compute_addr(
214                &mut builder.cursor(),
215                heap,
216                env.pointer_type(),
217                index,
218                offset,
219            ))
220        }
221    })
222}
223
224/// Get the bound of a dynamic heap as an `ir::Value`.
225fn get_dynamic_heap_bound(
226    builder: &mut FunctionBuilder,
227    env: &mut FuncEnvironment<'_>,
228    heap: &HeapData,
229) -> ir::Value {
230    match (heap.max_size, &heap.style) {
231        // The heap has a constant size, no need to actually load the bound.
232        (Some(max_size), HeapStyle::Dynamic { .. }) if heap.min_size == max_size => {
233            builder.ins().iconst(env.pointer_type(), max_size as i64)
234        }
235        // Load the heap bound from its global variable.
236        (_, HeapStyle::Dynamic { bound_gv }) => {
237            materialize_global_value(&mut builder.cursor(), env.pointer_type(), *bound_gv)
238        }
239        (_, HeapStyle::Static) => unreachable!("not a dynamic heap"),
240    }
241}
242
243fn cast_index_to_pointer_ty(
244    index: ir::Value,
245    index_ty: ir::Type,
246    pointer_ty: ir::Type,
247    pos: &mut FuncCursor,
248) -> ir::Value {
249    if index_ty == pointer_ty {
250        return index;
251    }
252    // Note that using 64-bit heaps on a 32-bit host is not currently supported,
253    // would require at least a bounds check here to ensure that the truncation
254    // from 64-to-32 bits doesn't lose any upper bits. For now though we're
255    // mostly interested in the 32-bit-heaps-on-64-bit-hosts cast.
256    assert!(index_ty.bits() < pointer_ty.bits());
257
258    // Convert `index` to `addr_ty`.
259    let extended_index = pos.ins().uextend(pointer_ty, index);
260
261    // Add debug value-label alias so that debuginfo can name the extended
262    // value as the address
263    let loc = pos.srcloc();
264    let loc = RelSourceLoc::from_base_offset(pos.func.params.base_srcloc(), loc);
265    pos.func
266        .stencil
267        .dfg
268        .add_value_label_alias(extended_index, loc, index);
269
270    extended_index
271}
272
273/// Emit explicit checks on the given out-of-bounds condition for the Wasm
274/// address and return the native address.
275///
276/// This function deduplicates explicit bounds checks and Spectre mitigations
277/// that inherently also implement bounds checking.
278#[allow(clippy::too_many_arguments)]
279fn explicit_check_oob_condition_and_compute_addr(
280    pos: &mut FuncCursor,
281    heap: &HeapData,
282    addr_ty: ir::Type,
283    index: ir::Value,
284    offset: u32,
285    // Whether Spectre mitigations are enabled for heap accesses.
286    spectre_mitigations_enabled: bool,
287    // The `i8` boolean value that is non-zero when the heap access is out of
288    // bounds (and therefore we should trap) and is zero when the heap access is
289    // in bounds (and therefore we can proceed).
290    oob_condition: ir::Value,
291) -> ir::Value {
292    if !spectre_mitigations_enabled {
293        pos.ins()
294            .trapnz(oob_condition, ir::TrapCode::HEAP_OUT_OF_BOUNDS);
295    }
296
297    let mut addr = compute_addr(pos, heap, addr_ty, index, offset);
298
299    if spectre_mitigations_enabled {
300        let null = pos.ins().iconst(addr_ty, 0);
301        addr = pos.ins().select_spectre_guard(oob_condition, null, addr);
302    }
303
304    addr
305}
306
307/// Emit code for the native address computation of a Wasm address,
308/// without any bounds checks or overflow checks.
309///
310/// It is the caller's responsibility to ensure that any necessary bounds and
311/// overflow checks are emitted, and that the resulting address is never used
312/// unless they succeed.
313fn compute_addr(
314    pos: &mut FuncCursor,
315    heap: &HeapData,
316    addr_ty: ir::Type,
317    index: ir::Value,
318    offset: u32,
319) -> ir::Value {
320    debug_assert_eq!(pos.func.dfg.value_type(index), addr_ty);
321
322    let heap_base = materialize_global_value(pos, addr_ty, heap.base);
323
324    let base_and_index = pos.ins().iadd(heap_base, index);
325
326    if offset == 0 {
327        base_and_index
328    } else {
329        // NB: The addition of the offset immediate must happen *before* the
330        // `select_spectre_guard`, if any. If it happens after, then we
331        // potentially are letting speculative execution read the whole first
332        // 4GiB of memory.
333        let offset_val = pos.ins().iconst(addr_ty, i64::from(offset));
334
335        pos.ins().iadd(base_and_index, offset_val)
336    }
337}
338
339#[inline]
340fn offset_plus_size(offset: u32, size: u8) -> u64 {
341    // Cannot overflow because we are widening to `u64`.
342    offset as u64 + size as u64
343}