Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/crates/cranelift/src/bounds_checks.rs
3092 views
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
22
use crate::{
23
Reachability,
24
func_environ::FuncEnvironment,
25
translate::{HeapData, TargetEnvironment},
26
};
27
use Reachability::*;
28
use cranelift_codegen::{
29
cursor::{Cursor, FuncCursor},
30
ir::{self, InstBuilder, RelSourceLoc, condcodes::IntCC},
31
ir::{Expr, Fact},
32
};
33
use 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)]
42
pub 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.
80
pub 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
172
fn 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
&& memory_reservation >= offset_and_size
406
{
407
let adjusted_bound = memory_reservation.checked_sub(offset_and_size).unwrap();
408
let adjusted_bound_value = builder
409
.ins()
410
.iconst(env.pointer_type(), adjusted_bound as i64);
411
if pcc {
412
builder.func.dfg.facts[adjusted_bound_value] =
413
Some(Fact::constant(pointer_bit_width, adjusted_bound));
414
}
415
let oob = make_compare(
416
builder,
417
IntCC::UnsignedGreaterThan,
418
index,
419
Some(0),
420
adjusted_bound_value,
421
Some(0),
422
);
423
return Reachable(explicit_check_oob_condition_and_compute_addr(
424
env,
425
builder,
426
heap,
427
index,
428
offset,
429
access_size,
430
oob_behavior,
431
AddrPcc::static32(heap.pcc_memory_type, memory_reservation),
432
oob,
433
trap,
434
));
435
}
436
437
// Special case for when `offset + access_size == 1`:
438
//
439
// index + 1 > bound
440
// ==> index >= bound
441
//
442
// Note that this special case is skipped for Pulley targets to assist with
443
// pattern-matching bounds checks into single instructions. Otherwise more
444
// patterns/instructions would have to be added to match this. In the end
445
// the goal is to emit one instruction anyway, so this optimization is
446
// largely only applicable for native platforms.
447
if offset_and_size == 1 && !env.is_pulley() {
448
let bound = get_dynamic_heap_bound(builder, env, heap);
449
let oob = make_compare(
450
builder,
451
IntCC::UnsignedGreaterThanOrEqual,
452
index,
453
Some(0),
454
bound,
455
Some(0),
456
);
457
return Reachable(explicit_check_oob_condition_and_compute_addr(
458
env,
459
builder,
460
heap,
461
index,
462
offset,
463
access_size,
464
oob_behavior,
465
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
466
oob,
467
trap,
468
));
469
}
470
471
// Special case for when we know that there are enough guard
472
// pages to cover the offset and access size.
473
//
474
// The precise should-we-trap condition is
475
//
476
// index + offset + access_size > bound
477
//
478
// However, if we instead check only the partial condition
479
//
480
// index > bound
481
//
482
// then the most out of bounds that the access can be, while that
483
// partial check still succeeds, is `offset + access_size`.
484
//
485
// However, when we have a guard region that is at least as large as
486
// `offset + access_size`, we can rely on the virtual memory
487
// subsystem handling these out-of-bounds errors at
488
// runtime. Therefore, the partial `index > bound` check is
489
// sufficient for this heap configuration.
490
//
491
// Additionally, this has the advantage that a series of Wasm loads
492
// that use the same dynamic index operand but different static
493
// offset immediates -- which is a common code pattern when accessing
494
// multiple fields in the same struct that is in linear memory --
495
// will all emit the same `index > bound` check, which we can GVN.
496
if can_use_virtual_memory && offset_and_size <= memory_guard_size {
497
let bound = get_dynamic_heap_bound(builder, env, heap);
498
let oob = make_compare(
499
builder,
500
IntCC::UnsignedGreaterThan,
501
index,
502
Some(0),
503
bound,
504
Some(0),
505
);
506
return Reachable(explicit_check_oob_condition_and_compute_addr(
507
env,
508
builder,
509
heap,
510
index,
511
offset,
512
access_size,
513
oob_behavior,
514
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
515
oob,
516
trap,
517
));
518
}
519
520
// Special case for when `offset + access_size <= min_size`.
521
//
522
// We know that `bound >= min_size`, so we can do the following
523
// comparison, without fear of the right-hand side wrapping around:
524
//
525
// index + offset + access_size > bound
526
// ==> index > bound - (offset + access_size)
527
if offset_and_size <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX) {
528
let bound = get_dynamic_heap_bound(builder, env, heap);
529
let adjustment = offset_and_size as i64;
530
let adjustment_value = builder.ins().iconst(env.pointer_type(), adjustment);
531
if pcc {
532
builder.func.dfg.facts[adjustment_value] =
533
Some(Fact::constant(pointer_bit_width, offset_and_size));
534
}
535
let adjusted_bound = builder.ins().isub(bound, adjustment_value);
536
if pcc {
537
builder.func.dfg.facts[adjusted_bound] = Some(Fact::global_value_offset(
538
pointer_bit_width,
539
bound_gv,
540
-adjustment,
541
));
542
}
543
let oob = make_compare(
544
builder,
545
IntCC::UnsignedGreaterThan,
546
index,
547
Some(0),
548
adjusted_bound,
549
Some(adjustment),
550
);
551
return Reachable(explicit_check_oob_condition_and_compute_addr(
552
env,
553
builder,
554
heap,
555
index,
556
offset,
557
access_size,
558
oob_behavior,
559
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
560
oob,
561
trap,
562
));
563
}
564
565
// General case for dynamic bounds checks:
566
//
567
// index + offset + access_size > bound
568
//
569
// And we have to handle the overflow case in the left-hand side.
570
let access_size_val = builder
571
.ins()
572
// Explicit cast from u64 to i64: we just want the raw
573
// bits, and iconst takes an `Imm64`.
574
.iconst(env.pointer_type(), offset_and_size as i64);
575
if pcc {
576
builder.func.dfg.facts[access_size_val] =
577
Some(Fact::constant(pointer_bit_width, offset_and_size));
578
}
579
let adjusted_index = env.uadd_overflow_trap(builder, index, access_size_val, trap);
580
if pcc {
581
builder.func.dfg.facts[adjusted_index] = Some(Fact::value_offset(
582
pointer_bit_width,
583
index,
584
i64::try_from(offset_and_size).unwrap(),
585
));
586
}
587
let bound = get_dynamic_heap_bound(builder, env, heap);
588
let oob = make_compare(
589
builder,
590
IntCC::UnsignedGreaterThan,
591
adjusted_index,
592
i64::try_from(offset_and_size).ok(),
593
bound,
594
Some(0),
595
);
596
Reachable(explicit_check_oob_condition_and_compute_addr(
597
env,
598
builder,
599
heap,
600
index,
601
offset,
602
access_size,
603
oob_behavior,
604
AddrPcc::dynamic(heap.pcc_memory_type, bound_gv),
605
oob,
606
trap,
607
))
608
}
609
610
/// Get the bound of a dynamic heap as an `ir::Value`.
611
fn get_dynamic_heap_bound(
612
builder: &mut FunctionBuilder,
613
env: &mut FuncEnvironment<'_>,
614
heap: &HeapData,
615
) -> ir::Value {
616
let enable_pcc = heap.pcc_memory_type.is_some();
617
618
let (value, gv) = match heap.memory.static_heap_size() {
619
// The heap has a constant size, no need to actually load the
620
// bound. TODO: this is currently disabled for PCC because we
621
// can't easily prove that the GV load indeed results in a
622
// constant (that information is lost in the CLIF). We'll want
623
// to create an `iconst` GV expression kind to reify this fact
624
// in the GV, then re-enable this opt. (Or, alternately,
625
// compile such memories with a static-bound memtype and
626
// facts.)
627
Some(max_size) if !enable_pcc => (
628
builder.ins().iconst(env.pointer_type(), max_size as i64),
629
heap.bound,
630
),
631
632
// Load the heap bound from its global variable.
633
_ => (
634
builder.ins().global_value(env.pointer_type(), heap.bound),
635
heap.bound,
636
),
637
};
638
639
// If proof-carrying code is enabled, apply a fact to the range to
640
// tie it to the GV.
641
if enable_pcc {
642
builder.func.dfg.facts[value] = Some(Fact::global_value(
643
u16::try_from(env.pointer_type().bits()).unwrap(),
644
gv,
645
));
646
}
647
648
value
649
}
650
651
fn cast_index_to_pointer_ty(
652
index: ir::Value,
653
index_ty: ir::Type,
654
pointer_ty: ir::Type,
655
pcc: bool,
656
pos: &mut FuncCursor,
657
trap: ir::TrapCode,
658
) -> ir::Value {
659
if index_ty == pointer_ty {
660
return index;
661
}
662
663
// If the index size is larger than the pointer, that means that this is a
664
// 32-bit host platform with a 64-bit wasm linear memory. If the index is
665
// larger than 2**32 then that's guaranteed to be out-of-bounds, otherwise we
666
// `ireduce` the index.
667
//
668
// Also note that at this time this branch doesn't support pcc nor the
669
// value-label-ranges of the below path.
670
//
671
// Finally, note that the returned `low_bits` here are still subject to an
672
// explicit bounds check in wasm so in terms of Spectre speculation on
673
// either side of the `trapnz` should be ok.
674
if index_ty.bits() > pointer_ty.bits() {
675
assert_eq!(index_ty, ir::types::I64);
676
assert_eq!(pointer_ty, ir::types::I32);
677
let low_bits = pos.ins().ireduce(pointer_ty, index);
678
let c32 = pos.ins().iconst(pointer_ty, 32);
679
let high_bits = pos.ins().ushr(index, c32);
680
let high_bits = pos.ins().ireduce(pointer_ty, high_bits);
681
pos.ins().trapnz(high_bits, trap);
682
return low_bits;
683
}
684
685
// Convert `index` to `addr_ty`.
686
let extended_index = pos.ins().uextend(pointer_ty, index);
687
688
// Add a range fact on the extended value.
689
if pcc {
690
pos.func.dfg.facts[extended_index] = Some(Fact::max_range_for_width_extended(
691
u16::try_from(index_ty.bits()).unwrap(),
692
u16::try_from(pointer_ty.bits()).unwrap(),
693
));
694
}
695
696
// Add debug value-label alias so that debuginfo can name the extended
697
// value as the address
698
let loc = pos.srcloc();
699
let loc = RelSourceLoc::from_base_offset(pos.func.params.base_srcloc(), loc);
700
pos.func
701
.stencil
702
.dfg
703
.add_value_label_alias(extended_index, loc, index);
704
705
extended_index
706
}
707
708
/// Which facts do we want to emit for proof-carrying code, if any, on
709
/// address computations?
710
#[derive(Clone, Copy, Debug)]
711
enum AddrPcc {
712
/// A 32-bit static memory with the given size.
713
Static32(ir::MemoryType, u64),
714
/// Dynamic bounds-check, with actual memory size (the `GlobalValue`)
715
/// expressed symbolically.
716
Dynamic(ir::MemoryType, ir::GlobalValue),
717
}
718
impl AddrPcc {
719
fn static32(memory_type: Option<ir::MemoryType>, size: u64) -> Option<Self> {
720
memory_type.map(|ty| AddrPcc::Static32(ty, size))
721
}
722
fn dynamic(memory_type: Option<ir::MemoryType>, bound: ir::GlobalValue) -> Option<Self> {
723
memory_type.map(|ty| AddrPcc::Dynamic(ty, bound))
724
}
725
}
726
727
/// What to do on out-of-bounds for the
728
/// `explicit_check_oob_condition_and_compute_addr` function below.
729
enum OobBehavior {
730
/// An explicit `trapnz` instruction should be used.
731
ExplicitTrap,
732
/// A load from NULL should be issued if the address is out-of-bounds.
733
ConditionallyLoadFromZero {
734
/// Whether or not to use `select_spectre_guard` to choose the address
735
/// to load from. If `false` then a normal `select` is used.
736
select_spectre_guard: bool,
737
},
738
}
739
740
/// Emit explicit checks on the given out-of-bounds condition for the Wasm
741
/// address and return the native address.
742
///
743
/// This function deduplicates explicit bounds checks and Spectre mitigations
744
/// that inherently also implement bounds checking.
745
fn explicit_check_oob_condition_and_compute_addr(
746
env: &mut FuncEnvironment<'_>,
747
builder: &mut FunctionBuilder,
748
heap: &HeapData,
749
index: ir::Value,
750
offset: u32,
751
access_size: u8,
752
oob_behavior: OobBehavior,
753
// Whether we're emitting PCC facts.
754
pcc: Option<AddrPcc>,
755
// The `i8` boolean value that is non-zero when the heap access is out of
756
// bounds (and therefore we should trap) and is zero when the heap access is
757
// in bounds (and therefore we can proceed).
758
oob_condition: ir::Value,
759
trap: ir::TrapCode,
760
) -> ir::Value {
761
if let OobBehavior::ExplicitTrap = oob_behavior {
762
env.trapnz(builder, oob_condition, trap);
763
}
764
let addr_ty = env.pointer_type();
765
766
let mut addr = compute_addr(&mut builder.cursor(), heap, addr_ty, index, offset, pcc);
767
768
if let OobBehavior::ConditionallyLoadFromZero {
769
select_spectre_guard,
770
} = oob_behavior
771
{
772
// These mitigations rely on trapping when loading from NULL so
773
// CLIF memory instruction traps must be allowed for this to be
774
// generated.
775
assert!(env.load_from_zero_allowed());
776
let null = builder.ins().iconst(addr_ty, 0);
777
addr = if select_spectre_guard {
778
builder
779
.ins()
780
.select_spectre_guard(oob_condition, null, addr)
781
} else {
782
builder.ins().select(oob_condition, null, addr)
783
};
784
785
match pcc {
786
None => {}
787
Some(AddrPcc::Static32(ty, size)) => {
788
builder.func.dfg.facts[null] =
789
Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
790
builder.func.dfg.facts[addr] = Some(Fact::Mem {
791
ty,
792
min_offset: 0,
793
max_offset: size.checked_sub(u64::from(access_size)).unwrap(),
794
nullable: true,
795
});
796
}
797
Some(AddrPcc::Dynamic(ty, gv)) => {
798
builder.func.dfg.facts[null] =
799
Some(Fact::constant(u16::try_from(addr_ty.bits()).unwrap(), 0));
800
builder.func.dfg.facts[addr] = Some(Fact::DynamicMem {
801
ty,
802
min: Expr::constant(0),
803
max: Expr::offset(
804
&Expr::global_value(gv),
805
i64::try_from(env.tunables().memory_guard_size)
806
.unwrap()
807
.checked_sub(i64::from(access_size))
808
.unwrap(),
809
)
810
.unwrap(),
811
nullable: true,
812
});
813
}
814
}
815
}
816
817
addr
818
}
819
820
/// Emit code for the native address computation of a Wasm address,
821
/// without any bounds checks or overflow checks.
822
///
823
/// It is the caller's responsibility to ensure that any necessary bounds and
824
/// overflow checks are emitted, and that the resulting address is never used
825
/// unless they succeed.
826
fn compute_addr(
827
pos: &mut FuncCursor,
828
heap: &HeapData,
829
addr_ty: ir::Type,
830
index: ir::Value,
831
offset: u32,
832
pcc: Option<AddrPcc>,
833
) -> ir::Value {
834
debug_assert_eq!(pos.func.dfg.value_type(index), addr_ty);
835
836
let heap_base = pos.ins().global_value(addr_ty, heap.base);
837
838
match pcc {
839
None => {}
840
Some(AddrPcc::Static32(ty, _size)) => {
841
pos.func.dfg.facts[heap_base] = Some(Fact::Mem {
842
ty,
843
min_offset: 0,
844
max_offset: 0,
845
nullable: false,
846
});
847
}
848
Some(AddrPcc::Dynamic(ty, _limit)) => {
849
pos.func.dfg.facts[heap_base] = Some(Fact::dynamic_base_ptr(ty));
850
}
851
}
852
853
let base_and_index = pos.ins().iadd(heap_base, index);
854
855
match pcc {
856
None => {}
857
Some(AddrPcc::Static32(ty, _) | AddrPcc::Dynamic(ty, _)) => {
858
if let Some(idx) = pos.func.dfg.facts[index]
859
.as_ref()
860
.and_then(|f| f.as_symbol())
861
.cloned()
862
{
863
pos.func.dfg.facts[base_and_index] = Some(Fact::DynamicMem {
864
ty,
865
min: idx.clone(),
866
max: idx,
867
nullable: false,
868
});
869
} else {
870
pos.func.dfg.facts[base_and_index] = Some(Fact::Mem {
871
ty,
872
min_offset: 0,
873
max_offset: u64::from(u32::MAX),
874
nullable: false,
875
});
876
}
877
}
878
}
879
880
if offset == 0 {
881
base_and_index
882
} else {
883
// NB: The addition of the offset immediate must happen *before* the
884
// `select_spectre_guard`, if any. If it happens after, then we
885
// potentially are letting speculative execution read the whole first
886
// 4GiB of memory.
887
let offset_val = pos.ins().iconst(addr_ty, i64::from(offset));
888
889
if pcc.is_some() {
890
pos.func.dfg.facts[offset_val] = Some(Fact::constant(
891
u16::try_from(addr_ty.bits()).unwrap(),
892
u64::from(offset),
893
));
894
}
895
896
let result = pos.ins().iadd(base_and_index, offset_val);
897
898
match pcc {
899
None => {}
900
Some(AddrPcc::Static32(ty, _) | AddrPcc::Dynamic(ty, _)) => {
901
if let Some(idx) = pos.func.dfg.facts[index]
902
.as_ref()
903
.and_then(|f| f.as_symbol())
904
{
905
pos.func.dfg.facts[result] = Some(Fact::DynamicMem {
906
ty,
907
min: idx.clone(),
908
// Safety: adding an offset to an expression with
909
// zero offset -- add cannot wrap, so `unwrap()`
910
// cannot fail.
911
max: Expr::offset(idx, i64::from(offset)).unwrap(),
912
nullable: false,
913
});
914
} else {
915
pos.func.dfg.facts[result] = Some(Fact::Mem {
916
ty,
917
min_offset: u64::from(offset),
918
// Safety: can't overflow -- two u32s summed in a
919
// 64-bit add. TODO: when memory64 is supported here,
920
// `u32::MAX` is no longer true, and we'll need to
921
// handle overflow here.
922
max_offset: u64::from(u32::MAX) + u64::from(offset),
923
nullable: false,
924
});
925
}
926
}
927
}
928
result
929
}
930
}
931
932
#[inline]
933
fn offset_plus_size(offset: u32, size: u8) -> u64 {
934
// Cannot overflow because we are widening to `u64`.
935
offset as u64 + size as u64
936
}
937
938
/// Returns whether `index` is statically in-bounds with respect to this
939
/// `heap`'s configuration.
940
///
941
/// This is `true` when `index` is a constant and when the offset/size are added
942
/// in it's all still less than the minimum byte size of the heap.
943
///
944
/// The `offset_and_size` here are the static offset that was listed on the wasm
945
/// instruction plus the size of the access being made.
946
fn statically_in_bounds(
947
func: &ir::Function,
948
heap: &HeapData,
949
index: ir::Value,
950
offset_and_size: u64,
951
) -> bool {
952
func.dfg
953
.value_def(index)
954
.inst()
955
.and_then(|i| {
956
let imm = match func.dfg.insts[i] {
957
ir::InstructionData::UnaryImm {
958
opcode: ir::Opcode::Iconst,
959
imm,
960
} => imm,
961
_ => return None,
962
};
963
let ty = func.dfg.value_type(index);
964
let index = imm.zero_extend_from_width(ty.bits()).bits().cast_unsigned();
965
let final_addr = index.checked_add(offset_and_size)?;
966
Some(final_addr <= heap.memory.minimum_byte_size().unwrap_or(u64::MAX))
967
})
968
.unwrap_or(false)
969
}
970
971