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}