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}