wasmtime_internal_cranelift/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 crate::{
23 Reachability,
24 func_environ::FuncEnvironment,
25 translate::{HeapData, TargetEnvironment},
26};
27use Reachability::*;
28use cranelift_codegen::{
29 cursor::{Cursor, FuncCursor},
30 ir::{self, InstBuilder, RelSourceLoc, condcodes::IntCC},
31 ir::{Expr, Fact},
32};
33use cranelift_frontend::FunctionBuilder;
34
35/// The kind of bounds check to perform when accessing a Wasm linear memory or
36/// GC heap.
37///
38/// Prefer `BoundsCheck::*WholeObject` over `BoundsCheck::Field` when possible,
39/// as that approach allows the mid-end to deduplicate bounds checks across
40/// multiple accesses to the same GC object.
41#[derive(Debug)]
42pub enum BoundsCheck {
43 /// Check that this one access in particular is in bounds:
44 ///
45 /// ```ignore
46 /// index + offset + access_size <= bound
47 /// ```
48 StaticOffset { offset: u32, access_size: u8 },
49
50 /// Assuming the precondition `offset + access_size <= object_size`, check
51 /// that this whole object is in bounds:
52 ///
53 /// ```ignore
54 /// index + object_size <= bound
55 /// ```
56 #[cfg(feature = "gc")]
57 StaticObjectField {
58 offset: u32,
59 access_size: u8,
60 object_size: u32,
61 },
62
63 /// Like `StaticWholeObject` but with dynamic offset and object size.
64 ///
65 /// It is *your* responsibility to ensure that the `offset + access_size <=
66 /// object_size` precondition holds.
67 #[cfg(feature = "gc")]
68 DynamicObjectField {
69 offset: ir::Value,
70 object_size: ir::Value,
71 },
72}
73
74/// Helper used to emit bounds checks (as necessary) and compute the native
75/// address of a heap access.
76///
77/// Returns the `ir::Value` holding the native address of the heap access, or
78/// `Reachability::Unreachable` if the heap access will unconditionally trap and
79/// any subsequent code in this basic block is unreachable.
80pub fn bounds_check_and_compute_addr(
81 builder: &mut FunctionBuilder,
82 env: &mut FuncEnvironment<'_>,
83 heap: &HeapData,
84 index: ir::Value,
85 bounds_check: BoundsCheck,
86 trap: ir::TrapCode,
87) -> Reachability<ir::Value> {
88 match bounds_check {
89 BoundsCheck::StaticOffset {
90 offset,
91 access_size,
92 } => bounds_check_field_access(builder, env, heap, index, offset, access_size, trap),
93
94 #[cfg(feature = "gc")]
95 BoundsCheck::StaticObjectField {
96 offset,
97 access_size,
98 object_size,
99 } => {
100 // Assert that the precondition holds.
101 let offset_and_access_size = offset.checked_add(access_size.into()).unwrap();
102 assert!(offset_and_access_size <= object_size);
103
104 // When we can, pretend that we are doing one big access of the
105 // whole object all at once. This enables better GVN for repeated
106 // accesses of the same object.
107 if let Ok(object_size) = u8::try_from(object_size) {
108 let obj_ptr = match bounds_check_field_access(
109 builder,
110 env,
111 heap,
112 index,
113 0,
114 object_size,
115 trap,
116 ) {
117 Reachable(v) => v,
118 u @ Unreachable => return u,
119 };
120 let offset = builder.ins().iconst(env.pointer_type(), i64::from(offset));
121 let field_ptr = builder.ins().iadd(obj_ptr, offset);
122 return Reachable(field_ptr);
123 }
124
125 // Otherwise, bounds check just this one field's access.
126 bounds_check_field_access(builder, env, heap, index, offset, access_size, trap)
127 }
128
129 // Compute the index of the end of the object, bounds check that and get
130 // a pointer to just after the object, and then reverse offset from that
131 // to get the pointer to the field being accessed.
132 #[cfg(feature = "gc")]
133 BoundsCheck::DynamicObjectField {
134 offset,
135 object_size,
136 } => {
137 assert_eq!(heap.index_type(), ir::types::I32);
138 assert_eq!(builder.func.dfg.value_type(index), ir::types::I32);
139 assert_eq!(builder.func.dfg.value_type(offset), ir::types::I32);
140 assert_eq!(builder.func.dfg.value_type(object_size), ir::types::I32);
141
142 let index_and_object_size = builder.ins().uadd_overflow_trap(index, object_size, trap);
143 let ptr_just_after_obj = match bounds_check_field_access(
144 builder,
145 env,
146 heap,
147 index_and_object_size,
148 0,
149 0,
150 trap,
151 ) {
152 Reachable(v) => v,
153 u @ Unreachable => return u,
154 };
155
156 let backwards_offset = builder.ins().isub(object_size, offset);
157 let backwards_offset = cast_index_to_pointer_ty(
158 backwards_offset,
159 ir::types::I32,
160 env.pointer_type(),
161 false,
162 &mut builder.cursor(),
163 trap,
164 );
165
166 let field_ptr = builder.ins().isub(ptr_just_after_obj, backwards_offset);
167 Reachable(field_ptr)
168 }
169 }
170}
171
172fn bounds_check_field_access(
173 builder: &mut FunctionBuilder,
174 env: &mut FuncEnvironment<'_>,
175 heap: &HeapData,
176 index: ir::Value,
177 offset: u32,
178 access_size: u8,
179 trap: ir::TrapCode,
180) -> Reachability<ir::Value> {
181 let pointer_bit_width = u16::try_from(env.pointer_type().bits()).unwrap();
182 let bound_gv = heap.bound;
183 let orig_index = index;
184 let clif_memory_traps_enabled = env.clif_memory_traps_enabled();
185 let spectre_mitigations_enabled =
186 env.heap_access_spectre_mitigation() && clif_memory_traps_enabled;
187 let pcc = env.proof_carrying_code();
188
189 let host_page_size_log2 = env.target_config().page_size_align_log2;
190 let can_use_virtual_memory = heap
191 .memory
192 .can_use_virtual_memory(env.tunables(), host_page_size_log2)
193 && clif_memory_traps_enabled;
194 let can_elide_bounds_check = heap
195 .memory
196 .can_elide_bounds_check(env.tunables(), host_page_size_log2)
197 && clif_memory_traps_enabled;
198 let memory_guard_size = env.tunables().memory_guard_size;
199 let memory_reservation = env.tunables().memory_reservation;
200
201 let offset_and_size = offset_plus_size(offset, access_size);
202 let statically_in_bounds = statically_in_bounds(&builder.func, heap, index, offset_and_size);
203
204 let index = cast_index_to_pointer_ty(
205 index,
206 heap.index_type(),
207 env.pointer_type(),
208 heap.pcc_memory_type.is_some(),
209 &mut builder.cursor(),
210 trap,
211 );
212
213 let oob_behavior = if spectre_mitigations_enabled {
214 OobBehavior::ConditionallyLoadFromZero {
215 select_spectre_guard: true,
216 }
217 } else if env.load_from_zero_allowed() {
218 OobBehavior::ConditionallyLoadFromZero {
219 select_spectre_guard: false,
220 }
221 } else {
222 OobBehavior::ExplicitTrap
223 };
224
225 let make_compare = |builder: &mut FunctionBuilder,
226 compare_kind: IntCC,
227 lhs: ir::Value,
228 lhs_off: Option<i64>,
229 rhs: ir::Value,
230 rhs_off: Option<i64>| {
231 let result = builder.ins().icmp(compare_kind, lhs, rhs);
232 if pcc {
233 // Name the original value as a def of the SSA value;
234 // if the value was extended, name that as well with a
235 // dynamic range, overwriting the basic full-range
236 // fact that we previously put on the uextend.
237 builder.func.dfg.facts[orig_index] = Some(Fact::Def { value: orig_index });
238 if index != orig_index {
239 builder.func.dfg.facts[index] = Some(Fact::value(pointer_bit_width, orig_index));
240 }
241
242 // Create a fact on the LHS that is a "trivial symbolic
243 // fact": v1 has range v1+LHS_off..=v1+LHS_off
244 builder.func.dfg.facts[lhs] = Some(Fact::value_offset(
245 pointer_bit_width,
246 orig_index,
247 lhs_off.unwrap(),
248 ));
249 // If the RHS is a symbolic value (v1 or gv1), we can
250 // emit a Compare fact.
251 if let Some(rhs) = builder.func.dfg.facts[rhs]
252 .as_ref()
253 .and_then(|f| f.as_symbol())
254 {
255 builder.func.dfg.facts[result] = Some(Fact::Compare {
256 kind: compare_kind,
257 lhs: Expr::offset(&Expr::value(orig_index), lhs_off.unwrap()).unwrap(),
258 rhs: Expr::offset(rhs, rhs_off.unwrap()).unwrap(),
259 });
260 }
261 // Likewise, if the RHS is a constant, we can emit a
262 // Compare fact.
263 if let Some(k) = builder.func.dfg.facts[rhs]
264 .as_ref()
265 .and_then(|f| f.as_const(pointer_bit_width))
266 {
267 builder.func.dfg.facts[result] = Some(Fact::Compare {
268 kind: compare_kind,
269 lhs: Expr::offset(&Expr::value(orig_index), lhs_off.unwrap()).unwrap(),
270 rhs: Expr::constant((k as i64).checked_add(rhs_off.unwrap()).unwrap()),
271 });
272 }
273 }
274 result
275 };
276
277 // We need to emit code that will trap (or compute an address that will trap
278 // when accessed) if
279 //
280 // index + offset + access_size > bound
281 //
282 // or if the `index + offset + access_size` addition overflows.
283 //
284 // Note that we ultimately want a 64-bit integer (we only target 64-bit
285 // architectures at the moment) and that `offset` is a `u32` and
286 // `access_size` is a `u8`. This means that we can add the latter together
287 // as `u64`s without fear of overflow, and we only have to be concerned with
288 // whether adding in `index` will overflow.
289 //
290 // Finally, the following if/else chains do have a little
291 // bit of duplicated code across them, but I think writing it this way is
292 // worth it for readability and seeing very clearly each of our cases for
293 // different bounds checks and optimizations of those bounds checks. It is
294 // intentionally written in a straightforward case-matching style that will
295 // hopefully make it easy to port to ISLE one day.
296 if offset_and_size > heap.memory.maximum_byte_size().unwrap_or(u64::MAX) {
297 // Special case: trap immediately if `offset + access_size >
298 // max_memory_size`, since we will end up being out-of-bounds regardless
299 // of the given `index`.
300 env.before_unconditionally_trapping_memory_access(builder);
301 env.trap(builder, trap);
302 return Unreachable;
303 }
304
305 // Special case: if this is a 32-bit platform and the `offset_and_size`
306 // overflows the 32-bit address space then there's no hope of this ever
307 // being in-bounds. We can't represent `offset_and_size` in CLIF as the
308 // native pointer type anyway, so this is an unconditional trap.
309 if pointer_bit_width < 64 && offset_and_size >= (1 << pointer_bit_width) {
310 env.before_unconditionally_trapping_memory_access(builder);
311 env.trap(builder, trap);
312 return Unreachable;
313 }
314
315 // Special case for when we can completely omit explicit
316 // bounds checks for 32-bit memories.
317 //
318 // First, let's rewrite our comparison to move all of the constants
319 // to one side:
320 //
321 // index + offset + access_size > bound
322 // ==> index > bound - (offset + access_size)
323 //
324 // We know the subtraction on the right-hand side won't wrap because
325 // we didn't hit the unconditional trap case above.
326 //
327 // Additionally, we add our guard pages (if any) to the right-hand
328 // side, since we can rely on the virtual memory subsystem at runtime
329 // to catch out-of-bound accesses within the range `bound .. bound +
330 // guard_size`. So now we are dealing with
331 //
332 // index > bound + guard_size - (offset + access_size)
333 //
334 // Note that `bound + guard_size` cannot overflow for
335 // correctly-configured heaps, as otherwise the heap wouldn't fit in
336 // a 64-bit memory space.
337 //
338 // The complement of our should-this-trap comparison expression is
339 // the should-this-not-trap comparison expression:
340 //
341 // index <= bound + guard_size - (offset + access_size)
342 //
343 // If we know the right-hand side is greater than or equal to
344 // `u32::MAX`, then
345 //
346 // index <= u32::MAX <= bound + guard_size - (offset + access_size)
347 //
348 // This expression is always true when the heap is indexed with
349 // 32-bit integers because `index` cannot be larger than
350 // `u32::MAX`. This means that `index` is always either in bounds or
351 // within the guard page region, neither of which require emitting an
352 // explicit bounds check.
353 if can_elide_bounds_check
354 && u64::from(u32::MAX) <= memory_reservation + memory_guard_size - offset_and_size
355 {
356 assert!(heap.index_type() == ir::types::I32);
357 assert!(
358 can_use_virtual_memory,
359 "static memories require the ability to use virtual memory"
360 );
361 return Reachable(compute_addr(
362 &mut builder.cursor(),
363 heap,
364 env.pointer_type(),
365 index,
366 offset,
367 AddrPcc::static32(heap.pcc_memory_type, memory_reservation + memory_guard_size),
368 ));
369 }
370
371 // Special case when the `index` is a constant and statically known to be
372 // in-bounds on this memory, no bounds checks necessary.
373 if statically_in_bounds {
374 return Reachable(compute_addr(
375 &mut builder.cursor(),
376 heap,
377 env.pointer_type(),
378 index,
379 offset,
380 AddrPcc::static32(heap.pcc_memory_type, memory_reservation + memory_guard_size),
381 ));
382 }
383
384 // Special case for when we can rely on virtual memory, the minimum
385 // byte size of this memory fits within the memory reservation, and
386 // memory isn't allowed to move. In this situation we know that
387 // memory will statically not grow beyond `memory_reservation` so we
388 // and we know that memory from 0 to that limit is guaranteed to be
389 // valid or trap. Here we effectively assume that the dynamic size
390 // of linear memory is its maximal value, `memory_reservation`, and
391 // we can avoid loading the actual length of memory.
392 //
393 // We have to explicitly test whether
394 //
395 // index > bound - (offset + access_size)
396 //
397 // and trap if so.
398 //
399 // Since we have to emit explicit bounds checks, we might as well be
400 // precise, not rely on the virtual memory subsystem at all, and not
401 // factor in the guard pages here.
402 if can_use_virtual_memory
403 && heap.memory.minimum_byte_size().unwrap_or(u64::MAX) <= memory_reservation
404 && !heap.memory.memory_may_move(env.tunables())
405 {
406 let adjusted_bound = memory_reservation.checked_sub(offset_and_size).unwrap();
407 let adjusted_bound_value = builder
408 .ins()
409 .iconst(env.pointer_type(), adjusted_bound as i64);
410 if pcc {
411 builder.func.dfg.facts[adjusted_bound_value] =
412 Some(Fact::constant(pointer_bit_width, adjusted_bound));
413 }
414 let oob = make_compare(
415 builder,
416 IntCC::UnsignedGreaterThan,
417 index,
418 Some(0),
419 adjusted_bound_value,
420 Some(0),
421 );
422 return Reachable(explicit_check_oob_condition_and_compute_addr(
423 env,
424 builder,
425 heap,
426 index,
427 offset,
428 access_size,
429 oob_behavior,
430 AddrPcc::static32(heap.pcc_memory_type, memory_reservation),
431 oob,
432 trap,
433 ));
434 }
435
436 // Special case for when `offset + access_size == 1`:
437 //
438 // index + 1 > bound
439 // ==> index >= bound
440 //
441 // Note that this special case is skipped for Pulley targets to assist with
442 // pattern-matching bounds checks into single instructions. Otherwise more
443 // patterns/instructions would have to be added to match this. In the end
444 // the goal is to emit one instruction anyway, so this optimization is
445 // largely only applicable for native platforms.
446 if offset_and_size == 1 && !env.is_pulley() {
447 let bound = get_dynamic_heap_bound(builder, env, heap);
448 let oob = make_compare(
449 builder,
450 IntCC::UnsignedGreaterThanOrEqual,
451 index,
452 Some(0),
453 bound,
454 Some(0),
455 );
456 return Reachable(explicit_check_oob_condition_and_compute_addr(
457 env,
458 builder,
459 heap,
460 index,
461 offset,
462 access_size,
463 oob_behavior,
464 AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
465 oob,
466 trap,
467 ));
468 }
469
470 // Special case for when we know that there are enough guard
471 // pages to cover the offset and access size.
472 //
473 // The precise should-we-trap condition is
474 //
475 // index + offset + access_size > bound
476 //
477 // However, if we instead check only the partial condition
478 //
479 // index > bound
480 //
481 // then the most out of bounds that the access can be, while that
482 // partial check still succeeds, is `offset + access_size`.
483 //
484 // However, when we have a guard region that is at least as large as
485 // `offset + access_size`, we can rely on the virtual memory
486 // subsystem handling these out-of-bounds errors at
487 // runtime. Therefore, the partial `index > bound` check is
488 // sufficient for this heap configuration.
489 //
490 // Additionally, this has the advantage that a series of Wasm loads
491 // that use the same dynamic index operand but different static
492 // offset immediates -- which is a common code pattern when accessing
493 // multiple fields in the same struct that is in linear memory --
494 // will all emit the same `index > bound` check, which we can GVN.
495 if can_use_virtual_memory && offset_and_size <= memory_guard_size {
496 let bound = get_dynamic_heap_bound(builder, env, heap);
497 let oob = make_compare(
498 builder,
499 IntCC::UnsignedGreaterThan,
500 index,
501 Some(0),
502 bound,
503 Some(0),
504 );
505 return Reachable(explicit_check_oob_condition_and_compute_addr(
506 env,
507 builder,
508 heap,
509 index,
510 offset,
511 access_size,
512 oob_behavior,
513 AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
514 oob,
515 trap,
516 ));
517 }
518
519 // Special case for when `offset + access_size <= min_size`.
520 //
521 // We know that `bound >= min_size`, so we can do the following
522 // comparison, without fear of the right-hand side wrapping around:
523 //
524 // index + offset + access_size > bound
525 // ==> index > bound - (offset + access_size)
526 if offset_and_size <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX) {
527 let bound = get_dynamic_heap_bound(builder, env, heap);
528 let adjustment = offset_and_size as i64;
529 let adjustment_value = builder.ins().iconst(env.pointer_type(), adjustment);
530 if pcc {
531 builder.func.dfg.facts[adjustment_value] =
532 Some(Fact::constant(pointer_bit_width, offset_and_size));
533 }
534 let adjusted_bound = builder.ins().isub(bound, adjustment_value);
535 if pcc {
536 builder.func.dfg.facts[adjusted_bound] = Some(Fact::global_value_offset(
537 pointer_bit_width,
538 bound_gv,
539 -adjustment,
540 ));
541 }
542 let oob = make_compare(
543 builder,
544 IntCC::UnsignedGreaterThan,
545 index,
546 Some(0),
547 adjusted_bound,
548 Some(adjustment),
549 );
550 return Reachable(explicit_check_oob_condition_and_compute_addr(
551 env,
552 builder,
553 heap,
554 index,
555 offset,
556 access_size,
557 oob_behavior,
558 AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
559 oob,
560 trap,
561 ));
562 }
563
564 // General case for dynamic bounds checks:
565 //
566 // index + offset + access_size > bound
567 //
568 // And we have to handle the overflow case in the left-hand side.
569 let access_size_val = builder
570 .ins()
571 // Explicit cast from u64 to i64: we just want the raw
572 // bits, and iconst takes an `Imm64`.
573 .iconst(env.pointer_type(), offset_and_size as i64);
574 if pcc {
575 builder.func.dfg.facts[access_size_val] =
576 Some(Fact::constant(pointer_bit_width, offset_and_size));
577 }
578 let adjusted_index = env.uadd_overflow_trap(builder, index, access_size_val, trap);
579 if pcc {
580 builder.func.dfg.facts[adjusted_index] = Some(Fact::value_offset(
581 pointer_bit_width,
582 index,
583 i64::try_from(offset_and_size).unwrap(),
584 ));
585 }
586 let bound = get_dynamic_heap_bound(builder, env, heap);
587 let oob = make_compare(
588 builder,
589 IntCC::UnsignedGreaterThan,
590 adjusted_index,
591 i64::try_from(offset_and_size).ok(),
592 bound,
593 Some(0),
594 );
595 Reachable(explicit_check_oob_condition_and_compute_addr(
596 env,
597 builder,
598 heap,
599 index,
600 offset,
601 access_size,
602 oob_behavior,
603 AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
604 oob,
605 trap,
606 ))
607}
608
609/// Get the bound of a dynamic heap as an `ir::Value`.
610fn get_dynamic_heap_bound(
611 builder: &mut FunctionBuilder,
612 env: &mut FuncEnvironment<'_>,
613 heap: &HeapData,
614) -> ir::Value {
615 let enable_pcc = heap.pcc_memory_type.is_some();
616
617 let (value, gv) = match heap.memory.static_heap_size() {
618 // The heap has a constant size, no need to actually load the
619 // bound. TODO: this is currently disabled for PCC because we
620 // can't easily prove that the GV load indeed results in a
621 // constant (that information is lost in the CLIF). We'll want
622 // to create an `iconst` GV expression kind to reify this fact
623 // in the GV, then re-enable this opt. (Or, alternately,
624 // compile such memories with a static-bound memtype and
625 // facts.)
626 Some(max_size) if !enable_pcc => (
627 builder.ins().iconst(env.pointer_type(), max_size as i64),
628 heap.bound,
629 ),
630
631 // Load the heap bound from its global variable.
632 _ => (
633 builder.ins().global_value(env.pointer_type(), heap.bound),
634 heap.bound,
635 ),
636 };
637
638 // If proof-carrying code is enabled, apply a fact to the range to
639 // tie it to the GV.
640 if enable_pcc {
641 builder.func.dfg.facts[value] = Some(Fact::global_value(
642 u16::try_from(env.pointer_type().bits()).unwrap(),
643 gv,
644 ));
645 }
646
647 value
648}
649
650fn cast_index_to_pointer_ty(
651 index: ir::Value,
652 index_ty: ir::Type,
653 pointer_ty: ir::Type,
654 pcc: bool,
655 pos: &mut FuncCursor,
656 trap: ir::TrapCode,
657) -> ir::Value {
658 if index_ty == pointer_ty {
659 return index;
660 }
661
662 // If the index size is larger than the pointer, that means that this is a
663 // 32-bit host platform with a 64-bit wasm linear memory. If the index is
664 // larger than 2**32 then that's guaranteed to be out-of-bounds, otherwise we
665 // `ireduce` the index.
666 //
667 // Also note that at this time this branch doesn't support pcc nor the
668 // value-label-ranges of the below path.
669 //
670 // Finally, note that the returned `low_bits` here are still subject to an
671 // explicit bounds check in wasm so in terms of Spectre speculation on
672 // either side of the `trapnz` should be ok.
673 if index_ty.bits() > pointer_ty.bits() {
674 assert_eq!(index_ty, ir::types::I64);
675 assert_eq!(pointer_ty, ir::types::I32);
676 let low_bits = pos.ins().ireduce(pointer_ty, index);
677 let c32 = pos.ins().iconst(pointer_ty, 32);
678 let high_bits = pos.ins().ushr(index, c32);
679 let high_bits = pos.ins().ireduce(pointer_ty, high_bits);
680 pos.ins().trapnz(high_bits, trap);
681 return low_bits;
682 }
683
684 // Convert `index` to `addr_ty`.
685 let extended_index = pos.ins().uextend(pointer_ty, index);
686
687 // Add a range fact on the extended value.
688 if pcc {
689 pos.func.dfg.facts[extended_index] = Some(Fact::max_range_for_width_extended(
690 u16::try_from(index_ty.bits()).unwrap(),
691 u16::try_from(pointer_ty.bits()).unwrap(),
692 ));
693 }
694
695 // Add debug value-label alias so that debuginfo can name the extended
696 // value as the address
697 let loc = pos.srcloc();
698 let loc = RelSourceLoc::from_base_offset(pos.func.params.base_srcloc(), loc);
699 pos.func
700 .stencil
701 .dfg
702 .add_value_label_alias(extended_index, loc, index);
703
704 extended_index
705}
706
707/// Which facts do we want to emit for proof-carrying code, if any, on
708/// address computations?
709#[derive(Clone, Copy, Debug)]
710enum AddrPcc {
711 /// A 32-bit static memory with the given size.
712 Static32(ir::MemoryType, u64),
713 /// Dynamic bounds-check, with actual memory size (the `GlobalValue`)
714 /// expressed symbolically.
715 Dynamic(ir::MemoryType, ir::GlobalValue),
716}
717impl AddrPcc {
718 fn static32(memory_type: Option<ir::MemoryType>, size: u64) -> Option<Self> {
719 memory_type.map(|ty| AddrPcc::Static32(ty, size))
720 }
721 fn dynamic(memory_type: Option<ir::MemoryType>, bound: ir::GlobalValue) -> Option<Self> {
722 memory_type.map(|ty| AddrPcc::Dynamic(ty, bound))
723 }
724}
725
726/// What to do on out-of-bounds for the
727/// `explicit_check_oob_condition_and_compute_addr` function below.
728enum OobBehavior {
729 /// An explicit `trapnz` instruction should be used.
730 ExplicitTrap,
731 /// A load from NULL should be issued if the address is out-of-bounds.
732 ConditionallyLoadFromZero {
733 /// Whether or not to use `select_spectre_guard` to choose the address
734 /// to load from. If `false` then a normal `select` is used.
735 select_spectre_guard: bool,
736 },
737}
738
739/// Emit explicit checks on the given out-of-bounds condition for the Wasm
740/// address and return the native address.
741///
742/// This function deduplicates explicit bounds checks and Spectre mitigations
743/// that inherently also implement bounds checking.
744fn explicit_check_oob_condition_and_compute_addr(
745 env: &mut FuncEnvironment<'_>,
746 builder: &mut FunctionBuilder,
747 heap: &HeapData,
748 index: ir::Value,
749 offset: u32,
750 access_size: u8,
751 oob_behavior: OobBehavior,
752 // Whether we're emitting PCC facts.
753 pcc: Option<AddrPcc>,
754 // The `i8` boolean value that is non-zero when the heap access is out of
755 // bounds (and therefore we should trap) and is zero when the heap access is
756 // in bounds (and therefore we can proceed).
757 oob_condition: ir::Value,
758 trap: ir::TrapCode,
759) -> ir::Value {
760 if let OobBehavior::ExplicitTrap = oob_behavior {
761 env.trapnz(builder, oob_condition, trap);
762 }
763 let addr_ty = env.pointer_type();
764
765 let mut addr = compute_addr(&mut builder.cursor(), heap, addr_ty, index, offset, pcc);
766
767 if let OobBehavior::ConditionallyLoadFromZero {
768 select_spectre_guard,
769 } = oob_behavior
770 {
771 // These mitigations rely on trapping when loading from NULL so
772 // CLIF memory instruction traps must be allowed for this to be
773 // generated.
774 assert!(env.load_from_zero_allowed());
775 let null = builder.ins().iconst(addr_ty, 0);
776 addr = if select_spectre_guard {
777 builder
778 .ins()
779 .select_spectre_guard(oob_condition, null, addr)
780 } else {
781 builder.ins().select(oob_condition, null, addr)
782 };
783
784 match pcc {
785 None => {}
786 Some(AddrPcc::Static32(ty, size)) => {
787 builder.func.dfg.facts[null] =
788 Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
789 builder.func.dfg.facts[addr] = Some(Fact::Mem {
790 ty,
791 min_offset: 0,
792 max_offset: size.checked_sub(u64::from(access_size)).unwrap(),
793 nullable: true,
794 });
795 }
796 Some(AddrPcc::Dynamic(ty, gv)) => {
797 builder.func.dfg.facts[null] =
798 Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
799 builder.func.dfg.facts[addr] = Some(Fact::DynamicMem {
800 ty,
801 min: Expr::constant(0),
802 max: Expr::offset(
803 &Expr::global_value(gv),
804 i64::try_from(env.tunables().memory_guard_size)
805 .unwrap()
806 .checked_sub(i64::from(access_size))
807 .unwrap(),
808 )
809 .unwrap(),
810 nullable: true,
811 });
812 }
813 }
814 }
815
816 addr
817}
818
819/// Emit code for the native address computation of a Wasm address,
820/// without any bounds checks or overflow checks.
821///
822/// It is the caller's responsibility to ensure that any necessary bounds and
823/// overflow checks are emitted, and that the resulting address is never used
824/// unless they succeed.
825fn compute_addr(
826 pos: &mut FuncCursor,
827 heap: &HeapData,
828 addr_ty: ir::Type,
829 index: ir::Value,
830 offset: u32,
831 pcc: Option<AddrPcc>,
832) -> ir::Value {
833 debug_assert_eq!(pos.func.dfg.value_type(index), addr_ty);
834
835 let heap_base = pos.ins().global_value(addr_ty, heap.base);
836
837 match pcc {
838 None => {}
839 Some(AddrPcc::Static32(ty, _size)) => {
840 pos.func.dfg.facts[heap_base] = Some(Fact::Mem {
841 ty,
842 min_offset: 0,
843 max_offset: 0,
844 nullable: false,
845 });
846 }
847 Some(AddrPcc::Dynamic(ty, _limit)) => {
848 pos.func.dfg.facts[heap_base] = Some(Fact::dynamic_base_ptr(ty));
849 }
850 }
851
852 let base_and_index = pos.ins().iadd(heap_base, index);
853
854 match pcc {
855 None => {}
856 Some(AddrPcc::Static32(ty, _) | AddrPcc::Dynamic(ty, _)) => {
857 if let Some(idx) = pos.func.dfg.facts[index]
858 .as_ref()
859 .and_then(|f| f.as_symbol())
860 .cloned()
861 {
862 pos.func.dfg.facts[base_and_index] = Some(Fact::DynamicMem {
863 ty,
864 min: idx.clone(),
865 max: idx,
866 nullable: false,
867 });
868 } else {
869 pos.func.dfg.facts[base_and_index] = Some(Fact::Mem {
870 ty,
871 min_offset: 0,
872 max_offset: u64::from(u32::MAX),
873 nullable: false,
874 });
875 }
876 }
877 }
878
879 if offset == 0 {
880 base_and_index
881 } else {
882 // NB: The addition of the offset immediate must happen *before* the
883 // `select_spectre_guard`, if any. If it happens after, then we
884 // potentially are letting speculative execution read the whole first
885 // 4GiB of memory.
886 let offset_val = pos.ins().iconst(addr_ty, i64::from(offset));
887
888 if pcc.is_some() {
889 pos.func.dfg.facts[offset_val] = Some(Fact::constant(
890 u16::try_from(addr_ty.bits()).unwrap(),
891 u64::from(offset),
892 ));
893 }
894
895 let result = pos.ins().iadd(base_and_index, offset_val);
896
897 match pcc {
898 None => {}
899 Some(AddrPcc::Static32(ty, _) | AddrPcc::Dynamic(ty, _)) => {
900 if let Some(idx) = pos.func.dfg.facts[index]
901 .as_ref()
902 .and_then(|f| f.as_symbol())
903 {
904 pos.func.dfg.facts[result] = Some(Fact::DynamicMem {
905 ty,
906 min: idx.clone(),
907 // Safety: adding an offset to an expression with
908 // zero offset -- add cannot wrap, so `unwrap()`
909 // cannot fail.
910 max: Expr::offset(idx, i64::from(offset)).unwrap(),
911 nullable: false,
912 });
913 } else {
914 pos.func.dfg.facts[result] = Some(Fact::Mem {
915 ty,
916 min_offset: u64::from(offset),
917 // Safety: can't overflow -- two u32s summed in a
918 // 64-bit add. TODO: when memory64 is supported here,
919 // `u32::MAX` is no longer true, and we'll need to
920 // handle overflow here.
921 max_offset: u64::from(u32::MAX) + u64::from(offset),
922 nullable: false,
923 });
924 }
925 }
926 }
927 result
928 }
929}
930
931#[inline]
932fn offset_plus_size(offset: u32, size: u8) -> u64 {
933 // Cannot overflow because we are widening to `u64`.
934 offset as u64 + size as u64
935}
936
937/// Returns whether `index` is statically in-bounds with respect to this
938/// `heap`'s configuration.
939///
940/// This is `true` when `index` is a constant and when the offset/size are added
941/// in it's all still less than the minimum byte size of the heap.
942///
943/// The `offset_and_size` here are the static offset that was listed on the wasm
944/// instruction plus the size of the access being made.
945fn statically_in_bounds(
946 func: &ir::Function,
947 heap: &HeapData,
948 index: ir::Value,
949 offset_and_size: u64,
950) -> bool {
951 func.dfg
952 .value_def(index)
953 .inst()
954 .and_then(|i| {
955 let imm = match func.dfg.insts[i] {
956 ir::InstructionData::UnaryImm {
957 opcode: ir::Opcode::Iconst,
958 imm,
959 } => imm,
960 _ => return None,
961 };
962 let ty = func.dfg.value_type(index);
963 let index = imm.zero_extend_from_width(ty.bits()).bits().cast_unsigned();
964 let final_addr = index.checked_add(offset_and_size)?;
965 Some(final_addr <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX))
966 })
967 .unwrap_or(false)
968}