soroban_env_host_zephyr/host/
error.rs

1use crate::{
2    budget::AsBudget,
3    events::Events,
4    xdr::{self, LedgerKey, ScAddress, ScError, ScErrorCode, ScErrorType},
5    ConversionError, EnvBase, Error, Host, TryFromVal, U32Val, Val,
6};
7
8#[cfg(any(test, feature = "testutils"))]
9use backtrace::{Backtrace, BacktraceFrame};
10
11use core::fmt::Debug;
12use std::{
13    cell::{Ref, RefCell, RefMut},
14    ops::DerefMut,
15};
16
17use super::metered_clone::MeteredClone;
18
19#[derive(Clone)]
20pub(crate) struct DebugInfo {
21    events: Events,
22    #[cfg(any(test, feature = "testutils"))]
23    backtrace: Backtrace,
24}
25
26#[derive(Clone)]
27pub struct HostError {
28    pub error: Error,
29    pub(crate) info: Option<Box<DebugInfo>>,
30}
31
32impl std::error::Error for HostError {}
33
34impl Into<Error> for HostError {
35    fn into(self) -> Error {
36        self.error
37    }
38}
39
40impl DebugInfo {
41    fn write_events(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
42        // TODO: maybe make this something users can adjust?
43        // https://github.com/stellar/rs-soroban-env/issues/1288
44        const MAX_EVENTS: usize = 25;
45        let mut wrote_heading = false;
46        for (i, e) in self.events.0.iter().rev().take(MAX_EVENTS).enumerate() {
47            if !wrote_heading {
48                writeln!(f)?;
49                writeln!(f, "Event log (newest first):")?;
50                wrote_heading = true;
51            }
52            writeln!(f, "   {}: {}", i, e)?;
53        }
54        if self.events.0.len() > MAX_EVENTS {
55            writeln!(
56                f,
57                "   {}: ... {} events elided ...",
58                MAX_EVENTS,
59                self.events.0.len() - MAX_EVENTS
60            )?;
61        }
62        Ok(())
63    }
64
65    #[cfg(not(any(test, feature = "testutils")))]
66    fn write_backtrace(&self, _f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        Ok(())
68    }
69
70    #[cfg(any(test, feature = "testutils"))]
71    fn write_backtrace(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        // We do a little trimming here, skipping the first two frames (which
73        // are always into, from, and one or more Host::err_foo calls) and all
74        // the frames _after_ the short-backtrace marker that rust compiles-in.
75
76        fn frame_name_matches(frame: &BacktraceFrame, pat: &str) -> bool {
77            for sym in frame.symbols() {
78                match sym.name() {
79                    Some(sn) if format!("{:}", sn).contains(pat) => {
80                        return true;
81                    }
82                    _ => (),
83                }
84            }
85            false
86        }
87
88        fn frame_is_short_backtrace_start(frame: &BacktraceFrame) -> bool {
89            frame_name_matches(frame, "__rust_begin_short_backtrace")
90        }
91
92        fn frame_is_initial_error_plumbing(frame: &BacktraceFrame) -> bool {
93            frame_name_matches(frame, "::from")
94                || frame_name_matches(frame, "::into")
95                || frame_name_matches(frame, "host::err")
96                || frame_name_matches(frame, "Host::err")
97                || frame_name_matches(frame, "Host>::err")
98                || frame_name_matches(frame, "::augment_err_result")
99                || frame_name_matches(frame, "::with_shadow_mode")
100                || frame_name_matches(frame, "::with_debug_mode")
101                || frame_name_matches(frame, "::maybe_get_debug_info")
102                || frame_name_matches(frame, "::map_err")
103        }
104        let mut bt = self.backtrace.clone();
105        bt.resolve();
106        let frames: Vec<BacktraceFrame> = bt
107            .frames()
108            .iter()
109            .skip_while(|f| frame_is_initial_error_plumbing(f))
110            .take_while(|f| !frame_is_short_backtrace_start(f))
111            .cloned()
112            .collect();
113        let bt: Backtrace = frames.into();
114        writeln!(f)?;
115        writeln!(f, "Backtrace (newest first):")?;
116        writeln!(f, "{:?}", bt)
117    }
118}
119
120impl Debug for HostError {
121    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122        writeln!(f, "HostError: {:?}", self.error)?;
123        if let Some(info) = &self.info {
124            info.write_events(f)?;
125            info.write_backtrace(f)
126        } else {
127            writeln!(f, "DebugInfo not available")
128        }
129    }
130}
131
132impl HostError {
133    #[cfg(any(test, feature = "testutils"))]
134    pub fn result_matches_err<T, C>(res: Result<T, HostError>, code: C) -> bool
135    where
136        Error: From<C>,
137    {
138        match res {
139            Ok(_) => {
140                eprintln!("result is not an error");
141                false
142            }
143            Err(he) => {
144                let error: Error = code.into();
145                if he.error != error {
146                    eprintln!(
147                        "expected error != actual error: {:?} != {:?}",
148                        error, he.error
149                    );
150                }
151                he.error == error
152            }
153        }
154    }
155
156    /// Identifies whether the error can be meaningfully recovered from.
157    ///
158    /// We consider errors that occur due to broken execution preconditions (
159    /// such as incorrect footprint) non-recoverable.
160    pub fn is_recoverable(&self) -> bool {
161        // All internal errors that originate from the host can be considered
162        // non-recoverable (they should only appear if there is some bug in the
163        // host implementation or setup).
164        if !self.error.is_type(ScErrorType::Contract)
165            && self.error.is_code(ScErrorCode::InternalError)
166        {
167            return false;
168        }
169        // Exceeding the budget or storage limit is non-recoverable. Exceeding
170        // storage 'limit' is basically accessing entries outside of the
171        // supplied footprint.
172        if self.error.is_code(ScErrorCode::ExceededLimit)
173            && (self.error.is_type(ScErrorType::Storage) || self.error.is_type(ScErrorType::Budget))
174        {
175            return false;
176        }
177
178        true
179    }
180}
181
182impl<T> From<T> for HostError
183where
184    Error: From<T>,
185{
186    fn from(error: T) -> Self {
187        let error = error.into();
188        Self { error, info: None }
189    }
190}
191
192impl std::fmt::Display for HostError {
193    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
194        <HostError as Debug>::fmt(self, f)
195    }
196}
197
198impl TryFrom<&HostError> for ScError {
199    type Error = xdr::Error;
200    fn try_from(err: &HostError) -> Result<Self, Self::Error> {
201        err.error.try_into()
202    }
203}
204
205impl From<HostError> for std::io::Error {
206    fn from(e: HostError) -> Self {
207        std::io::Error::new(std::io::ErrorKind::Other, e)
208    }
209}
210
211pub(crate) trait TryBorrowOrErr<T> {
212    fn try_borrow_or_err(&self) -> Result<Ref<'_, T>, Error>;
213    fn try_borrow_mut_or_err(&self) -> Result<RefMut<'_, T>, Error>;
214    fn try_borrow_or_err_with(&self, host: &Host, msg: &str) -> Result<Ref<'_, T>, HostError> {
215        self.try_borrow_or_err()
216            .map_err(|e| host.error(e, msg, &[]))
217    }
218    fn try_borrow_mut_or_err_with(
219        &self,
220        host: &Host,
221        msg: &str,
222    ) -> Result<RefMut<'_, T>, HostError> {
223        self.try_borrow_mut_or_err()
224            .map_err(|e| host.error(e, msg, &[]))
225    }
226}
227
228impl<T> TryBorrowOrErr<T> for RefCell<T> {
229    fn try_borrow_or_err(&self) -> Result<Ref<'_, T>, Error> {
230        self.try_borrow().map_err(|_| {
231            Error::from_type_and_code(ScErrorType::Context, ScErrorCode::InternalError)
232        })
233    }
234
235    fn try_borrow_mut_or_err(&self) -> Result<RefMut<'_, T>, Error> {
236        self.try_borrow_mut().map_err(|_| {
237            Error::from_type_and_code(ScErrorType::Context, ScErrorCode::InternalError)
238        })
239    }
240}
241
242impl Host {
243    /// Convenience function to construct an [Error] and pass to [Host::error].
244    pub(crate) fn err(
245        &self,
246        type_: ScErrorType,
247        code: ScErrorCode,
248        msg: &str,
249        args: &[Val],
250    ) -> HostError {
251        let error = Error::from_type_and_code(type_, code);
252        self.error(error, msg, args)
253    }
254
255    /// At minimum constructs and returns a [HostError] built from the provided
256    /// [Error], and when running in [DiagnosticMode::Debug] additionally
257    /// records a diagnostic event with the provided `msg` and `args` and then
258    /// enriches the returned [Error] with [DebugInfo] in the form of a
259    /// [Backtrace] and snapshot of the [Events] buffer.
260    pub fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError {
261        let mut he = HostError::from(error);
262        self.with_debug_mode(|| {
263            // We _try_ to take a mutable borrow of the events buffer refcell
264            // while building up the event we're going to emit into the events
265            // log, failing gracefully (just emitting a no-debug-info
266            // `HostError` wrapping the supplied `Error`) if we cannot acquire
267            // the refcell. This is to handle the "double fault" case where we
268            // get an error _while performing_ any of the steps needed to record
269            // an error as an event, below.
270            if let Ok(mut events_refmut) = self.0.events.try_borrow_mut() {
271                self.record_err_diagnostics(events_refmut.deref_mut(), error, msg, args);
272            }
273            he = HostError {
274                error,
275                info: self.maybe_get_debug_info(),
276            };
277            Ok(())
278        });
279        he
280    }
281
282    pub(crate) fn maybe_get_debug_info(&self) -> Option<Box<DebugInfo>> {
283        #[allow(unused_mut)]
284        let mut res = None;
285        // DebugInfo should never even be _possible_ to turn on in a production
286        // environment. It does not contribute to the diagnostics emitted in the
287        // debug stream -- those happen elsewhere -- DebugInfo only exists for
288        // users doing local testing to get nice backtraces on their console.
289        #[cfg(any(test, feature = "testutils"))]
290        {
291            self.with_debug_mode(|| {
292                if let Ok(events_ref) = self.0.events.try_borrow() {
293                    let events = events_ref.externalize(self)?;
294                    let backtrace = Backtrace::new_unresolved();
295                    res = Some(Box::new(DebugInfo { backtrace, events }));
296                }
297                Ok(())
298            });
299        }
300        res
301    }
302
303    // Some common error patterns here.
304
305    pub(crate) fn err_arith_overflow(&self) -> HostError {
306        self.err(
307            ScErrorType::Value,
308            ScErrorCode::ArithDomain,
309            "arithmetic overflow",
310            &[],
311        )
312    }
313
314    pub(crate) fn err_oob_linear_memory(&self) -> HostError {
315        self.err(
316            ScErrorType::WasmVm,
317            ScErrorCode::IndexBounds,
318            "out-of-bounds access to WASM linear memory",
319            &[],
320        )
321    }
322
323    pub(crate) fn err_oob_object_index(&self, index: Option<u32>) -> HostError {
324        let type_ = ScErrorType::Object;
325        let code = ScErrorCode::IndexBounds;
326        let msg = "object index out of bounds";
327        match index {
328            None => self.err(type_, code, msg, &[]),
329            Some(index) => self.err(type_, code, msg, &[U32Val::from(index).to_val()]),
330        }
331    }
332
333    /// Given a result carrying some error type that can be converted to an
334    /// [Error] and supports [core::fmt::Debug], calls [Host::error] with the
335    /// error when there's an error, also passing the result of
336    /// [core::fmt::Debug::fmt] when [Host::is_debug] is `true`. Returns a
337    /// [Result] over [HostError].
338    ///
339    /// If you have an error type `T` you want to record as a detailed debug
340    /// event and a less-detailed [Error] code embedded in a [HostError], add an
341    /// `impl From<T> for Error` over in `soroban_env_common::error`, or in the
342    /// module defining `T`, and call this where the error is generated.
343    ///
344    /// Note: we do _not_ want to `impl From<T> for HostError` for such types,
345    /// as doing so will avoid routing them through the host in order to record
346    /// their extended diagnostic information into the event log. This means you
347    /// will wind up writing `host.map_err(...)?` a bunch in code that you used
348    /// to be able to get away with just writing `...?`, there's no way around
349    /// this if we want to record the diagnostic information.
350    pub(crate) fn map_err<T, E>(&self, res: Result<T, E>) -> Result<T, HostError>
351    where
352        Error: From<E>,
353        E: Debug,
354    {
355        res.map_err(|e| {
356            use std::borrow::Cow;
357            let mut msg: Cow<'_, str> = Cow::Borrowed(&"");
358            // This observes the debug state, but it only causes a different
359            // (richer) string to be logged as a diagnostic event, which
360            // is itself not observable outside the debug state.
361            self.with_debug_mode(|| {
362                msg = Cow::Owned(format!("{:?}", e));
363                Ok(())
364            });
365            self.error(e.into(), &msg, &[])
366        })
367    }
368
369    // Extracts the account id from the given ledger key as address object `Val`.
370    // Returns Void for unsupported entries.
371    // Useful as a helper for error reporting.
372    pub(crate) fn account_address_from_key(&self, lk: &LedgerKey) -> Result<Val, HostError> {
373        let account_id = match lk {
374            LedgerKey::Account(e) => &e.account_id,
375            LedgerKey::Trustline(e) => &e.account_id,
376            _ => {
377                return Ok(Val::VOID.into());
378            }
379        };
380        self.add_host_object(ScAddress::Account(
381            account_id.metered_clone(self.as_budget())?,
382        ))
383        .map(|a| a.to_val())
384    }
385}
386
387pub(crate) trait DebugArg {
388    fn debug_arg(host: &Host, arg: &Self) -> Val {
389        // We similarly guard against double-faulting here by try-acquiring the
390        // event buffer, which will fail if we're re-entering error reporting
391        // _while_ forming a debug argument.
392        let mut val: Option<Val> = None;
393        if let Ok(_guard) = host.0.events.try_borrow_mut() {
394            host.with_debug_mode(|| {
395                if let Ok(v) = Self::debug_arg_maybe_expensive_or_fallible(host, arg) {
396                    val = Some(v);
397                }
398                Ok(())
399            });
400            val.unwrap_or_else(|| {
401                Error::from_type_and_code(ScErrorType::Events, ScErrorCode::InternalError).into()
402            })
403        } else {
404            Error::from_type_and_code(ScErrorType::Events, ScErrorCode::InternalError).into()
405        }
406    }
407    fn debug_arg_maybe_expensive_or_fallible(host: &Host, arg: &Self) -> Result<Val, HostError>;
408}
409
410impl<T> DebugArg for T
411where
412    Val: TryFromVal<Host, T>,
413    HostError: From<<Val as TryFromVal<Host, T>>::Error>,
414{
415    fn debug_arg_maybe_expensive_or_fallible(host: &Host, arg: &Self) -> Result<Val, HostError> {
416        Val::try_from_val(host, arg).map_err(|e| HostError::from(e))
417    }
418}
419
420impl DebugArg for xdr::Hash {
421    fn debug_arg_maybe_expensive_or_fallible(host: &Host, arg: &Self) -> Result<Val, HostError> {
422        host.bytes_new_from_slice(arg.as_slice()).map(|b| b.into())
423    }
424}
425
426impl DebugArg for str {
427    fn debug_arg_maybe_expensive_or_fallible(host: &Host, arg: &Self) -> Result<Val, HostError> {
428        host.string_new_from_slice(arg.as_bytes()).map(|s| s.into())
429    }
430}
431
432impl DebugArg for usize {
433    fn debug_arg_maybe_expensive_or_fallible(_host: &Host, arg: &Self) -> Result<Val, HostError> {
434        u32::try_from(*arg)
435            .map(|x| U32Val::from(x).into())
436            .map_err(|_| HostError::from(ConversionError))
437    }
438}
439
440/// Helper for building multi-argument errors.
441/// For example:
442/// ```ignore
443/// err!(host, error, "message", arg1, arg2);
444/// ```
445/// All arguments must be convertible to [Val] with [TryIntoVal]. This is
446/// expected to be called from within a function that returns
447/// `Result<_, HostError>`. If these requirements can't be fulfilled, use
448/// the [Host::error] function directly.
449#[macro_export]
450macro_rules! err {
451    ($host:expr, $error:expr, $msg:literal, $($args:expr),*) => {
452        {
453            const fn voidarg(_: &'static str) -> $crate::Val {
454                $crate::Val::VOID.to_val()
455            }
456            // The stringify and voidarg calls here exist just to cause the
457            // macro to stack-allocate a fixed-size local array with one VOID
458            // initializer per argument. The stringified-arguments themselves
459            // are not actually used at this point, they exist to have a macro
460            // expression that corresponds to the number of arguments.
461            let mut buf = [$(voidarg(stringify!($args))),*];
462            let mut i = 0;
463            $host.with_debug_mode(||{
464                $(
465                    // Args actually get used here, where we fill in array cells..
466                    buf[i] = <_ as $crate::host::error::DebugArg>::debug_arg($host, &$args);
467                    // .. and extend the end-index of the args-slice we'll pass.
468                    i += 1;
469                )*
470                Ok(())
471            });
472            $host.error($error.into(), $msg, &buf[0..i])
473        }
474    };
475}