Skip to main content

wasmer_compiler/engine/unwind/
systemv.rs

1// This file contains code from external sources.
2// Attributions: https://github.com/wasmerio/wasmer/blob/main/docs/ATTRIBUTIONS.md
3
4//! Module for System V ABI unwind registry.
5
6use core::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Once;
8
9use crate::types::unwind::CompiledFunctionUnwindInfoReference;
10
11/// Represents a registry of function unwind information for System V ABI.
12pub struct UnwindRegistry {
13    published: bool,
14    #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
15    registrations: Vec<usize>,
16    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
17    compact_unwind_mgr: compact_unwind::CompactUnwindManager,
18}
19
20unsafe extern "C" {
21    // libunwind import
22    fn __register_frame(fde: *const u8);
23    fn __deregister_frame(fde: *const u8);
24    fn wasmer_using_libunwind() -> bool;
25}
26
27// Apple-specific unwind functions - the following is taken from LLVM's libunwind itself.
28#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
29mod compact_unwind;
30
31/// There are two primary unwinders on Unix platforms: libunwind and libgcc.
32///
33/// Unfortunately their interface to `__register_frame` is different. The
34/// libunwind library takes a pointer to an individual FDE while libgcc takes a
35/// null-terminated list of FDEs. This means we need to know what unwinder
36/// is being used at runtime.
37///
38/// This detection is done currently by looking for a libunwind-specific symbol.
39/// This specific symbol was somewhat recommended by LLVM's
40/// "RTDyldMemoryManager.cpp" file which says:
41///
42/// > We use the presence of __unw_add_dynamic_fde to detect libunwind.
43///
44/// I'll note that there's also a different libunwind project at
45/// https://www.nongnu.org/libunwind/ but that doesn't appear to have
46/// `__register_frame` so I don't think that interacts with this.
47#[allow(dead_code)]
48fn using_libunwind() -> bool {
49    // dlsym (e.g. on musl target) cannot distinguish reliably LLVM libunwind and the libgcc
50    // if the target binary is statically linked. Probe a
51    // weak symbol in C so the final link determines the registration ABI.
52    //
53    // On macOS the libgcc interface is never used so libunwind is always used.
54
55    cfg!(target_os = "macos") || unsafe { wasmer_using_libunwind() }
56}
57
58impl UnwindRegistry {
59    /// Creates a new unwind registry with the given base address.
60    pub fn new() -> Self {
61        // Register atexit handler that will tell us if exit has been called.
62        static INIT: Once = Once::new();
63        INIT.call_once(|| unsafe {
64            let result = libc::atexit(atexit_handler);
65            assert_eq!(result, 0, "libc::atexit must succeed");
66        });
67        assert!(
68            !EXIT_CALLED.load(Ordering::SeqCst),
69            "Cannot register unwind information during the process exit"
70        );
71
72        Self {
73            published: false,
74            #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
75            registrations: Vec::new(),
76            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
77            compact_unwind_mgr: Default::default(),
78        }
79    }
80
81    /// Registers a function given the start offset, length, and unwind information.
82    pub fn register(
83        &mut self,
84        _base_address: usize,
85        _func_start: u32,
86        _func_len: u32,
87        info: &CompiledFunctionUnwindInfoReference,
88    ) -> Result<(), String> {
89        match info {
90            CompiledFunctionUnwindInfoReference::Dwarf => {}
91            _ => return Err(format!("unsupported unwind information {info:?}")),
92        };
93        Ok(())
94    }
95
96    /// Publishes all registered functions (coming from .eh_frame sections).
97    #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
98    pub fn publish_eh_frame(&mut self, eh_frame: Option<&[u8]>) -> Result<(), String> {
99        if self.published {
100            return Err("unwind registry has already been published".to_string());
101        }
102
103        unsafe {
104            if let Some(eh_frame) = eh_frame {
105                self.register_eh_frames(eh_frame);
106            }
107        }
108
109        self.published = true;
110
111        Ok(())
112    }
113
114    #[allow(clippy::cast_ptr_alignment)]
115    #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
116    unsafe fn register_eh_frames(&mut self, eh_frame: &[u8]) {
117        {
118            // Validate that the `.eh_frame` is well-formed before registering it.
119            // See https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html for more details.
120            // We put the frame records into a vector before registering them, because
121            // calling `__register_frame` with invalid data can cause segfaults.
122
123            // Pointers to the registrations that will be registered with `__register_frame`.
124            // For libgcc based systems, these are CIEs.
125            // For libunwind based systems, these are FDEs.
126            let mut records_to_register = Vec::new();
127
128            let mut current = 0;
129            let mut last_len = 0;
130            let using_libunwind = using_libunwind();
131            while current <= (eh_frame.len() - size_of::<u32>()) {
132                // If a CFI or a FDE starts with 0u32 it is a terminator.
133                let len = u32::from_ne_bytes(eh_frame[current..(current + 4)].try_into().unwrap());
134                if len == 0 {
135                    current += size_of::<u32>();
136                    last_len = 0;
137                    continue;
138                }
139                // The first record after a terminator is always a CIE.
140                let is_cie = last_len == 0;
141                last_len = len;
142                let record = eh_frame.as_ptr() as usize + current;
143                current = current + len as usize + 4;
144
145                if using_libunwind {
146                    // For libunwind based systems, `__register_frame` takes a pointer to an FDE.
147                    if !is_cie {
148                        // Every record that's not a CIE is an FDE.
149                        records_to_register.push(record);
150                    }
151                } else {
152                    // For libgcc based systems, `__register_frame` takes a pointer to a CIE.
153                    if is_cie {
154                        records_to_register.push(record);
155                    }
156                }
157            }
158
159            assert_eq!(
160                last_len, 0,
161                "The last record in the `.eh_frame` must be a terminator (but it actually has length {last_len})"
162            );
163            assert_eq!(
164                current,
165                eh_frame.len(),
166                "The `.eh_frame` must be finished after the last record",
167            );
168
169            for record in records_to_register {
170                // Register the CFI with libgcc
171                unsafe {
172                    __register_frame(record as *const u8);
173                }
174                self.registrations.push(record);
175            }
176        }
177    }
178
179    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
180    pub(crate) fn publish_compact_unwind(
181        &mut self,
182        compact_unwind: &[u8],
183        eh_personality_addr_in_got: Option<usize>,
184    ) -> Result<(), String> {
185        if self.published {
186            return Err("unwind registry has already been published".to_string());
187        }
188
189        unsafe {
190            self.compact_unwind_mgr.read_compact_unwind_section(
191                compact_unwind.as_ptr() as _,
192                compact_unwind.len(),
193                eh_personality_addr_in_got,
194            )?;
195            self.compact_unwind_mgr
196                .finalize()
197                .map_err(|v| v.to_string())?;
198            self.compact_unwind_mgr.register();
199        }
200
201        self.published = true;
202        Ok(())
203    }
204}
205
206/// Global flag indicating whether the program exit has been initiated.
207/// Set to true by an atexit handler to prevent crashes during shutdown
208/// when deregistering unwind frames. Accesses use `Ordering::SeqCst`
209/// to ensure correct memory ordering across threads.
210pub static EXIT_CALLED: AtomicBool = AtomicBool::new(false);
211
212extern "C" fn atexit_handler() {
213    EXIT_CALLED.store(true, Ordering::SeqCst);
214}
215
216impl Drop for UnwindRegistry {
217    fn drop(&mut self) {
218        if self.published {
219            #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
220            // libgcc stores the frame entries as a linked list in decreasing sort order
221            // based on the PC value of the registered entry.
222            //
223            // As we store the registrations in increasing order, it would be O(N^2) to
224            // deregister in that order.
225            //
226            // To ensure that we just pop off the first element in the list upon every
227            // deregistration, walk our list of registrations backwards.
228            for registration in self.registrations.iter().rev() {
229                // We don't want to deregister frames in UnwindRegistry::Drop as that could be called during
230                // program shutdown and can collide with release_registered_frames and lead to
231                // crashes.
232                if EXIT_CALLED.load(Ordering::SeqCst) {
233                    return;
234                }
235
236                unsafe {
237                    __deregister_frame(*registration as *const _);
238                }
239            }
240
241            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
242            {
243                if EXIT_CALLED.load(Ordering::SeqCst) {
244                    return;
245                }
246                self.compact_unwind_mgr.deregister();
247            }
248        }
249    }
250}