1use moire_trace_types::{
2 BacktraceId, BacktraceRecord, InvariantError, ModuleId, ModulePath, RuntimeBase,
3};
4use std::error::Error;
5use std::fmt;
6use std::num::NonZeroUsize;
7use std::sync::Once;
8
9#[derive(Debug, Clone, Copy)]
10pub struct CaptureOptions {
11 pub max_frames: NonZeroUsize,
12 pub skip_frames: usize,
13}
14
15impl Default for CaptureOptions {
16 fn default() -> Self {
17 Self {
18 max_frames: NonZeroUsize::new(256)
19 .expect("invariant violated: default max_frames must be non-zero"),
20 skip_frames: 0,
21 }
22 }
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct CapturedModule {
27 pub id: ModuleId,
28 pub path: ModulePath,
29 pub runtime_base: RuntimeBase,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct CapturedBacktrace {
34 pub backtrace: BacktraceRecord,
35 pub modules: Vec<CapturedModule>,
36}
37
38#[derive(Debug)]
39pub enum CaptureError {
40 UnsupportedPlatform {
41 target_os: &'static str,
42 },
43 EmptyBacktrace,
44 MissingModuleInfo {
45 ip: u64,
46 },
47 MissingModulePath {
48 ip: u64,
49 },
50 ZeroModuleBase {
51 ip: u64,
52 },
53 IpBeforeModuleBase {
54 ip: u64,
55 module_base: RuntimeBase,
56 },
57 InvariantViolation {
58 context: &'static str,
59 source: InvariantError,
60 },
61}
62
63impl CaptureError {
64 fn invariant(context: &'static str, source: InvariantError) -> Self {
65 Self::InvariantViolation { context, source }
66 }
67}
68
69impl fmt::Display for CaptureError {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 match self {
72 Self::UnsupportedPlatform { target_os } => {
73 write!(
74 f,
75 "unsupported platform for trace capture backend: {target_os}; only Unix targets are implemented"
76 )
77 }
78 Self::EmptyBacktrace => write!(
79 f,
80 "invariant violated: captured backtrace must be non-empty"
81 ),
82 Self::MissingModuleInfo { ip } => {
83 write!(
84 f,
85 "invariant violated: dladdr returned no module info for ip=0x{ip:x}"
86 )
87 }
88 Self::MissingModulePath { ip } => {
89 write!(
90 f,
91 "invariant violated: module path is required for ip=0x{ip:x}"
92 )
93 }
94 Self::ZeroModuleBase { ip } => {
95 write!(
96 f,
97 "invariant violated: module base must be non-zero for ip=0x{ip:x}"
98 )
99 }
100 Self::IpBeforeModuleBase { ip, module_base } => {
101 write!(
102 f,
103 "invariant violated: instruction pointer 0x{ip:x} is below module base 0x{:x}",
104 module_base.get()
105 )
106 }
107 Self::InvariantViolation { context, source } => {
108 write!(f, "invariant violated in {context}: {source}")
109 }
110 }
111 }
112}
113
114impl Error for CaptureError {
115 fn source(&self) -> Option<&(dyn Error + 'static)> {
116 match self {
117 Self::InvariantViolation { source, .. } => Some(source),
118 _ => None,
119 }
120 }
121}
122
123static FRAME_POINTER_VALIDATION_ONCE: Once = Once::new();
124
125pub fn validate_frame_pointers_or_panic() {
127 FRAME_POINTER_VALIDATION_ONCE.call_once(|| {
128 if let Err(reason) = platform::validate_frame_pointers_impl() {
129 panic!(
130 "frame-pointer validation failed: {reason}. \
131recompile with -C force-frame-pointers=yes"
132 );
133 }
134 });
135}
136
137pub fn capture_current(
139 backtrace_id: BacktraceId,
140 options: CaptureOptions,
141) -> Result<CapturedBacktrace, CaptureError> {
142 platform::capture_current_impl(backtrace_id, options)
143}
144
145#[cfg(unix)]
146mod platform {
147 use super::{CaptureError, CaptureOptions, CapturedBacktrace, CapturedModule};
148 use moire_trace_types::{
149 BacktraceId, BacktraceRecord, FrameKey, ModuleId, ModulePath, RelPc, RuntimeBase,
150 };
151 use std::collections::BTreeMap;
152 use std::ffi::{CStr, c_void};
153 use std::sync::{Mutex as StdMutex, OnceLock};
154
155 pub fn validate_frame_pointers_impl() -> Result<(), String> {
156 #[inline(never)]
157 fn layer0() -> Result<(), String> {
158 layer1()
159 }
160 #[inline(never)]
161 fn layer1() -> Result<(), String> {
162 layer2()
163 }
164 #[inline(never)]
165 fn layer2() -> Result<(), String> {
166 layer3()
167 }
168 #[inline(never)]
169 fn layer3() -> Result<(), String> {
170 layer4()
171 }
172 #[inline(never)]
173 fn layer4() -> Result<(), String> {
174 validate_frame_pointer_chain(6)
175 }
176
177 layer0()
178 }
179
180 fn validate_frame_pointer_chain(min_depth: usize) -> Result<(), String> {
181 let mut frame_ptr = read_frame_pointer()?;
182 if frame_ptr == 0 {
183 return Err("current frame pointer is null".to_string());
184 }
185
186 let mut prev_frame_ptr = 0usize;
187 let mut depth = 0usize;
188 const MAX_FRAMES: usize = 4096;
189
190 for _ in 0..MAX_FRAMES {
191 if frame_ptr == 0 {
192 break;
193 }
194
195 if frame_ptr % std::mem::align_of::<usize>() != 0 {
196 return Err(format!("misaligned frame pointer 0x{frame_ptr:x}"));
197 }
198
199 if prev_frame_ptr != 0 && frame_ptr <= prev_frame_ptr {
200 return Err(format!(
201 "frame pointer did not increase: current=0x{frame_ptr:x}, previous=0x{prev_frame_ptr:x}"
202 ));
203 }
204
205 let next_frame_ptr = unsafe { *(frame_ptr as *const usize) };
206 depth += 1;
207 if next_frame_ptr == 0 {
208 break;
209 }
210
211 prev_frame_ptr = frame_ptr;
212 frame_ptr = next_frame_ptr;
213 }
214
215 if depth < min_depth {
216 return Err(format!(
217 "frame pointer chain too shallow: got {depth}, need at least {min_depth}"
218 ));
219 }
220
221 Ok(())
222 }
223
224 #[cfg(target_arch = "x86_64")]
225 fn read_frame_pointer() -> Result<usize, String> {
226 let frame_ptr: usize;
227 unsafe {
228 core::arch::asm!(
229 "mov {}, rbp",
230 out(reg) frame_ptr,
231 options(nomem, nostack, preserves_flags)
232 );
233 }
234 Ok(frame_ptr)
235 }
236
237 #[cfg(target_arch = "aarch64")]
238 fn read_frame_pointer() -> Result<usize, String> {
239 let frame_ptr: usize;
240 unsafe {
241 core::arch::asm!(
242 "mov {}, x29",
243 out(reg) frame_ptr,
244 options(nomem, nostack, preserves_flags)
245 );
246 }
247 Ok(frame_ptr)
248 }
249
250 #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
251 fn read_frame_pointer() -> Result<usize, String> {
252 Err(format!(
253 "unsupported architecture for frame pointer validation: {}",
254 std::env::consts::ARCH
255 ))
256 }
257
258 pub fn capture_current_impl(
260 backtrace_id: BacktraceId,
261 options: CaptureOptions,
262 ) -> Result<CapturedBacktrace, CaptureError> {
263 let raw_ips = collect_raw_ips(options)?;
264
265 if raw_ips.is_empty() {
266 return Err(CaptureError::EmptyBacktrace);
267 }
268
269 let mut modules_by_key: BTreeMap<(RuntimeBase, String), ModuleId> = BTreeMap::new();
270 let mut modules = Vec::new();
271 let mut frames = Vec::with_capacity(raw_ips.len());
272 for ip in raw_ips {
273 let module = module_info_for_ip(ip)?;
274
275 let key = (module.runtime_base, module.path.clone());
276 let module_id = if let Some(module_id) = modules_by_key.get(&key).copied() {
277 module_id
278 } else {
279 let module_id =
280 ModuleId::next().map_err(|err| CaptureError::invariant("module_id", err))?;
281 let module_path = ModulePath::new(module.path)
282 .map_err(|err| CaptureError::invariant("module_path", err))?;
283
284 modules.push(CapturedModule {
285 id: module_id,
286 path: module_path,
287 runtime_base: module.runtime_base,
288 });
289
290 modules_by_key.insert(key, module_id);
291 module_id
292 };
293
294 if ip < module.runtime_base.get() {
295 return Err(CaptureError::IpBeforeModuleBase {
296 ip,
297 module_base: module.runtime_base,
298 });
299 }
300 let rel_pc = RelPc::new(ip - module.runtime_base.get())
301 .map_err(|err| CaptureError::invariant("rel_pc", err))?;
302
303 frames.push(FrameKey { module_id, rel_pc });
304 }
305
306 let backtrace = BacktraceRecord::new(backtrace_id, frames)
307 .map_err(|err| CaptureError::invariant("backtrace_record", err))?;
308
309 Ok(CapturedBacktrace { backtrace, modules })
310 }
311
312 fn collect_raw_ips(options: CaptureOptions) -> Result<Vec<u64>, CaptureError> {
313 let mut raw_ips = Vec::new();
314 let mut skip_remaining = options.skip_frames;
315 let mut frame_ptr =
316 read_frame_pointer().map_err(|_| CaptureError::UnsupportedPlatform {
317 target_os: std::env::consts::OS,
318 })?;
319
320 while frame_ptr != 0 && raw_ips.len() < options.max_frames.get() {
321 if frame_ptr % std::mem::align_of::<usize>() != 0 {
322 break;
323 }
324
325 let next_frame_ptr = unsafe { *(frame_ptr as *const usize) };
326 let return_ip = unsafe { *((frame_ptr as *const usize).add(1)) };
327
328 if return_ip != 0 {
329 if skip_remaining > 0 {
330 skip_remaining -= 1;
331 } else {
332 raw_ips.push(return_ip as u64);
333 }
334 }
335
336 if next_frame_ptr == 0 || next_frame_ptr <= frame_ptr {
337 break;
338 }
339
340 frame_ptr = next_frame_ptr;
341 }
342
343 Ok(raw_ips)
344 }
345
346 #[derive(Debug, Clone)]
347 struct RawModuleInfo {
348 runtime_base: RuntimeBase,
349 path: String,
350 }
351
352 fn module_info_cache() -> &'static StdMutex<BTreeMap<u64, RawModuleInfo>> {
353 static CACHE: OnceLock<StdMutex<BTreeMap<u64, RawModuleInfo>>> = OnceLock::new();
354 CACHE.get_or_init(|| StdMutex::new(BTreeMap::new()))
355 }
356
357 fn module_info_for_ip(ip: u64) -> Result<RawModuleInfo, CaptureError> {
358 let cached = {
359 let Ok(cache) = module_info_cache().lock() else {
360 panic!("module info cache mutex poisoned; cannot continue");
361 };
362 cache.get(&ip).cloned()
363 };
364 if let Some(info) = cached {
365 return Ok(info);
366 }
367
368 let resolved = resolve_module_info_for_ip(ip)?;
369
370 let Ok(mut cache) = module_info_cache().lock() else {
371 panic!("module info cache mutex poisoned; cannot continue");
372 };
373 cache.insert(ip, resolved.clone());
374 Ok(resolved)
375 }
376
377 fn resolve_module_info_for_ip(ip: u64) -> Result<RawModuleInfo, CaptureError> {
378 let mut info = std::mem::MaybeUninit::<libc::Dl_info>::zeroed();
379 let ok = unsafe { libc::dladdr(ip as usize as *const c_void, info.as_mut_ptr()) };
380 if ok == 0 {
381 return Err(CaptureError::MissingModuleInfo { ip });
382 }
383
384 let info = unsafe { info.assume_init() };
385 if info.dli_fbase.is_null() {
386 return Err(CaptureError::ZeroModuleBase { ip });
387 }
388
389 let runtime_base = RuntimeBase::new(info.dli_fbase as usize as u64)
390 .map_err(|err| CaptureError::invariant("runtime_base", err))?;
391
392 if info.dli_fname.is_null() {
393 return Err(CaptureError::MissingModulePath { ip });
394 }
395
396 let path = unsafe { CStr::from_ptr(info.dli_fname) }
397 .to_string_lossy()
398 .into_owned();
399 if path.is_empty() {
400 return Err(CaptureError::MissingModulePath { ip });
401 }
402
403 Ok(RawModuleInfo { runtime_base, path })
404 }
405}
406
407#[cfg(not(unix))]
408mod platform {
409 use super::{CaptureError, CaptureOptions, CapturedBacktrace};
410 use moire_trace_types::BacktraceId;
411
412 pub fn validate_frame_pointers_impl() -> Result<(), String> {
413 Err(format!(
414 "unsupported platform for trace capture backend: {}",
415 std::env::consts::OS
416 ))
417 }
418
419 pub fn capture_current_impl(
420 _backtrace_id: BacktraceId,
421 _options: CaptureOptions,
422 ) -> Result<CapturedBacktrace, CaptureError> {
423 Err(CaptureError::UnsupportedPlatform {
424 target_os: std::env::consts::OS,
425 })
426 }
427}