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