wasmer_compiler_cranelift/
eh.rs

1//! Helpers for generating DWARF LSDA data for Cranelift-compiled functions.
2//!
3//! The structures and encoding implemented here mirror what LLVM produces for
4//! Wasm exception handling so that Wasmer's libunwind personalities can parse
5//! the tables without any runtime changes.
6
7use cranelift_codegen::{
8    ExceptionContextLoc, FinalizedMachCallSite, FinalizedMachExceptionHandler,
9};
10use cranelift_entity::EntityRef;
11use std::collections::hash_map::Entry;
12use std::collections::{HashMap, HashSet};
13use std::convert::TryFrom;
14use std::io::{Cursor, Write};
15
16use wasmer_compiler::types::{
17    relocation::{Relocation, RelocationKind, RelocationTarget},
18    section::{CustomSection, CustomSectionProtection, SectionBody, SectionIndex},
19};
20
21/// Relocation information for an LSDA entry that references a tag constant.
22#[derive(Debug, Clone)]
23pub struct TagRelocation {
24    /// Offset within the LSDA blob where the relocation should be applied.
25    pub offset: u32,
26    /// The module-local exception tag value.
27    pub tag: u32,
28}
29
30/// Fully encoded LSDA bytes for a single function, together with pending tag
31/// relocations that will be resolved once the global tag section is built.
32#[derive(Debug, Clone)]
33pub struct FunctionLsdaData {
34    pub bytes: Vec<u8>,
35    pub relocations: Vec<TagRelocation>,
36}
37
38/// Build the LSDA for a single function given the finalized Cranelift
39/// call-site metadata.
40pub fn build_function_lsda<'a>(
41    call_sites: impl Iterator<Item = FinalizedMachCallSite<'a>>,
42    function_length: usize,
43    pointer_bytes: u8,
44) -> Option<FunctionLsdaData> {
45    let mut sites = Vec::new();
46
47    for site in call_sites {
48        let mut catches = Vec::new();
49        let mut landing_pad = None;
50
51        // Our landing pads handle all the tags considered for a call instruction, thus
52        // we use the latest landing pad.
53        for handler in site.exception_handlers {
54            match handler {
55                FinalizedMachExceptionHandler::Tag(tag, offset) => {
56                    landing_pad = Some(landing_pad.unwrap_or(*offset));
57                    catches.push(ActionKind::Tag {
58                        tag: u32::try_from(tag.index()).expect("tag index fits in u32"),
59                    });
60                }
61                FinalizedMachExceptionHandler::Default(offset) => {
62                    landing_pad = Some(landing_pad.unwrap_or(*offset));
63                    catches.push(ActionKind::CatchAll);
64                }
65                FinalizedMachExceptionHandler::Context(context) => {
66                    // Context records are used by Cranelift to thread VMContext
67                    // information through the landing pad. We emit the LSDA
68                    // regardless of whether we see them; nothing to do here.
69                    match context {
70                        ExceptionContextLoc::SPOffset(_) | ExceptionContextLoc::GPR(_) => {}
71                    }
72                }
73            }
74        }
75
76        if catches.is_empty() {
77            continue;
78        }
79
80        let landing_pad = landing_pad.expect("landing pad offset set when catches exist");
81        let cs_start = site.ret_addr.saturating_sub(1);
82
83        sites.push(CallSiteDesc {
84            start: cs_start,
85            len: 1,
86            landing_pad,
87            actions: catches,
88        });
89    }
90
91    if sites.is_empty() {
92        return None;
93    }
94
95    // Ensure all instructions in the function are covered by filling gaps with
96    // default unwinding behavior (no catch actions).
97    let mut current_pos = 0u32;
98    let mut filled_sites = Vec::new();
99
100    for site in sites {
101        if site.start > current_pos {
102            // Gap found: add a default site that covers instructions with no handlers
103            filled_sites.push(CallSiteDesc {
104                start: current_pos,
105                len: site.start - current_pos,
106                landing_pad: 0,
107                actions: Vec::new(),
108            });
109        }
110        current_pos = site.start + site.len;
111        filled_sites.push(site);
112    }
113
114    // Cover any remaining instructions at the end of the function
115    if current_pos < function_length as u32 {
116        filled_sites.push(CallSiteDesc {
117            start: current_pos,
118            len: function_length as u32 - current_pos,
119            landing_pad: 0,
120            actions: Vec::new(),
121        });
122    }
123
124    let sites = filled_sites;
125
126    let mut type_entries = TypeTable::new();
127    let mut callsite_actions = Vec::with_capacity(sites.len());
128
129    for site in &sites {
130        let mut action_indices = Vec::new();
131        for action in &site.actions {
132            let index = match action {
133                ActionKind::Tag { tag } => type_entries.get_or_insert_tag(*tag),
134                ActionKind::CatchAll => type_entries.get_or_insert_catch_all(),
135            };
136            action_indices.push(index as i32);
137        }
138        callsite_actions.push(action_indices);
139    }
140
141    let action_table = encode_action_table(&callsite_actions);
142    let call_site_table = encode_call_site_table(&sites, &action_table);
143    let (type_table_bytes, type_table_relocs) = type_entries.encode(pointer_bytes);
144
145    let call_site_table_len = call_site_table.len() as u64;
146    let mut writer = Cursor::new(Vec::new());
147    writer
148        .write_all(&gimli::DW_EH_PE_omit.0.to_le_bytes())
149        .unwrap(); // lpstart encoding omitted (relative to function start)
150
151    if type_entries.is_empty() {
152        writer
153            .write_all(&gimli::DW_EH_PE_omit.0.to_le_bytes())
154            .unwrap();
155    } else {
156        writer
157            .write_all(&gimli::DW_EH_PE_absptr.0.to_le_bytes())
158            .unwrap();
159    }
160
161    if !type_entries.is_empty() {
162        let ttype_table_end = 1 // call-site encoding byte
163            + uleb128_len(call_site_table_len)
164            + call_site_table.len()
165            + action_table.bytes.len()
166            + type_table_bytes.len();
167        leb128::write::unsigned(&mut writer, ttype_table_end as u64).unwrap();
168    }
169
170    writer
171        .write_all(&gimli::DW_EH_PE_udata4.0.to_le_bytes())
172        .unwrap();
173    leb128::write::unsigned(&mut writer, call_site_table_len).unwrap();
174    writer.write_all(&call_site_table).unwrap();
175    writer.write_all(&action_table.bytes).unwrap();
176
177    let type_table_offset = writer.position() as u32;
178    writer.write_all(&type_table_bytes).unwrap();
179
180    let mut relocations = Vec::new();
181    for reloc in type_table_relocs {
182        relocations.push(TagRelocation {
183            offset: type_table_offset + reloc.offset,
184            tag: reloc.tag,
185        });
186    }
187
188    Some(FunctionLsdaData {
189        bytes: writer.into_inner(),
190        relocations,
191    })
192}
193
194/// Build the global tag section and a tag->offset map.
195pub fn build_tag_section(
196    lsda_data: &[Option<FunctionLsdaData>],
197) -> Option<(CustomSection, HashMap<u32, u32>)> {
198    let mut unique_tags = HashSet::new();
199    for data in lsda_data.iter().flatten() {
200        for reloc in &data.relocations {
201            unique_tags.insert(reloc.tag);
202        }
203    }
204
205    if unique_tags.is_empty() {
206        return None;
207    }
208
209    let mut tags: Vec<u32> = unique_tags.into_iter().collect();
210    tags.sort_unstable();
211
212    let mut bytes = Vec::with_capacity(tags.len() * std::mem::size_of::<u32>());
213    let mut offsets = HashMap::new();
214    for tag in tags {
215        let offset = bytes.len() as u32;
216        bytes.extend_from_slice(&tag.to_ne_bytes());
217        offsets.insert(tag, offset);
218    }
219
220    let section = CustomSection {
221        protection: CustomSectionProtection::Read,
222        alignment: None,
223        bytes: SectionBody::new_with_vec(bytes),
224        relocations: Vec::new(),
225    };
226
227    Some((section, offsets))
228}
229
230/// Build the LSDA custom section and record the offset for each function.
231///
232/// Returns the section (if any) and a vector mapping each function index to
233/// its LSDA offset inside the section. Even when utilizing the same landing pad for exception tags,
234/// Cranelift generates separate landing pad locations.
235/// These locations are essentially small trampolines that redirect to the basic block we established (the EH dispatch block).
236///
237/// The section can be dumped using the elfutils' readelf tool:
238/// ```shell
239/// objcopy -I binary -O elf64-x86-64 --rename-section .data=.gcc_except_table,alloc,contents lsda.bin object.o && eu-readelf -w object.o
240/// ```
241pub fn build_lsda_section(
242    lsda_data: Vec<Option<FunctionLsdaData>>,
243    pointer_bytes: u8,
244    tag_offsets: &HashMap<u32, u32>,
245    tag_section_index: Option<SectionIndex>,
246) -> (Option<CustomSection>, Vec<Option<u32>>) {
247    let mut bytes = Vec::new();
248    let mut relocations = Vec::new();
249    let mut offsets_per_function = Vec::with_capacity(lsda_data.len());
250
251    let pointer_kind = match pointer_bytes {
252        4 => RelocationKind::Abs4,
253        8 => RelocationKind::Abs8,
254        other => panic!("unsupported pointer size {other} for LSDA generation"),
255    };
256
257    for data in lsda_data.into_iter() {
258        if let Some(data) = data {
259            let base = bytes.len() as u32;
260            bytes.extend_from_slice(&data.bytes);
261
262            for reloc in &data.relocations {
263                let target_offset = tag_offsets
264                    .get(&reloc.tag)
265                    .copied()
266                    .expect("missing tag offset for relocation");
267                relocations.push(Relocation {
268                    kind: pointer_kind,
269                    reloc_target: RelocationTarget::CustomSection(
270                        tag_section_index
271                            .expect("tag section index must exist when relocations are present"),
272                    ),
273                    offset: base + reloc.offset,
274                    addend: target_offset as i64,
275                });
276            }
277
278            offsets_per_function.push(Some(base));
279        } else {
280            offsets_per_function.push(None);
281        }
282    }
283
284    if bytes.is_empty() {
285        (None, offsets_per_function)
286    } else {
287        (
288            Some(CustomSection {
289                protection: CustomSectionProtection::Read,
290                alignment: None,
291                bytes: SectionBody::new_with_vec(bytes),
292                relocations,
293            }),
294            offsets_per_function,
295        )
296    }
297}
298
299#[derive(Debug)]
300struct CallSiteDesc {
301    start: u32,
302    len: u32,
303    landing_pad: u32,
304    actions: Vec<ActionKind>,
305}
306
307#[derive(Debug)]
308enum ActionKind {
309    Tag { tag: u32 },
310    CatchAll,
311}
312
313#[derive(Debug)]
314struct TypeTable {
315    entries: Vec<TypeEntry>,
316    index_map: HashMap<TypeKey, usize>,
317}
318
319impl TypeTable {
320    fn new() -> Self {
321        Self {
322            entries: Vec::new(),
323            index_map: HashMap::new(),
324        }
325    }
326
327    fn is_empty(&self) -> bool {
328        self.entries.is_empty()
329    }
330
331    fn get_or_insert_tag(&mut self, tag: u32) -> usize {
332        let key = TypeKey::Tag(tag);
333        if let Some(idx) = self.index_map.get(&key) {
334            *idx
335        } else {
336            let idx = self.entries.len() + 1;
337            self.entries.push(TypeEntry::Tag { tag });
338            self.index_map.insert(key, idx);
339            idx
340        }
341    }
342
343    fn get_or_insert_catch_all(&mut self) -> usize {
344        let key = TypeKey::CatchAll;
345        if let Some(idx) = self.index_map.get(&key) {
346            *idx
347        } else {
348            let idx = self.entries.len() + 1;
349            self.entries.push(TypeEntry::CatchAll);
350            self.index_map.insert(key, idx);
351            idx
352        }
353    }
354
355    fn encode(&self, pointer_bytes: u8) -> (Vec<u8>, Vec<TagRelocation>) {
356        let mut bytes = Vec::with_capacity(self.entries.len() * pointer_bytes as usize);
357        let mut relocations = Vec::new();
358
359        for entry in self.entries.iter().rev() {
360            let offset = bytes.len() as u32;
361            match entry {
362                TypeEntry::Tag { tag } => {
363                    bytes.extend_from_slice(&vec![0; pointer_bytes as usize]);
364                    relocations.push(TagRelocation { offset, tag: *tag });
365                }
366                TypeEntry::CatchAll => {
367                    bytes.extend_from_slice(&vec![0; pointer_bytes as usize]);
368                }
369            }
370        }
371
372        (bytes, relocations)
373    }
374}
375
376#[derive(Debug, Hash, PartialEq, Eq)]
377enum TypeKey {
378    Tag(u32),
379    CatchAll,
380}
381
382#[derive(Debug)]
383enum TypeEntry {
384    Tag { tag: u32 },
385    CatchAll,
386}
387
388struct ActionTable {
389    bytes: Vec<u8>,
390    first_action_offsets: Vec<Option<u32>>,
391}
392
393fn encode_action_table(callsite_actions: &[Vec<i32>]) -> ActionTable {
394    let mut writer = Cursor::new(Vec::new());
395    let mut first_action_offsets = Vec::new();
396
397    let mut cache = HashMap::new();
398
399    for actions in callsite_actions {
400        if actions.is_empty() {
401            first_action_offsets.push(None);
402        } else {
403            match cache.entry(actions.clone()) {
404                Entry::Occupied(entry) => {
405                    first_action_offsets.push(Some(*entry.get()));
406                }
407                Entry::Vacant(entry) => {
408                    let mut last_action_start = 0;
409                    for (i, &ttype_index) in actions.iter().enumerate() {
410                        let next_action_start = writer.position();
411                        leb128::write::signed(&mut writer, ttype_index as i64)
412                            .expect("leb128 write failed");
413
414                        if i != 0 {
415                            // Make a linked list to the previous action
416                            let displacement = last_action_start - writer.position() as i64;
417                            leb128::write::signed(&mut writer, displacement)
418                                .expect("leb128 write failed");
419                        } else {
420                            leb128::write::signed(&mut writer, 0).expect("leb128 write failed");
421                        }
422                        last_action_start = next_action_start as i64;
423                    }
424                    let last_action_start = last_action_start as u32;
425                    entry.insert(last_action_start);
426                    first_action_offsets.push(Some(last_action_start));
427                }
428            }
429        }
430    }
431
432    ActionTable {
433        bytes: writer.into_inner(),
434        first_action_offsets,
435    }
436}
437
438fn encode_call_site_table(callsites: &[CallSiteDesc], action_table: &ActionTable) -> Vec<u8> {
439    let mut writer = Cursor::new(Vec::new());
440    for (idx, site) in callsites.iter().enumerate() {
441        write_encoded_offset(site.start, &mut writer);
442        write_encoded_offset(site.len, &mut writer);
443        write_encoded_offset(site.landing_pad, &mut writer);
444
445        let action = match action_table.first_action_offsets[idx] {
446            Some(offset) => offset as u64 + 1,
447            None => 0,
448        };
449        leb128::write::unsigned(&mut writer, action).expect("leb128 write failed");
450    }
451    writer.into_inner()
452}
453
454fn write_encoded_offset(val: u32, out: &mut impl Write) {
455    // We use DW_EH_PE_udata4 for all offsets.
456    out.write_all(&val.to_le_bytes())
457        .expect("write to buffer failed")
458}
459
460fn uleb128_len(value: u64) -> usize {
461    let mut cursor = Cursor::new([0u8; 10]);
462    leb128::write::unsigned(&mut cursor, value).unwrap()
463}