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