1 //! Compact representation of exception handlers associated with
2 //! callsites, for use when searching a Cranelift stack for a handler.
3 //!
4 //! This module implements (i) conversion from the metadata provided
5 //! alongside Cranelift's compilation result (as provided by
6 //! [`cranelift_codegen::MachBufferFinalized::call_sites`]) to its
7 //! format, and (ii) use of its format to find a handler efficiently.
8 //!
9 //! The format has been designed so that it can be mapped in from disk
10 //! and used without post-processing; this enables efficient
11 //! module-loading in runtimes such as Wasmtime.
12
13 use object::{Bytes, LittleEndian, U32};
14
15 #[cfg(feature = "cranelift")]
16 use alloc::vec;
17 use alloc::vec::Vec;
18 #[cfg(feature = "cranelift")]
19 use cranelift_codegen::{
20 ExceptionContextLoc, FinalizedMachCallSite, FinalizedMachExceptionHandler, binemit::CodeOffset,
21 };
22 use wasmtime_environ::prelude::*;
23
24 /// Collector struct for exception handlers per call site.
25 ///
26 /// # Format
27 ///
28 /// We keep six different arrays (`Vec`s) that we build as we visit
29 /// callsites, in ascending offset (address relative to beginning of
30 /// code segment) order: callsite offsets, frame offsets,
31 /// tag/destination ranges, tags, tag context SP offset, destination
32 /// offsets.
33 ///
34 /// The callsite offsets, frame offsets, and tag/destination ranges
35 /// logically form a sorted lookup array, allowing us to find
36 /// information for any single callsite. The frame offset specifies
37 /// distance down to the SP value at the callsite (in bytes), relative
38 /// to the FP of that frame. The range denotes a range of indices in
39 /// the tag/context and destination offset arrays. Ranges are stored
40 /// with the (exclusive) *end* index only; the start index is implicit
41 /// as the previous end, or zero if first element.
42 ///
43 /// The slices of tag, context, and handlers arrays named by `ranges`
44 /// for each callsite specify a series of handler items for that
45 /// callsite. The tag and context together allow a
46 /// dynamic-tag-instance match in the unwinder: the context specifies
47 /// an offset from SP at the callsite that contains a machine word
48 /// (e.g. with vmctx) that, together with the static tag index, can be
49 /// used to perform a dynamic match. A context of `-1` indicates no
50 /// dynamic context, and a tag of `-1` indicates a catch-all
51 /// handler. If a handler item matches, control should be transferred
52 /// to the code offset given in the last array, `handlers`.
53 ///
54 /// # Example
55 ///
56 /// An example of this data format:
57 ///
58 /// ```plain
59 /// callsites: [0x10, 0x50, 0xf0] // callsites (return addrs) at offsets 0x10, 0x50, 0xf0
60 /// ranges: [2, 4, 5] // corresponding ranges for each callsite
61 /// frame_offsets: [0, 0x10, 0] // corresponding SP-to-FP offsets for each callsite
62 /// tags: [1, 5, 1, -1, -1] // tags for each handler at each callsite
63 /// contexts: [-1, -1, 0x10, 0x20, 0x30] // SP-offset for context for each tag
64 /// handlers: [0x40, 0x42, 0x6f, 0x71, 0xf5] // handler destinations at each callsite
65 /// ```
66 ///
67 /// Expanding this out:
68 ///
69 /// ```plain
70 /// callsites: [0x10, 0x50, 0xf0], # PCs relative to some start of return-points.
71 /// frame_offsets: [0, 0x10, 0], # SP-to-FP offsets at each callsite.
72 /// ranges: [
73 /// 2, # callsite 0x10 has tags/handlers indices 0..2
74 /// 4, # callsite 0x50 has tags/handlers indices 2..4
75 /// 5, # callsite 0xf0 has tags/handlers indices 4..5
76 /// ],
77 /// tags: [
78 /// # tags for callsite 0x10:
79 /// 1,
80 /// 5,
81 /// # tags for callsite 0x50:
82 /// 1,
83 /// -1, # "catch-all"
84 /// # tags for callsite 0xf0:
85 /// -1, # "catch-all"
86 /// ]
87 /// contexts: [
88 /// # SP-offsets for context for each tag at callsite 0x10:
89 /// -1,
90 /// -1,
91 /// # for callsite 0x50:
92 /// 0x10,
93 /// 0x20,
94 /// # for callsite 0xf0:
95 /// 0x30,
96 /// ]
97 /// handlers: [
98 /// # handlers for callsite 0x10:
99 /// 0x40, # relative PC to handle tag 1 (above)
100 /// 0x42, # relative PC to handle tag 5
101 /// # handlers for callsite 0x50:
102 /// 0x6f, # relative PC to handle tag 1
103 /// 0x71, # relative PC to handle all other tags
104 /// # handlers for callsite 0xf0:
105 /// 0xf5, # relative PC to handle all other tags
106 /// ]
107 /// ```
108 #[cfg(feature = "cranelift")]
109 #[derive(Clone, Debug, Default)]
110 pub struct ExceptionTableBuilder {
111 pub callsites: Vec<U32<LittleEndian>>,
112 pub frame_offsets: Vec<U32<LittleEndian>>,
113 pub ranges: Vec<U32<LittleEndian>>,
114 pub tags: Vec<U32<LittleEndian>>,
115 pub contexts: Vec<U32<LittleEndian>>,
116 pub handlers: Vec<U32<LittleEndian>>,
117 last_start_offset: CodeOffset,
118 }
119
120 #[cfg(feature = "cranelift")]
121 impl ExceptionTableBuilder {
122 /// Add a function at a given offset from the start of the
123 /// compiled code section, recording information about its call
124 /// sites.
125 ///
126 /// Functions must be added in ascending offset order.
add_func<'a>( &mut self, start_offset: CodeOffset, call_sites: impl Iterator<Item = FinalizedMachCallSite<'a>>, ) -> Result<()>127 pub fn add_func<'a>(
128 &mut self,
129 start_offset: CodeOffset,
130 call_sites: impl Iterator<Item = FinalizedMachCallSite<'a>>,
131 ) -> Result<()> {
132 // Ensure that we see functions in offset order.
133 assert!(start_offset >= self.last_start_offset);
134 self.last_start_offset = start_offset;
135
136 // Visit each callsite in turn, translating offsets from
137 // function-local to section-local.
138 let mut handlers = vec![];
139 for call_site in call_sites {
140 let ret_addr = call_site.ret_addr.checked_add(start_offset).unwrap();
141 handlers.extend(call_site.exception_handlers.iter().cloned());
142
143 let start_idx = u32::try_from(self.tags.len()).unwrap();
144 let mut context = u32::MAX;
145 for handler in call_site.exception_handlers {
146 match handler {
147 FinalizedMachExceptionHandler::Tag(tag, offset) => {
148 self.tags.push(U32::new(LittleEndian, tag.as_u32()));
149 self.contexts.push(U32::new(LittleEndian, context));
150 self.handlers.push(U32::new(
151 LittleEndian,
152 offset.checked_add(start_offset).unwrap(),
153 ));
154 }
155 FinalizedMachExceptionHandler::Default(offset) => {
156 self.tags.push(U32::new(LittleEndian, u32::MAX));
157 self.contexts.push(U32::new(LittleEndian, context));
158 self.handlers.push(U32::new(
159 LittleEndian,
160 offset.checked_add(start_offset).unwrap(),
161 ));
162 }
163 FinalizedMachExceptionHandler::Context(ExceptionContextLoc::SPOffset(
164 offset,
165 )) => {
166 context = *offset;
167 }
168 FinalizedMachExceptionHandler::Context(ExceptionContextLoc::GPR(_)) => {
169 panic!(
170 "Wasmtime exception unwind info only supports dynamic contexts on the stack"
171 );
172 }
173 }
174 }
175 let end_idx = u32::try_from(self.tags.len()).unwrap();
176
177 // Omit empty callsites for compactness.
178 if end_idx > start_idx {
179 self.ranges.push(U32::new(LittleEndian, end_idx));
180 self.frame_offsets.push(U32::new(
181 LittleEndian,
182 call_site.frame_offset.unwrap_or(u32::MAX),
183 ));
184 self.callsites.push(U32::new(LittleEndian, ret_addr));
185 }
186 }
187
188 Ok(())
189 }
190
191 /// Serialize the exception-handler data section, taking a closure
192 /// to consume slices.
serialize<F: FnMut(&[u8])>(&self, mut f: F)193 pub fn serialize<F: FnMut(&[u8])>(&self, mut f: F) {
194 // Serialize the length of `callsites` / `ranges`.
195 let callsite_count = u32::try_from(self.callsites.len()).unwrap();
196 f(&callsite_count.to_le_bytes());
197 // Serialize the length of `tags` / `handlers`.
198 let handler_count = u32::try_from(self.handlers.len()).unwrap();
199 f(&handler_count.to_le_bytes());
200
201 // Serialize `callsites`, `ranges`, `tags`, and `handlers` in
202 // that order.
203 f(object::bytes_of_slice(&self.callsites));
204 f(object::bytes_of_slice(&self.frame_offsets));
205 f(object::bytes_of_slice(&self.ranges));
206 f(object::bytes_of_slice(&self.tags));
207 f(object::bytes_of_slice(&self.contexts));
208 f(object::bytes_of_slice(&self.handlers));
209 }
210
211 /// Serialize the exception-handler data section to a vector of
212 /// bytes.
to_vec(&self) -> Vec<u8>213 pub fn to_vec(&self) -> Vec<u8> {
214 let mut bytes = vec![];
215 self.serialize(|slice| bytes.extend(slice.iter().cloned()));
216 bytes
217 }
218 }
219
220 /// ExceptionTable deserialized from a serialized slice.
221 ///
222 /// This struct retains borrows of the various serialized parts of the
223 /// exception table data as produced by
224 /// [`ExceptionTableBuilder::serialize`].
225 #[derive(Clone, Debug)]
226 pub struct ExceptionTable<'a> {
227 callsites: &'a [U32<LittleEndian>],
228 ranges: &'a [U32<LittleEndian>],
229 frame_offsets: &'a [U32<LittleEndian>],
230 tags: &'a [U32<LittleEndian>],
231 contexts: &'a [U32<LittleEndian>],
232 handlers: &'a [U32<LittleEndian>],
233 }
234
235 /// Wasmtime exception table item, after parsing.
236 ///
237 /// Note that this is separately defined from the equivalent type in
238 /// Cranelift, `cranelift_codegen::FinalizedMachExceptionHandler`,
239 /// because we need this in runtime-only builds when Cranelift is not
240 /// included.
241 #[derive(Clone, Debug, PartialEq, Eq)]
242 pub struct ExceptionHandler {
243 /// A tag (arbitrary `u32` identifier from CLIF) or `None` for catch-all.
244 pub tag: Option<u32>,
245 /// Dynamic context, if provided, with which to interpret the
246 /// tag. Context is available at the given offset from SP in this
247 /// frame.
248 pub context_sp_offset: Option<u32>,
249 /// Handler code offset.
250 pub handler_offset: u32,
251 }
252
253 impl<'a> ExceptionTable<'a> {
254 /// Parse exception tables from a byte-slice as produced by
255 /// [`ExceptionTableBuilder::serialize`].
parse(data: &'a [u8]) -> Result<ExceptionTable<'a>>256 pub fn parse(data: &'a [u8]) -> Result<ExceptionTable<'a>> {
257 let mut data = Bytes(data);
258 let callsite_count = data
259 .read::<U32<LittleEndian>>()
260 .map_err(|_| format_err!("Unable to read callsite count prefix"))?;
261 let callsite_count = usize::try_from(callsite_count.get(LittleEndian))?;
262 let handler_count = data
263 .read::<U32<LittleEndian>>()
264 .map_err(|_| format_err!("Unable to read handler count prefix"))?;
265 let handler_count = usize::try_from(handler_count.get(LittleEndian))?;
266 let (callsites, data) =
267 object::slice_from_bytes::<U32<LittleEndian>>(data.0, callsite_count)
268 .map_err(|_| format_err!("Unable to read callsites slice"))?;
269 let (frame_offsets, data) =
270 object::slice_from_bytes::<U32<LittleEndian>>(data, callsite_count)
271 .map_err(|_| format_err!("Unable to read frame_offsets slice"))?;
272 let (ranges, data) = object::slice_from_bytes::<U32<LittleEndian>>(data, callsite_count)
273 .map_err(|_| format_err!("Unable to read ranges slice"))?;
274 let (tags, data) = object::slice_from_bytes::<U32<LittleEndian>>(data, handler_count)
275 .map_err(|_| format_err!("Unable to read tags slice"))?;
276 let (contexts, data) = object::slice_from_bytes::<U32<LittleEndian>>(data, handler_count)
277 .map_err(|_| format_err!("Unable to read contexts slice"))?;
278 let (handlers, data) = object::slice_from_bytes::<U32<LittleEndian>>(data, handler_count)
279 .map_err(|_| format_err!("Unable to read handlers slice"))?;
280
281 if !data.is_empty() {
282 bail!("Unexpected data at end of serialized exception table");
283 }
284
285 Ok(ExceptionTable {
286 callsites,
287 frame_offsets,
288 ranges,
289 tags,
290 contexts,
291 handlers,
292 })
293 }
294
295 /// Look up the set of handlers, if any, for a given return
296 /// address (as an offset into the code section).
297 ///
298 /// The handler for `None` (the catch-all/default handler), if
299 /// any, will always come last.
300 ///
301 /// Note: we use raw `u32` types for code offsets here to avoid
302 /// dependencies on `cranelift-codegen` when this crate is built
303 /// without compiler backend support (runtime-only config).
304 ///
305 /// Returns a tuple of `(frame offset, handler iterator)`. The
306 /// frame offset, if `Some`, specifies the distance from SP to FP
307 /// at this callsite.
lookup_pc(&self, pc: u32) -> (Option<u32>, impl Iterator<Item = ExceptionHandler> + '_)308 pub fn lookup_pc(&self, pc: u32) -> (Option<u32>, impl Iterator<Item = ExceptionHandler> + '_) {
309 let callsite_idx = self
310 .callsites
311 .binary_search_by_key(&pc, |callsite| callsite.get(LittleEndian))
312 .ok();
313 let frame_offset = callsite_idx
314 .map(|idx| self.frame_offsets[idx])
315 .and_then(|offset| option_from_u32(offset.get(LittleEndian)));
316
317 (
318 frame_offset,
319 callsite_idx
320 .into_iter()
321 .flat_map(|callsite_idx| self.handlers_for_callsite(callsite_idx)),
322 )
323 }
324
325 /// Look up the frame offset and handler destination if any, for a
326 /// given return address (as an offset into the code section) and
327 /// exception tag.
328 ///
329 /// Note: we use raw `u32` types for code offsets and tags here to
330 /// avoid dependencies on `cranelift-codegen` when this crate is
331 /// built without compiler backend support (runtime-only config).
lookup_pc_tag(&self, pc: u32, tag: u32) -> Option<(u32, u32)>332 pub fn lookup_pc_tag(&self, pc: u32, tag: u32) -> Option<(u32, u32)> {
333 // First, look up the callsite in the sorted callsites list.
334 let callsite_idx = self
335 .callsites
336 .binary_search_by_key(&pc, |callsite| callsite.get(LittleEndian))
337 .ok()?;
338 let frame_offset =
339 option_from_u32(self.frame_offsets[callsite_idx].get(LittleEndian)).unwrap_or(0);
340
341 let (tags, _, handlers) = self.tags_contexts_handlers_for_callsite(callsite_idx);
342
343 // Is there any handler with an exact tag match?
344 if let Ok(handler_idx) = tags.binary_search_by_key(&tag, |tag| tag.get(LittleEndian)) {
345 return Some((frame_offset, handlers[handler_idx].get(LittleEndian)));
346 }
347
348 // If not, is there a fallback handler? Note that we serialize
349 // it with the tag `u32::MAX`, so it is always last in sorted
350 // order.
351 if tags.last().map(|v| v.get(LittleEndian)) == Some(u32::MAX) {
352 return Some((frame_offset, handlers.last().unwrap().get(LittleEndian)));
353 }
354
355 None
356 }
357
tags_contexts_handlers_for_callsite( &self, idx: usize, ) -> ( &[U32<LittleEndian>], &[U32<LittleEndian>], &[U32<LittleEndian>], )358 fn tags_contexts_handlers_for_callsite(
359 &self,
360 idx: usize,
361 ) -> (
362 &[U32<LittleEndian>],
363 &[U32<LittleEndian>],
364 &[U32<LittleEndian>],
365 ) {
366 let end_idx = self.ranges[idx].get(LittleEndian);
367 let start_idx = if idx > 0 {
368 self.ranges[idx - 1].get(LittleEndian)
369 } else {
370 0
371 };
372
373 // Take the subslices of `tags`, `contexts`, and `handlers`
374 // corresponding to this callsite.
375 let start_idx = usize::try_from(start_idx).unwrap();
376 let end_idx = usize::try_from(end_idx).unwrap();
377 let tags = &self.tags[start_idx..end_idx];
378 let contexts = &self.contexts[start_idx..end_idx];
379 let handlers = &self.handlers[start_idx..end_idx];
380 (tags, contexts, handlers)
381 }
382
handlers_for_callsite(&self, idx: usize) -> impl Iterator<Item = ExceptionHandler>383 fn handlers_for_callsite(&self, idx: usize) -> impl Iterator<Item = ExceptionHandler> {
384 let (tags, contexts, handlers) = self.tags_contexts_handlers_for_callsite(idx);
385 tags.iter()
386 .zip(contexts.iter())
387 .zip(handlers.iter())
388 .map(|((tag, context), handler)| {
389 let tag = option_from_u32(tag.get(LittleEndian));
390 let context = option_from_u32(context.get(LittleEndian));
391 let handler = handler.get(LittleEndian);
392 ExceptionHandler {
393 tag,
394 context_sp_offset: context,
395 handler_offset: handler,
396 }
397 })
398 }
399
400 /// Provide an iterator over callsites, and for each callsite, the
401 /// frame offset and arrays of handlers.
into_iter(self) -> impl Iterator<Item = (u32, Option<u32>, Vec<ExceptionHandler>)> + 'a402 pub fn into_iter(self) -> impl Iterator<Item = (u32, Option<u32>, Vec<ExceptionHandler>)> + 'a {
403 self.callsites
404 .iter()
405 .map(|pc| pc.get(LittleEndian))
406 .enumerate()
407 .map(move |(i, pc)| {
408 (
409 pc,
410 option_from_u32(self.frame_offsets[i].get(LittleEndian)),
411 self.handlers_for_callsite(i).collect(),
412 )
413 })
414 }
415 }
416
option_from_u32(value: u32) -> Option<u32>417 fn option_from_u32(value: u32) -> Option<u32> {
418 if value == u32::MAX { None } else { Some(value) }
419 }
420
421 #[cfg(all(test, feature = "cranelift"))]
422 mod test {
423 use super::*;
424 use cranelift_codegen::entity::EntityRef;
425 use cranelift_codegen::ir::ExceptionTag;
426
427 #[test]
serialize_exception_table()428 fn serialize_exception_table() {
429 let callsites = [
430 FinalizedMachCallSite {
431 ret_addr: 0x10,
432 frame_offset: None,
433 exception_handlers: &[
434 FinalizedMachExceptionHandler::Tag(ExceptionTag::new(1), 0x20),
435 FinalizedMachExceptionHandler::Tag(ExceptionTag::new(2), 0x30),
436 FinalizedMachExceptionHandler::Default(0x40),
437 ],
438 },
439 FinalizedMachCallSite {
440 ret_addr: 0x48,
441 frame_offset: None,
442 exception_handlers: &[],
443 },
444 FinalizedMachCallSite {
445 ret_addr: 0x50,
446 frame_offset: Some(0x20),
447 exception_handlers: &[FinalizedMachExceptionHandler::Default(0x60)],
448 },
449 ];
450
451 let mut builder = ExceptionTableBuilder::default();
452 builder.add_func(0x100, callsites.into_iter()).unwrap();
453 let mut bytes = vec![];
454 builder.serialize(|slice| bytes.extend(slice.iter().cloned()));
455
456 let deserialized = ExceptionTable::parse(&bytes).unwrap();
457
458 let (frame_offset, iter) = deserialized.lookup_pc(0x148);
459 assert_eq!(frame_offset, None);
460 assert_eq!(iter.collect::<Vec<ExceptionHandler>>(), vec![]);
461
462 let (frame_offset, iter) = deserialized.lookup_pc(0x110);
463 assert_eq!(frame_offset, None);
464 assert_eq!(
465 iter.collect::<Vec<ExceptionHandler>>(),
466 vec![
467 ExceptionHandler {
468 tag: Some(1),
469 context_sp_offset: None,
470 handler_offset: 0x120
471 },
472 ExceptionHandler {
473 tag: Some(2),
474 context_sp_offset: None,
475 handler_offset: 0x130
476 },
477 ExceptionHandler {
478 tag: None,
479 context_sp_offset: None,
480 handler_offset: 0x140
481 },
482 ]
483 );
484
485 let (frame_offset, iter) = deserialized.lookup_pc(0x150);
486 assert_eq!(frame_offset, Some(0x20));
487 assert_eq!(
488 iter.collect::<Vec<ExceptionHandler>>(),
489 vec![ExceptionHandler {
490 tag: None,
491 context_sp_offset: None,
492 handler_offset: 0x160
493 }]
494 );
495 }
496 }
497