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::{AtomicUsize, Ordering::Relaxed};
7
8use crate::types::unwind::CompiledFunctionUnwindInfoReference;
9
10/// Represents a registry of function unwind information for System V ABI.
11pub struct UnwindRegistry {
12    registrations: Vec<usize>,
13    published: bool,
14    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
15    compact_unwind_mgr: compact_unwind::CompactUnwindManager,
16}
17
18unsafe extern "C" {
19    // libunwind import
20    fn __register_frame(fde: *const u8);
21    fn __deregister_frame(fde: *const u8);
22}
23
24// Apple-specific unwind functions - the following is taken from LLVM's libunwind itself.
25#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
26mod compact_unwind;
27
28/// There are two primary unwinders on Unix platforms: libunwind and libgcc.
29///
30/// Unfortunately their interface to `__register_frame` is different. The
31/// libunwind library takes a pointer to an individual FDE while libgcc takes a
32/// null-terminated list of FDEs. This means we need to know what unwinder
33/// is being used at runtime.
34///
35/// This detection is done currently by looking for a libunwind-specific symbol.
36/// This specific symbol was somewhat recommended by LLVM's
37/// "RTDyldMemoryManager.cpp" file which says:
38///
39/// > We use the presence of __unw_add_dynamic_fde to detect libunwind.
40///
41/// I'll note that there's also a different libunwind project at
42/// https://www.nongnu.org/libunwind/ but that doesn't appear to have
43/// `__register_frame` so I don't think that interacts with this.
44#[allow(dead_code)]
45fn using_libunwind() -> bool {
46    static USING_LIBUNWIND: AtomicUsize = AtomicUsize::new(LIBUNWIND_UNKNOWN);
47
48    const LIBUNWIND_UNKNOWN: usize = 0;
49    const LIBUNWIND_YES: usize = 1;
50    const LIBUNWIND_NO: usize = 2;
51
52    // On macOS the libgcc interface is never used so libunwind is always used.
53    if cfg!(target_os = "macos") {
54        return true;
55    }
56
57    // On other platforms the unwinder can vary. Sometimes the unwinder is
58    // selected at build time and sometimes it differs at build time and runtime
59    // (or at least I think that's possible). Fall back to a `libc::dlsym` to
60    // figure out what we're using and branch based on that.
61    //
62    // Note that the result of `libc::dlsym` is cached to only look this up
63    // once.
64    match USING_LIBUNWIND.load(Relaxed) {
65        LIBUNWIND_YES => true,
66        LIBUNWIND_NO => false,
67        LIBUNWIND_UNKNOWN => {
68            let looks_like_libunwind = unsafe {
69                !libc::dlsym(
70                    std::ptr::null_mut(),
71                    c"__unw_add_dynamic_fde".as_ptr().cast(),
72                )
73                .is_null()
74            };
75            USING_LIBUNWIND.store(
76                if looks_like_libunwind {
77                    LIBUNWIND_YES
78                } else {
79                    LIBUNWIND_NO
80                },
81                Relaxed,
82            );
83            looks_like_libunwind
84        }
85        _ => unreachable!(),
86    }
87}
88
89impl UnwindRegistry {
90    /// Creates a new unwind registry with the given base address.
91    pub fn new() -> Self {
92        Self {
93            registrations: Vec::new(),
94            published: false,
95            #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
96            compact_unwind_mgr: Default::default(),
97        }
98    }
99
100    /// Registers a function given the start offset, length, and unwind information.
101    pub fn register(
102        &mut self,
103        _base_address: usize,
104        _func_start: u32,
105        _func_len: u32,
106        info: &CompiledFunctionUnwindInfoReference,
107    ) -> Result<(), String> {
108        match info {
109            CompiledFunctionUnwindInfoReference::Dwarf => {}
110            _ => return Err(format!("unsupported unwind information {info:?}")),
111        };
112        Ok(())
113    }
114
115    /// Publishes all registered functions.
116    pub fn publish(&mut self, eh_frame: Option<&[u8]>) -> Result<(), String> {
117        if self.published {
118            return Err("unwind registry has already been published".to_string());
119        }
120
121        if let Some(eh_frame) = eh_frame {
122            unsafe {
123                self.register_frames(eh_frame);
124            }
125        }
126
127        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
128        {
129            self.compact_unwind_mgr
130                .finalize()
131                .map_err(|v| v.to_string())?;
132            self.compact_unwind_mgr.register();
133        }
134
135        self.published = true;
136
137        Ok(())
138    }
139
140    #[allow(clippy::cast_ptr_alignment)]
141    unsafe fn register_frames(&mut self, eh_frame: &[u8]) {
142        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
143        {
144            // Special call for macOS on aarch64 to register the `.eh_frame` section.
145            // TODO: I am not 100% sure if it's correct to never deregister the `.eh_frame` section. It was this way before
146            // I started working on this, so I kept it that way.
147            unsafe {
148                compact_unwind::__unw_add_dynamic_eh_frame_section(eh_frame.as_ptr() as usize);
149            }
150        }
151
152        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
153        {
154            // Validate that the `.eh_frame` is well-formed before registering it.
155            // See https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-Core-generic/LSB-Core-generic/ehframechpt.html for more details.
156            // We put the frame records into a vector before registering them, because
157            // calling `__register_frame` with invalid data can cause segfaults.
158
159            // Pointers to the registrations that will be registered with `__register_frame`.
160            // For libgcc based systems, these are CIEs.
161            // For libunwind based systems, these are FDEs.
162            let mut records_to_register = Vec::new();
163
164            let mut current = 0;
165            let mut last_len = 0;
166            while current <= (eh_frame.len() - size_of::<u32>()) {
167                // If a CFI or a FDE starts with 0u32 it is a terminator.
168                let len = u32::from_ne_bytes(eh_frame[current..(current + 4)].try_into().unwrap());
169                if len == 0 {
170                    current += size_of::<u32>();
171                    last_len = 0;
172                    continue;
173                }
174                // The first record after a terminator is always a CIE.
175                let is_cie = last_len == 0;
176                last_len = len;
177                let record = eh_frame.as_ptr() as usize + current;
178                current = current + len as usize + 4;
179
180                if using_libunwind() {
181                    // For libunwind based systems, `__register_frame` takes a pointer to an FDE.
182                    if !is_cie {
183                        // Every record that's not a CIE is an FDE.
184                        records_to_register.push(record);
185                    }
186                    continue;
187                }
188
189                // For libgcc based systems, `__register_frame` takes a pointer to a CIE.
190                if is_cie {
191                    records_to_register.push(record);
192                }
193            }
194
195            assert_eq!(
196                last_len, 0,
197                "The last record in the `.eh_frame` must be a terminator (but it actually has length {last_len})"
198            );
199            assert_eq!(
200                current,
201                eh_frame.len(),
202                "The `.eh_frame` must be finished after the last record",
203            );
204
205            for record in records_to_register {
206                // Register the CFI with libgcc
207                unsafe {
208                    __register_frame(record as *const u8);
209                }
210                self.registrations.push(record);
211            }
212        }
213    }
214
215    pub(crate) fn register_compact_unwind(
216        &mut self,
217        compact_unwind: Option<&[u8]>,
218        eh_personality_addr_in_got: Option<usize>,
219    ) -> Result<(), String> {
220        #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
221        unsafe {
222            if let Some(slice) = compact_unwind {
223                self.compact_unwind_mgr.read_compact_unwind_section(
224                    slice.as_ptr() as _,
225                    slice.len(),
226                    eh_personality_addr_in_got,
227                )?;
228            }
229        }
230
231        #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
232        {
233            _ = compact_unwind;
234            _ = eh_personality_addr_in_got;
235        }
236        Ok(())
237    }
238}
239
240impl Drop for UnwindRegistry {
241    fn drop(&mut self) {
242        if self.published {
243            unsafe {
244                // libgcc stores the frame entries as a linked list in decreasing sort order
245                // based on the PC value of the registered entry.
246                //
247                // As we store the registrations in increasing order, it would be O(N^2) to
248                // deregister in that order.
249                //
250                // To ensure that we just pop off the first element in the list upon every
251                // deregistration, walk our list of registrations backwards.
252                for registration in self.registrations.iter().rev() {
253                    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
254                    {
255                        compact_unwind::__unw_remove_dynamic_eh_frame_section(*registration);
256                        self.compact_unwind_mgr.deregister();
257                    }
258
259                    #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
260                    {
261                        __deregister_frame(*registration as *const _);
262                    }
263                }
264            }
265        }
266    }
267}