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