wasmtime/runtime/vm/sys/unix/unwind.rs
1//! Module for System V ABI unwind registry.
2
3use crate::prelude::*;
4use crate::runtime::vm::SendSyncPtr;
5use crate::sync::OnceLock;
6use core::ptr::NonNull;
7
8type UnwindFn = unsafe extern "C" fn(usize);
9
10/// Represents a registration of function unwind information for System V ABI.
11pub enum UnwindRegistration {
12 /// libgcc registers the whole table with one call to `__register_frame`.
13 SingleFrame(SendSyncPtr<u8>),
14 /// Older libunwind versions register each FDE separately.
15 MultipleFrames(TryVec<SendSyncPtr<u8>>),
16 /// Newer libunwind versions can register an entire `.eh_frame` section.
17 LibunwindSection {
18 section: SendSyncPtr<u8>,
19 deregister: UnwindFn,
20 },
21}
22
23// Keep this layout in sync with `struct Libunwind` in helpers.c.
24#[repr(C)]
25#[derive(Default)]
26struct Libunwind {
27 add_dynamic_fde: Option<UnwindFn>,
28 add_dynamic_eh_frame_section: Option<UnwindFn>,
29 remove_dynamic_eh_frame_section: Option<UnwindFn>,
30}
31
32cfg_select! {
33 // FIXME: at least on the `gcc-arm-linux-gnueabihf` toolchain on Ubuntu
34 // these symbols are not provided by default like they are on other targets.
35 // I'm not ARM expert so I don't know why. For now though consider this an
36 // optional integration feature with the platform and stub out the functions
37 // to do nothing which won't break any tests it just means that
38 // runtime-generated backtraces won't have the same level of fidelity they
39 // do on other targets.
40 target_arch = "arm" => {
41 unsafe extern "C" fn __register_frame(_: *const u8) {}
42 unsafe extern "C" fn __deregister_frame(_: *const u8) {}
43 unsafe fn wasmtime_libunwind() -> Libunwind {
44 Libunwind::default()
45 }
46 }
47 _ => {
48 unsafe extern "C" {
49 fn __register_frame(fde: *const u8);
50 fn __deregister_frame(fde: *const u8);
51 #[wasmtime_versioned_export_macros::versioned_link]
52 fn wasmtime_libunwind() -> Libunwind;
53 }
54 }
55}
56
57impl Libunwind {
58 fn get() -> &'static Self {
59 static LIBUNWIND: OnceLock<Libunwind> = OnceLock::new();
60 LIBUNWIND.get_or_init(|| {
61 // SAFETY: the C helper returns nullable function pointers with the
62 // signatures and layout declared above.
63 let api = unsafe { wasmtime_libunwind() };
64 #[cfg(target_os = "macos")]
65 let api = {
66 let mut api = api;
67 if let Some((add, remove)) = load_unwind_section_api() {
68 api.add_dynamic_eh_frame_section = Some(add);
69 api.remove_dynamic_eh_frame_section = Some(remove);
70 }
71 api
72 };
73 api
74 })
75 }
76
77 /// There are two primary unwinders on Unix platforms: libunwind and libgcc.
78 ///
79 /// Unfortunately their interface to `__register_frame` is different. The
80 /// libunwind library takes a pointer to an individual FDE while libgcc takes a
81 /// null-terminated list of FDEs. This means we need to know what unwinder
82 /// is being used at runtime.
83 ///
84 /// This detection is done currently by looking for a libunwind-specific symbol.
85 /// This specific symbol was somewhat recommended by LLVM's
86 /// "RTDyldMemoryManager.cpp" file which says:
87 ///
88 /// > We use the presence of __unw_add_dynamic_fde to detect libunwind.
89 ///
90 /// I'll note that there's also a different libunwind project at
91 /// https://www.nongnu.org/libunwind/ but that doesn't appear to have
92 /// `__register_frame` so I don't think that interacts with this.
93 fn is_libunwind(&self) -> bool {
94 // macOS always uses the libunwind convention.
95 cfg!(target_os = "macos") || self.add_dynamic_fde.is_some()
96 }
97}
98
99#[cfg(target_os = "macos")]
100fn load_unwind_section_api() -> Option<(UnwindFn, UnwindFn)> {
101 // Weak imports tolerate symbols missing at runtime, but the linker still
102 // needs their exports in the build SDK. The macOS 11.3 SDK has the older
103 // __unw_add_dynamic_fde export but lacks the section APIs, so resolve those
104 // at runtime to keep building with older SDKs.
105 //
106 // SAFETY: dladdr initializes info on success. The returned path is a C
107 // string owned by the loaded image containing our linked registration API.
108 unsafe {
109 let mut info = core::mem::MaybeUninit::<libc::Dl_info>::uninit();
110 if libc::dladdr((__register_frame as *const ()).cast(), info.as_mut_ptr()) == 0 {
111 return None;
112 }
113 let info = info.assume_init();
114 if info.dli_fname.is_null() {
115 return None;
116 }
117
118 // Use the same provider as the linked individual-FDE API. RTLD_FIRST
119 // prevents borrowing a missing API from a dependency's separate registry.
120 let lib = libc::dlopen(
121 info.dli_fname,
122 libc::RTLD_LAZY | libc::RTLD_LOCAL | libc::RTLD_FIRST,
123 );
124 if lib.is_null() {
125 return None;
126 }
127
128 // Search overrides or interposition can change which library is opened.
129 if libc::dlsym(lib, c"__register_frame".as_ptr())
130 != __register_frame as *const () as *mut libc::c_void
131 || libc::dlsym(lib, c"__deregister_frame".as_ptr())
132 != __deregister_frame as *const () as *mut libc::c_void
133 {
134 libc::dlclose(lib);
135 return None;
136 }
137 let add = libc::dlsym(lib, c"__unw_add_dynamic_eh_frame_section".as_ptr());
138 let remove = libc::dlsym(lib, c"__unw_remove_dynamic_eh_frame_section".as_ptr());
139 if add.is_null() || remove.is_null() {
140 libc::dlclose(lib);
141 return None;
142 }
143
144 // Both symbols have the UnwindFn ABI. Keep the library handle open for
145 // the lifetime of the function pointers cached in Libunwind::get.
146 Some((
147 core::mem::transmute::<*mut libc::c_void, UnwindFn>(add),
148 core::mem::transmute::<*mut libc::c_void, UnwindFn>(remove),
149 ))
150 }
151}
152
153impl UnwindRegistration {
154 pub const SECTION_NAME: &'static str = ".eh_frame";
155
156 /// Registers precompiled unwinding information with the system.
157 ///
158 /// The `_base_address` field is ignored here (only used on other
159 /// platforms), but the `unwind_info` and `unwind_len` parameters should
160 /// describe an in-memory representation of a `.eh_frame` section. This is
161 /// typically arranged for by the `wasmtime-obj` crate.
162 pub unsafe fn new(
163 _base_address: *const u8,
164 unwind_info: *const u8,
165 unwind_len: usize,
166 ) -> Result<UnwindRegistration> {
167 #[cfg(has_virtual_memory)]
168 debug_assert_eq!(
169 unwind_info as usize % crate::runtime::vm::host_page_size(),
170 0,
171 "The unwind info must always be aligned to a page"
172 );
173
174 let api = Libunwind::get();
175 let info = SendSyncPtr::new(NonNull::new(unwind_info.cast_mut()).unwrap());
176 unsafe {
177 if !api.is_libunwind() {
178 // libgcc walks the table until an entry of length zero.
179 __register_frame(unwind_info);
180 return Ok(Self::SingleFrame(info));
181 }
182
183 // Require both APIs so removal uses the same registration grouping.
184 if let (Some(add), Some(remove)) = (
185 api.add_dynamic_eh_frame_section,
186 api.remove_dynamic_eh_frame_section,
187 ) {
188 add(unwind_info as usize);
189 return Ok(Self::LibunwindSection {
190 section: info,
191 deregister: remove,
192 });
193 }
194
195 // libunwind's __register_frame takes a single FDE. Our .eh_frame
196 // section ends with a four-byte zero terminator.
197 let mut registration = Self::MultipleFrames(TryVec::new());
198 let Self::MultipleFrames(frames) = &mut registration else {
199 unreachable!();
200 };
201 let end = unwind_info.add(unwind_len - 4);
202 let mut current = unwind_info;
203 while current < end {
204 let len = current.cast::<u32>().read_unaligned() as usize;
205 // Skip the CIE at the beginning of the section.
206 if current != unwind_info {
207 // Record the pointer before registering it, so an allocation
208 // failure unregisters all previous entries via Drop.
209 frames.push(SendSyncPtr::new(NonNull::new(current.cast_mut()).unwrap()))?;
210 __register_frame(current);
211 }
212 current = current.add(len + 4);
213 }
214 Ok(registration)
215 }
216 }
217}
218
219impl Drop for UnwindRegistration {
220 fn drop(&mut self) {
221 unsafe {
222 match self {
223 Self::SingleFrame(frame) => __deregister_frame(frame.as_ptr()),
224 Self::MultipleFrames(frames) => {
225 // Preserve reverse registration order when removing FDEs.
226 for frame in frames.iter().rev() {
227 __deregister_frame(frame.as_ptr());
228 }
229 }
230 Self::LibunwindSection {
231 section,
232 deregister,
233 } => {
234 deregister(section.as_ptr() as usize);
235 }
236 }
237 }
238 }
239}