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        // With static memories we know the size of the heap bound at compile
206        // time.
207        //
208        // 1. First special case: trap immediately if `offset + access_size >
209        //    bound`, since we will end up being out-of-bounds regardless of the
210        //    given `index`.
211        HeapStyle::Static { bound } if offset_and_size > bound => {
212            assert!(
213                can_use_virtual_memory,
214                "static memories require the ability to use virtual memory"
215            );
216            builder.ins().trap(ir::TrapCode::HEAP_OUT_OF_BOUNDS);
217            Unreachable
218        }
219
220        // 2. Second special case for when we can completely omit explicit
221        //    bounds checks for 32-bit static memories.
222        //
223        //    First, let's rewrite our comparison to move all of the constants
224        //    to one side:
225        //
226        //            index + offset + access_size > bound
227        //        ==> index > bound - (offset + access_size)
228        //
229        //    We know the subtraction on the right-hand side won't wrap because
230        //    we didn't hit the first special case.
231        //
232        //    Additionally, we add our guard pages (if any) to the right-hand
233        //    side, since we can rely on the virtual memory subsystem at runtime
234        //    to catch out-of-bound accesses within the range `bound .. bound +
235        //    guard_size`. So now we are dealing with
236        //
237        //        index > bound + guard_size - (offset + access_size)
238        //
239        //    Note that `bound + guard_size` cannot overflow for
240        //    correctly-configured heaps, as otherwise the heap wouldn't fit in
241        //    a 64-bit memory space.
242        //
243        //    The complement of our should-this-trap comparison expression is
244        //    the should-this-not-trap comparison expression:
245        //
246        //        index <= bound + guard_size - (offset + access_size)
247        //
248        //    If we know the right-hand side is greater than or equal to
249        //    `u32::MAX`, then
250        //
251        //        index <= u32::MAX <= bound + guard_size - (offset + access_size)
252        //
253        //    This expression is always true when the heap is indexed with
254        //    32-bit integers because `index` cannot be larger than
255        //    `u32::MAX`. This means that `index` is always either in bounds or
256        //    within the guard page region, neither of which require emitting an
257        //    explicit bounds check.
258        HeapStyle::Static { bound }
259            if can_use_virtual_memory
260                && heap.index_type == ir::types::I32
261                && u64::from(u32::MAX) <= bound + heap.offset_guard_size - offset_and_size =>
262        {
263            assert!(
264                can_use_virtual_memory,
265                "static memories require the ability to use virtual memory"
266            );
267            Reachable(compute_addr(
268                &mut builder.cursor(),
269                heap,
270                env.pointer_type(),
271                index,
272                offset,
273            ))
274        }
275
276        // 3. General case for static memories.
277        //
278        //    We have to explicitly test whether
279        //
280        //        index > bound - (offset + access_size)
281        //
282        //    and trap if so.
283        //
284        //    Since we have to emit explicit bounds checks, we might as well be
285        //    precise, not rely on the virtual memory subsystem at all, and not
286        //    factor in the guard pages here.
287        HeapStyle::Static { bound } => {
288            assert!(
289                can_use_virtual_memory,
290                "static memories require the ability to use virtual memory"
291            );
292            // NB: this subtraction cannot wrap because we didn't hit the first
293            // special case.
294            let adjusted_bound = bound - offset_and_size;
295            let adjusted_bound_value = builder
296                .ins()
297                .iconst(env.pointer_type(), adjusted_bound as i64);
298            let oob = make_compare(
299                builder,
300                IntCC::UnsignedGreaterThan,
301                index,
302                adjusted_bound_value,
303            );
304            Reachable(explicit_check_oob_condition_and_compute_addr(
305                &mut builder.cursor(),
306                heap,
307                env.pointer_type(),
308                index,
309                offset,
310                spectre_mitigations_enabled,
311                oob,
312            ))
313        }
314    })
315}
316
317/// Get the bound of a dynamic heap as an `ir::Value`.
318fn get_dynamic_heap_bound(
319    builder: &mut FunctionBuilder,
320    env: &mut FuncEnvironment<'_>,
321    heap: &HeapData,
322) -> ir::Value {
323    match (heap.max_size, &heap.style) {
324        // The heap has a constant size, no need to actually load the bound.
325        (Some(max_size), HeapStyle::Dynamic { .. }) if heap.min_size == max_size => {
326            builder.ins().iconst(env.pointer_type(), max_size as i64)
327        }
328        // Load the heap bound from its global variable.
329        (_, HeapStyle::Dynamic { bound_gv }) => {
330            materialize_global_value(&mut builder.cursor(), env.pointer_type(), *bound_gv)
331        }
332        (_, HeapStyle::Static { .. }) => unreachable!("not a dynamic heap"),
333    }
334}
335
336fn cast_index_to_pointer_ty(
337    index: ir::Value,
338    index_ty: ir::Type,
339    pointer_ty: ir::Type,
340    pos: &mut FuncCursor,
341) -> ir::Value {
342    if index_ty == pointer_ty {
343        return index;
344    }
345    // Note that using 64-bit heaps on a 32-bit host is not currently supported,
346    // would require at least a bounds check here to ensure that the truncation
347    // from 64-to-32 bits doesn't lose any upper bits. For now though we're
348    // mostly interested in the 32-bit-heaps-on-64-bit-hosts cast.
349    assert!(index_ty.bits() < pointer_ty.bits());
350
351    // Convert `index` to `addr_ty`.
352    let extended_index = pos.ins().uextend(pointer_ty, index);
353
354    // Add debug value-label alias so that debuginfo can name the extended
355    // value as the address
356    let loc = pos.srcloc();
357    let loc = RelSourceLoc::from_base_offset(pos.func.params.base_srcloc(), loc);
358    pos.func
359        .stencil
360        .dfg
361        .add_value_label_alias(extended_index, loc, index);
362
363    extended_index
364}
365
366/// Emit explicit checks on the given out-of-bounds condition for the Wasm
367/// address and return the native address.
368///
369/// This function deduplicates explicit bounds checks and Spectre mitigations
370/// that inherently also implement bounds checking.
371#[allow(clippy::too_many_arguments)]
372fn explicit_check_oob_condition_and_compute_addr(
373    pos: &mut FuncCursor,
374    heap: &HeapData,
375    addr_ty: ir::Type,
376    index: ir::Value,
377    offset: u32,
378    // Whether Spectre mitigations are enabled for heap accesses.
379    spectre_mitigations_enabled: bool,
380    // The `i8` boolean value that is non-zero when the heap access is out of
381    // bounds (and therefore we should trap) and is zero when the heap access is
382    // in bounds (and therefore we can proceed).
383    oob_condition: ir::Value,
384) -> ir::Value {
385    if !spectre_mitigations_enabled {
386        pos.ins()
387            .trapnz(oob_condition, ir::TrapCode::HEAP_OUT_OF_BOUNDS);
388    }
389
390    let mut addr = compute_addr(pos, heap, addr_ty, index, offset);
391
392    if spectre_mitigations_enabled {
393        let null = pos.ins().iconst(addr_ty, 0);
394        addr = pos.ins().select_spectre_guard(oob_condition, null, addr);
395    }
396
397    addr
398}
399
400/// Emit code for the native address computation of a Wasm address,
401/// without any bounds checks or overflow checks.
402///
403/// It is the caller's responsibility to ensure that any necessary bounds and
404/// overflow checks are emitted, and that the resulting address is never used
405/// unless they succeed.
406fn compute_addr(
407    pos: &mut FuncCursor,
408    heap: &HeapData,
409    addr_ty: ir::Type,
410    index: ir::Value,
411    offset: u32,
412) -> ir::Value {
413    debug_assert_eq!(pos.func.dfg.value_type(index), addr_ty);
414
415    let heap_base = materialize_global_value(pos, addr_ty, heap.base);
416
417    let base_and_index = pos.ins().iadd(heap_base, index);
418
419    if offset == 0 {
420        base_and_index
421    } else {
422        // NB: The addition of the offset immediate must happen *before* the
423        // `select_spectre_guard`, if any. If it happens after, then we
424        // potentially are letting speculative execution read the whole first
425        // 4GiB of memory.
426        let offset_val = pos.ins().iconst(addr_ty, i64::from(offset));
427
428        pos.ins().iadd(base_and_index, offset_val)
429    }
430}
431
432#[inline]
433fn offset_plus_size(offset: u32, size: u8) -> u64 {
434    // Cannot overflow because we are widening to `u64`.
435    offset as u64 + size as u64
436}