Skip to content

Commit 168dd9c

Browse files
committed
add support for propagating backtraces on fatal errors.
1 parent 7a0bfc1 commit 168dd9c

File tree

5 files changed

+97
-37
lines changed

5 files changed

+97
-37
lines changed

fvm/src/call_manager/backtrace.rs

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ use fvm_shared::{ActorID, MethodNum};
77

88
use crate::kernel::SyscallError;
99

10-
/// A call backtrace records _why_ an actor exited with a specific error code.
10+
/// A call backtrace records the actors an error was propagated through, from
11+
/// the moment it was emitted. The original error is the _cause_. Backtraces are
12+
/// useful for identifying the root cause of an error.
1113
#[derive(Debug, Default, Clone)]
1214
pub struct Backtrace {
1315
/// The actors through which this error was propagated from bottom (source) to top.
@@ -34,22 +36,35 @@ impl Backtrace {
3436
self.frames.is_empty() && self.cause.is_none()
3537
}
3638

37-
/// Clear the backtrace. This should be called:
38-
///
39-
/// 1. Before all syscalls except "abort"
40-
/// 2. After an actor returns with a 0 exit code.
39+
/// Clear the backtrace.
4140
pub fn clear(&mut self) {
4241
self.cause = None;
4342
self.frames.clear();
4443
}
4544

46-
/// Set the backtrace cause. If there is an existing backtrace, this will clear it.
47-
pub fn set_cause(&mut self, cause: Cause) {
45+
/// Begins a new backtrace. If there is an existing backtrace, this will clear it.
46+
///
47+
/// Note: Backtraces are populated _backwards_. That is, a frame is inserted
48+
/// every time an actor returns. That's why `begin()` resets any currently
49+
/// accumulated state, as once an error occurs, we want to track its
50+
/// propagation all the way up.
51+
pub fn begin(&mut self, cause: Cause) {
4852
self.cause = Some(cause);
4953
self.frames.clear();
5054
}
5155

56+
/// Sets the cause of a backtrace.
57+
///
58+
/// This is useful to stamp a backtrace with its cause after the frames
59+
/// have been collected, such as when we ultimately handle a fatal error at
60+
/// the top of its propagation chain.
61+
pub fn set_cause(&mut self, cause: Cause) {
62+
self.cause = Some(cause);
63+
}
64+
5265
/// Push a "frame" (actor exit) onto the backtrace.
66+
///
67+
/// This should be called every time an actor exits.
5368
pub fn push_frame(&mut self, frame: Frame) {
5469
self.frames.push(frame)
5570
}
@@ -83,9 +98,8 @@ impl Display for Frame {
8398
}
8499
}
85100

86-
/// The ultimate "cause" of a failed message.
87101
#[derive(Clone, Debug)]
88-
pub struct Cause {
102+
pub struct SyscallCause {
89103
/// The syscall "module".
90104
pub module: &'static str,
91105
/// The syscall function name.
@@ -96,23 +110,64 @@ pub struct Cause {
96110
pub message: String,
97111
}
98112

113+
#[derive(Clone, Debug)]
114+
pub struct FatalCause {
115+
/// The error message from the error.
116+
pub error_msg: String,
117+
/// The original cause that initiated the error chain.
118+
pub root_cause: String,
119+
/// The backtrace, captured if the relevant
120+
/// [environment variables](https://doc.rust-lang.org/std/backtrace/index.html#environment-variables) are enabled.
121+
pub backtrace: String,
122+
}
123+
124+
/// The ultimate "cause" of a failed message.
125+
#[derive(Clone, Debug)]
126+
pub enum Cause {
127+
/// The original cause was a syscall error.
128+
Syscall(SyscallCause),
129+
/// The original cause was a fatal error.
130+
Fatal(FatalCause),
131+
}
132+
99133
impl Cause {
100-
pub fn new(module: &'static str, function: &'static str, err: SyscallError) -> Self {
101-
Self {
134+
/// Records a failing syscall as the cause of a backtrace.
135+
pub fn from_syscall(module: &'static str, function: &'static str, err: SyscallError) -> Self {
136+
Self::Syscall(SyscallCause {
102137
module,
103138
function,
104139
error: err.1,
105140
message: err.0,
106-
}
141+
})
142+
}
143+
144+
/// Records a fatal error as the cause of a backtrace.
145+
pub fn from_fatal(err: anyhow::Error) -> Self {
146+
Self::Fatal(FatalCause {
147+
error_msg: err.to_string(),
148+
root_cause: err.root_cause().to_string(),
149+
backtrace: err.backtrace().to_string(),
150+
})
107151
}
108152
}
109153

110154
impl Display for Cause {
111155
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112-
write!(
113-
f,
114-
"{}::{} -- {} ({}: {})",
115-
self.module, self.function, &self.message, self.error as u32, self.error,
116-
)
156+
match self {
157+
Cause::Syscall(cause) => {
158+
write!(
159+
f,
160+
"{}::{} -- {} ({}: {})",
161+
cause.module, cause.function, &cause.message, cause.error as u32, cause.error,
162+
)
163+
}
164+
Cause::Fatal(msg) => {
165+
write!(
166+
f,
167+
"[FATAL] Root cause: {}, Stacktrace: {}",
168+
msg.root_cause, msg.backtrace
169+
)
170+
}
171+
}
117172
}
118173
}

fvm/src/call_manager/default.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ where
393393
Ok(value) => Ok(InvocationResult::Return(value)),
394394
Err(abort) => {
395395
if let Some(err) = last_error {
396-
cm.backtrace.set_cause(err);
396+
cm.backtrace.begin(err);
397397
}
398398

399399
let (code, message, res) = match abort {

fvm/src/executor/default.rs

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -130,24 +130,29 @@ where
130130
ErrorNumber::Forbidden => ExitCode::SYS_ASSERTION_FAILED,
131131
};
132132

133-
backtrace.set_cause(backtrace::Cause::new("send", "send", err));
133+
backtrace.begin(backtrace::Cause::from_syscall("send", "send", err));
134134
Receipt {
135135
exit_code,
136136
return_data: Default::default(),
137137
gas_used,
138138
}
139139
}
140-
Err(ExecutionError::Fatal(e)) => {
141-
// Annotate the error with the message context.
142-
let _err = e.context(format!(
143-
"[from={}, to={}, seq={}, m={}, h={}] fatal error",
144-
msg.from,
145-
msg.to,
146-
msg.sequence,
147-
msg.method_num,
148-
self.context().epoch
149-
));
150-
// TODO backtrace
140+
Err(ExecutionError::Fatal(err)) => {
141+
// // Annotate the error with the message context.
142+
// let err = {
143+
// let backtrace = String::from("foo"); //e.backtrace().to_string();
144+
// // e.context(format!(
145+
// // "[from={}, to={}, seq={}, m={}, h={}] fatal error; backtrace: {}",
146+
// // msg.from,
147+
// // msg.to,
148+
// // msg.sequence,
149+
// // msg.method_num,
150+
// // self.context().epoch,
151+
// // backtrace,
152+
// // ))
153+
// };
154+
backtrace.set_cause(backtrace::Cause::from_fatal(err));
155+
// Produce a receipt that consumes the full gas amount.
151156
Receipt {
152157
exit_code: ExitCode::SYS_ASSERTION_FAILED,
153158
return_data: Default::default(),

fvm/src/syscalls/bind.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ macro_rules! impl_bind_syscalls {
141141
Ok(Err(err)) => {
142142
let code = err.1;
143143
log::trace!("syscall {}::{}: fail ({})", module, name, code as u32);
144-
data.last_error = Some(backtrace::Cause::new(module, name, err));
144+
data.last_error = Some(backtrace::Cause::from_syscall(module, name, err));
145145
Ok(code as u32)
146146
},
147147
Err(e) => Err(e.into()),
@@ -163,7 +163,7 @@ macro_rules! impl_bind_syscalls {
163163
if (ret as u64) > (memory.len() as u64)
164164
|| memory.len() - (ret as usize) < mem::size_of::<Ret::Value>() {
165165
let code = ErrorNumber::IllegalArgument;
166-
data.last_error = Some(backtrace::Cause::new(module, name, SyscallError(format!("no space for return value"), code)));
166+
data.last_error = Some(backtrace::Cause::from_syscall(module, name, SyscallError(format!("no space for return value"), code)));
167167
return Ok(code as u32);
168168
}
169169

@@ -178,7 +178,7 @@ macro_rules! impl_bind_syscalls {
178178
Ok(Err(err)) => {
179179
let code = err.1;
180180
log::trace!("syscall {}::{}: fail ({})", module, name, code as u32);
181-
data.last_error = Some(backtrace::Cause::new(module, name, err));
181+
data.last_error = Some(backtrace::Cause::from_syscall(module, name, err));
182182
Ok(code as u32)
183183
},
184184
Err(e) => Err(e.into()),

testing/integration/tests/lib.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ fn out_of_stack() {
206206

207207
#[test]
208208
fn backtraces() {
209+
// Note: this test **does not actually assert anything**, but it's useful to
210+
// let us peep into FVM backtrace generation under different scenarios.
209211
const WAT_ABORT: &str = r#"
210212
(module
211213
;; ipld::open
@@ -300,8 +302,7 @@ fn backtraces() {
300302
.execute_message(message, ApplyKind::Explicit, 100)
301303
.unwrap();
302304

303-
println!("abort backtrace:");
304-
dbg!(res.failure_info);
305+
println!("abort backtrace: {}", res.failure_info.unwrap());
305306

306307
// Send message
307308
let message = Message {
@@ -320,8 +321,7 @@ fn backtraces() {
320321
.execute_message(message, ApplyKind::Explicit, 100)
321322
.unwrap();
322323

323-
println!("fatal backtrace:");
324-
dbg!(res.failure_info);
324+
println!("fatal backtrace: {}", res.failure_info.unwrap());
325325
}
326326

327327
#[derive(Default)]

0 commit comments

Comments
 (0)