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