Skip to content

Commit 34059b6

Browse files
Chandhana Solainathanmeta-codesync[bot]
authored andcommitted
telemetry: Add backtrace_ffi Rust FFI crate
Summary: Adds a Rust FFI crate for cross-platform stack trace capture using the backtrace library. Captures raw frames at throw time and defers symbolization until the trace is actually needed. Internal infrastructure frames are stripped so the trace starts at the throw site. Reviewed By: vilatto Differential Revision: D102213373 fbshipit-source-id: 617b31ead31ec9555473a1be87281d84bf446741
1 parent ae383c0 commit 34059b6

4 files changed

Lines changed: 210 additions & 0 deletions

File tree

eden/fs/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ members = [
8585
"inodes/overlay/clients",
8686
"inodes/overlay/mocks",
8787
"inodes/overlay/services",
88+
"rust/backtrace_ffi",
8889
"rust/edenfs-asserted-states",
8990
"rust/edenfs-asserted-states-client",
9091
"rust/manifold_ffi",

eden/fs/rust/backtrace_ffi/BUCK

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
load("@fbsource//tools/build_defs:rust_library.bzl", "rust_library")
2+
3+
oncall("scm_client_infra")
4+
5+
rust_library(
6+
name = "backtrace-ffi",
7+
srcs = glob(["src/**/*.rs"]),
8+
crate_root = "src/lib.rs",
9+
cxx_bridge = "src/lib.rs",
10+
deps = [
11+
"fbsource//third-party/rust:backtrace",
12+
"fbsource//third-party/rust:cxx",
13+
],
14+
)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# @generated by autocargo from //eden/fs/rust/backtrace_ffi:backtrace-ffi
2+
3+
[package]
4+
name = "backtrace-ffi"
5+
version = "0.1.0"
6+
authors = ["Facebook Source Control Team <sourcecontrol-dev@fb.com>"]
7+
edition = "2024"
8+
license = "GPLv2+"
9+
10+
[dependencies]
11+
backtrace = "0.3"
12+
cxx = "1.0.119"
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This software may be used and distributed according to the terms of the
5+
* GNU General Public License version 2.
6+
*/
7+
8+
use std::cell::Cell;
9+
use std::cell::RefCell;
10+
use std::fmt::Write;
11+
12+
#[cxx::bridge(namespace = "facebook::eden")]
13+
mod ffi {
14+
extern "Rust" {
15+
/// Capture a backtrace (frames only, no symbolization).
16+
/// Stores the result in thread-local storage.
17+
fn capture_backtrace(max_depth: usize);
18+
19+
/// Check if a backtrace has been captured in the current thread.
20+
fn has_captured_trace() -> bool;
21+
22+
/// Symbolize the captured backtrace and return a formatted string.
23+
/// Strips infrastructure frames above the throw hook so the trace
24+
/// starts at the actual throw site.
25+
/// Clears the stored trace after symbolization.
26+
fn symbolize_captured_trace() -> String;
27+
28+
/// Clear any stored backtrace without symbolizing.
29+
fn clear_captured_trace();
30+
}
31+
}
32+
33+
thread_local! {
34+
static CAPTURED: RefCell<Option<backtrace::Backtrace>> = const { RefCell::new(None) };
35+
static CAPTURING: Cell<bool> = const { Cell::new(false) };
36+
}
37+
38+
struct CapturingGuard;
39+
40+
impl CapturingGuard {
41+
fn new() -> Option<Self> {
42+
CAPTURING.with(|c| {
43+
if c.get() {
44+
return None;
45+
}
46+
c.set(true);
47+
Some(CapturingGuard)
48+
})
49+
}
50+
}
51+
52+
impl Drop for CapturingGuard {
53+
fn drop(&mut self) {
54+
CAPTURING.with(|c| c.set(false));
55+
}
56+
}
57+
58+
fn capture_backtrace(max_depth: usize) {
59+
let Some(_guard) = CapturingGuard::new() else {
60+
return;
61+
};
62+
let mut frames = Vec::with_capacity(max_depth);
63+
backtrace::trace(|frame| {
64+
if frames.len() >= max_depth {
65+
return false;
66+
}
67+
frames.push(backtrace::BacktraceFrame::from(frame.clone()));
68+
true
69+
});
70+
let bt = backtrace::Backtrace::from(frames);
71+
CAPTURED.with(|slot| *slot.borrow_mut() = Some(bt));
72+
}
73+
74+
fn has_captured_trace() -> bool {
75+
CAPTURED.with(|slot| slot.borrow().is_some())
76+
}
77+
78+
fn clear_captured_trace() {
79+
CAPTURED.with(|slot| *slot.borrow_mut() = None);
80+
}
81+
82+
// Each platform uses a different function to throw C++ exceptions.
83+
// We scan for these names to find the throw site in the backtrace
84+
// and strip all infrastructure frames above it.
85+
// Windows uses CxxThrowException,
86+
// Linux uses __wrap___cxa_throw (via --wrap linker flag),
87+
// macOS overrides __cxa_throw directly (via dlsym RTLD_NEXT).
88+
fn is_throw_hook(name: &[u8]) -> bool {
89+
let s = match std::str::from_utf8(name) {
90+
Ok(s) => s,
91+
Err(_) => return false,
92+
};
93+
if cfg!(target_os = "windows") {
94+
s.contains("CxxThrowException")
95+
} else {
96+
s.contains("__cxa_throw")
97+
}
98+
}
99+
100+
// Symbolize captured frames, strip infrastructure frames above the throw
101+
// hook, format remaining frames, and clear the stored trace.
102+
fn symbolize_captured_trace() -> String {
103+
let Some(mut bt) = CAPTURED.with(|slot| slot.borrow_mut().take()) else {
104+
return String::new();
105+
};
106+
107+
bt.resolve();
108+
let frames = bt.frames();
109+
110+
let mut hook_idx = None;
111+
'outer: for (i, frame) in frames.iter().enumerate() {
112+
for symbol in frame.symbols() {
113+
if let Some(name) = symbol.name() {
114+
if is_throw_hook(name.as_bytes()) {
115+
hook_idx = Some(i);
116+
break 'outer;
117+
}
118+
}
119+
}
120+
}
121+
122+
let start = hook_idx.map_or(0, |i| i + 1);
123+
let mut result = String::new();
124+
for (i, frame) in frames[start..].iter().enumerate() {
125+
for symbol in frame.symbols() {
126+
let name = symbol.name();
127+
let file = symbol.filename().map(|f| f.to_string_lossy());
128+
let line = symbol.lineno();
129+
match (name, file, line) {
130+
(Some(n), Some(f), Some(l)) => {
131+
let _ = writeln!(result, "#{i} {n} at {f}:{l}");
132+
}
133+
(Some(n), Some(f), None) => {
134+
let _ = writeln!(result, "#{i} {n} at {f}");
135+
}
136+
(Some(n), None, _) => {
137+
let _ = writeln!(result, "#{i} {n}");
138+
}
139+
(None, _, _) => {
140+
let _ = writeln!(result, "#{i} <unknown>");
141+
}
142+
}
143+
}
144+
}
145+
result
146+
}
147+
148+
#[cfg(test)]
149+
mod tests {
150+
use super::*;
151+
152+
#[test]
153+
fn test_is_throw_hook_matching() {
154+
if cfg!(target_os = "windows") {
155+
assert!(is_throw_hook(b"CxxThrowException"));
156+
assert!(!is_throw_hook(b"__cxa_throw"));
157+
} else {
158+
assert!(is_throw_hook(b"__cxa_throw"));
159+
assert!(is_throw_hook(b"__wrap___cxa_throw"));
160+
assert!(!is_throw_hook(b"CxxThrowException"));
161+
}
162+
}
163+
164+
#[test]
165+
fn test_is_throw_hook_non_matching() {
166+
assert!(!is_throw_hook(b"std::runtime_error"));
167+
assert!(!is_throw_hook(b"main"));
168+
assert!(!is_throw_hook(b""));
169+
}
170+
171+
#[test]
172+
fn test_is_throw_hook_invalid_utf8() {
173+
assert!(!is_throw_hook(&[0xff, 0xfe]));
174+
}
175+
176+
#[test]
177+
fn test_capture_max_depth_zero() {
178+
capture_backtrace(0);
179+
assert!(has_captured_trace());
180+
let trace = symbolize_captured_trace();
181+
assert!(trace.is_empty());
182+
}
183+
}

0 commit comments

Comments
 (0)