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