Skip to main content

moire_trace_capture/
lib.rs

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
125// r[impl process.frame-pointer-validation]
126pub 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
137// r[impl process.backtrace-capture]
138pub 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    // r[impl process.backtrace-capture.impl]
259    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}