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