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}