Skip to content

Commit 8d4d002

Browse files
authored
transmute fatal errors into SYS_ASSERTION_FAILED exit code. (#548)
1 parent c4f4acb commit 8d4d002

File tree

9 files changed

+312
-64
lines changed

9 files changed

+312
-64
lines changed

fvm/src/call_manager/backtrace.rs

Lines changed: 73 additions & 23 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
}
@@ -85,34 +100,69 @@ impl Display for Frame {
85100

86101
/// The ultimate "cause" of a failed message.
87102
#[derive(Clone, Debug)]
88-
pub struct Cause {
89-
/// The syscall "module".
90-
pub module: &'static str,
91-
/// The syscall function name.
92-
pub function: &'static str,
93-
/// The exact syscall error.
94-
pub error: ErrorNumber,
95-
/// The informational syscall message.
96-
pub message: String,
103+
pub enum Cause {
104+
/// The original cause was a syscall error.
105+
Syscall {
106+
/// The syscall "module".
107+
module: &'static str,
108+
/// The syscall function name.
109+
function: &'static str,
110+
/// The exact syscall error.
111+
error: ErrorNumber,
112+
/// The informational syscall message.
113+
message: String,
114+
},
115+
/// The original cause was a fatal error.
116+
Fatal {
117+
/// The alternate-formatted message from the anyhow error.
118+
error_msg: 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+
backtrace: String,
122+
},
97123
}
98124

99125
impl Cause {
100-
pub fn new(module: &'static str, function: &'static str, err: SyscallError) -> Self {
101-
Self {
126+
/// Records a failing syscall as the cause of a backtrace.
127+
pub fn from_syscall(module: &'static str, function: &'static str, err: SyscallError) -> Self {
128+
Self::Syscall {
102129
module,
103130
function,
104131
error: err.1,
105132
message: err.0,
106133
}
107134
}
135+
136+
/// Records a fatal error as the cause of a backtrace.
137+
pub fn from_fatal(err: anyhow::Error) -> Self {
138+
Self::Fatal {
139+
error_msg: format!("{:#}", err),
140+
backtrace: err.backtrace().to_string(),
141+
}
142+
}
108143
}
109144

110145
impl Display for Cause {
111146
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-
)
147+
match self {
148+
Cause::Syscall {
149+
module,
150+
function,
151+
error,
152+
message,
153+
} => {
154+
write!(
155+
f,
156+
"{}::{} -- {} ({}: {})",
157+
module, function, &message, *error as u32, error,
158+
)
159+
}
160+
Cause::Fatal {
161+
error_msg,
162+
backtrace,
163+
} => {
164+
write!(f, "[FATAL] Error: {}, Backtrace:\n{}", error_msg, backtrace)
165+
}
166+
}
117167
}
118168
}

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: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,22 +130,36 @@ 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-
return Err(e.context(format!(
142-
"[from={}, to={}, seq={}, m={}, h={}] fatal error",
140+
Err(ExecutionError::Fatal(err)) => {
141+
// We produce a receipt with SYS_ASSERTION_FAILED exit code, and
142+
// we consume the full gas amount so that, in case of a network-
143+
// wide fatal errors, all nodes behave deterministically.
144+
//
145+
// We set the backtrace from the fatal error to aid diagnosis.
146+
// Note that we use backtrace#set_cause instead of backtrace#begin
147+
// because we want to retain the propagation chain that we've
148+
// accumulated on the way out.
149+
let err = err.context(format!(
150+
"[from={}, to={}, seq={}, m={}, h={}]",
143151
msg.from,
144152
msg.to,
145153
msg.sequence,
146154
msg.method_num,
147-
self.context().epoch
148-
)));
155+
self.context().epoch,
156+
));
157+
backtrace.set_cause(backtrace::Cause::from_fatal(err));
158+
Receipt {
159+
exit_code: ExitCode::SYS_ASSERTION_FAILED,
160+
return_data: Default::default(),
161+
gas_used: msg.gas_limit,
162+
}
149163
}
150164
};
151165

fvm/src/machine/default.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ where
113113
// This interface works for now because we know all actor CIDs
114114
// ahead of time, but with user-supplied code, we won't have that
115115
// guarantee.
116+
// Skip preloading all builtin actors when testing. This results in JIT
117+
// bytecode to machine code compilation, and leads to faster tests.
118+
#[cfg(not(feature = "testing"))]
116119
engine.preload(state_tree.store(), builtin_actors.left_values())?;
117120

118121
Ok(DefaultMachine {

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/examples/integration.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use fvm::executor::{ApplyKind, Executor};
22
use fvm_integration_tests::tester::{Account, Tester};
3+
use fvm_ipld_blockstore::MemoryBlockstore;
34
use fvm_ipld_encoding::tuple::*;
45
use fvm_shared::address::Address;
56
use fvm_shared::bigint::BigInt;
@@ -25,7 +26,12 @@ struct State {
2526

2627
pub fn main() {
2728
// Instantiate tester
28-
let mut tester = Tester::new(NetworkVersion::V15, StateTreeVersion::V4).unwrap();
29+
let mut tester = Tester::new(
30+
NetworkVersion::V15,
31+
StateTreeVersion::V4,
32+
MemoryBlockstore::default(),
33+
)
34+
.unwrap();
2935

3036
let sender: [Account; 1] = tester.create_accounts().unwrap();
3137

testing/integration/src/builtin.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use cid::Cid;
55
use futures::executor::block_on;
66
use fvm::state_tree::{ActorState, StateTree};
77
use fvm::{init_actor, system_actor};
8-
use fvm_ipld_blockstore::{Blockstore, MemoryBlockstore};
8+
use fvm_ipld_blockstore::Blockstore;
99
use fvm_ipld_car::load_car;
1010
use fvm_ipld_encoding::CborStore;
1111
use fvm_shared::actor::builtin::{load_manifest, Type};
@@ -24,7 +24,7 @@ const BUNDLES: [(NetworkVersion, &[u8]); 3] = [
2424

2525
// Import built-in actors
2626
pub fn import_builtin_actors(
27-
blockstore: &MemoryBlockstore,
27+
blockstore: &impl Blockstore,
2828
) -> Result<BTreeMap<NetworkVersion, Cid>> {
2929
BUNDLES
3030
.into_iter()
@@ -40,7 +40,7 @@ pub fn import_builtin_actors(
4040

4141
// Retrieve system, init and accounts actors code CID
4242
pub fn fetch_builtin_code_cid(
43-
blockstore: &MemoryBlockstore,
43+
blockstore: &impl Blockstore,
4444
builtin_actors: &Cid,
4545
ver: u32,
4646
) -> Result<(Cid, Cid, Cid)> {

0 commit comments

Comments
 (0)