arti_rpc_client_core/ffi/
err.rs

1//! Error handling logic for our ffi code.
2
3use paste::paste;
4use std::error::Error as StdError;
5use std::ffi::{c_char, c_int, CStr};
6use std::fmt::Display;
7use std::io::Error as IoError;
8use std::panic::{catch_unwind, UnwindSafe};
9
10use crate::conn::ErrorResponse;
11use crate::util::Utf8CString;
12
13use super::util::{ffi_body_raw, OptOutPtrExt as _, OutPtr};
14use super::ArtiRpcStatus;
15
16/// Helper:
17/// Given a restricted enum defining FfiStatus, also define a series of constants for its variants,
18/// and a string conversion function.
19//
20// NOTE: I tried to use derive_deftly here, but ran into trouble when defining the constants.
21// I wanted to have them be "pub const ARTI_FOO = FfiStatus::$vname",
22// but that doesn't work with cbindgen, which won't expose a constant unless it is a public type
23// it can recognize.
24// There is no way to use derive_deftly to look at the explicit discriminant of an enum.
25macro_rules! define_ffi_status {
26    {
27        $(#[$tm:meta])*
28        pub(crate) enum FfiStatus {
29            $(
30                $(#[$m:meta])*
31                [$s:expr]
32                $id:ident = $e:expr,
33            )+
34        }
35
36    } => {paste!{
37        $(#[$tm])*
38        pub(crate) enum FfiStatus {
39            $(
40                $(#[$m])*
41                $id = $e,
42            )+
43        }
44
45        $(
46            $(#[$m])*
47            pub const [<ARTI_RPC_STATUS_ $id:snake:upper >] : ArtiRpcStatus = $e;
48        )+
49
50        /// Return a string representing the meaning of a given `ArtiRpcStatus`.
51        ///
52        /// The result will always be non-NULL, even if the status is unrecognized.
53        #[no_mangle]
54        pub extern "C" fn arti_rpc_status_to_str(status: ArtiRpcStatus) -> *const c_char {
55            match status {
56                $(
57                    [<ARTI_RPC_STATUS_ $id:snake:upper>] => $s,
58                )+
59                _ => c"(unrecognized status)",
60            }.as_ptr()
61        }
62    }}
63}
64
65define_ffi_status! {
66/// View of FFI status as rust enumeration.
67///
68/// Not exposed in the FFI interfaces, except via cast to ArtiStatus.
69///
70/// We define this as an enumeration so that we can treat it exhaustively in Rust.
71#[derive(Copy, Clone, Debug)]
72#[repr(u32)]
73pub(crate) enum FfiStatus {
74    /// The function has returned successfully.
75    #[allow(dead_code)]
76    [c"Success"]
77    Success = 0,
78
79    /// One or more of the inputs to a library function was invalid.
80    ///
81    /// (This error was generated by the library, before any request was sent.)
82    [c"Invalid input"]
83    InvalidInput = 1,
84
85    /// Tried to use some functionality
86    /// (for example, an authentication method or connection scheme)
87    /// that wasn't available on this platform or build.
88    ///
89    /// (This error was generated by the library, before any request was sent.)
90    [c"Not supported"]
91    NotSupported = 2,
92
93    /// Tried to connect to Arti, but an IO error occurred.
94    ///
95    /// This may indicate that Arti wasn't running,
96    /// or that Arti was built without RPC support,
97    /// or that Arti wasn't running at the specified location.
98    ///
99    /// (This error was generated by the library.)
100    [c"An IO error occurred while connecting to Arti"]
101    ConnectIo = 3,
102
103    /// We tried to authenticate with Arti, but it rejected our attempt.
104    ///
105    /// (This error was sent by the peer.)
106    [c"Authentication rejected"]
107    BadAuth = 4,
108
109    /// Our peer has, in some way, violated the Arti-RPC protocol.
110    ///
111    /// (This error was generated by the library,
112    /// based on a response from Arti that appeared to be invalid.)
113    [c"Peer violated the RPC protocol"]
114    PeerProtocolViolation = 5,
115
116    /// The peer has closed our connection; possibly because it is shutting down.
117    ///
118    /// (This error was generated by the library,
119    /// based on the connection being closed or reset from the peer.)
120    [c"Peer has shut down"]
121    Shutdown = 6,
122
123    /// An internal error occurred in the arti rpc client.
124    ///
125    /// (This error was generated by the library.
126    /// If you see it, there is probably a bug in the library.)
127    [c"Internal error; possible bug?"]
128    Internal = 7,
129
130    /// The peer reports that one of our requests has failed.
131    ///
132    /// (This error was sent by the peer, in response to one of our requests.
133    /// No further responses to that request will be received or accepted.)
134    [c"Request has failed"]
135    RequestFailed = 8,
136
137    /// Tried to check the status of a request and found that it was no longer running.
138    [c"Request has already completed (or failed)"]
139    RequestCompleted = 9,
140
141    /// An IO error occurred while trying to negotiate a data stream
142    /// using Arti as a proxy.
143    [c"IO error while connecting to Arti as a Proxy"]
144    ProxyIo = 10,
145
146    /// An attempt to negotiate a data stream through Arti failed,
147    /// with an error from the proxy protocol.
148    //
149    // TODO RPC: expose the actual error type; see #1580.
150    [c"Data stream failed"]
151    ProxyStreamFailed = 11,
152
153    /// Some operation failed because it was attempted on an unauthenticated channel.
154    ///
155    /// (At present (Sep 2024) there is no way to get an unauthenticated channel from this library,
156    /// but that may change in the future.)
157    [c"Not authenticated"]
158    NotAuthenticated = 12,
159
160    /// All of our attempts to connect to Arti failed,
161    /// or we reached an explicit instruction to "abort" our connection attempts.
162    [c"All attempts to connect to Arti RPC failed"]
163    AllConnectAttemptsFailed = 13,
164
165    /// We tried to connect to Arti at a given connect point,
166    /// but it could not be used:
167    /// either because we don't know how,
168    /// or because we were not able to access some necessary file or directory.
169    [c"Connect point was not usable"]
170    ConnectPointNotUsable = 14,
171
172    /// We were unable to parse or resolve an entry
173    /// in our connect point search path.
174    [c"Invalid connect point search path"]
175    BadConnectPointPath = 15,
176}
177}
178
179/// An error as returned by the Arti FFI code.
180#[derive(Debug, Clone)]
181pub struct FfiError {
182    /// The status of this error messages
183    pub(super) status: ArtiRpcStatus,
184    /// A human-readable message describing this error
185    message: Utf8CString,
186    /// If present, a Json-formatted message from our peer that we are representing with this error.
187    error_response: Option<ErrorResponse>,
188    /// If present, the OS error code that caused this error.
189    //
190    // (Actually, this should be RawOsError, but that type isn't stable.)
191    os_error_code: Option<i32>,
192}
193
194impl FfiError {
195    /// Helper: If this error stems from a response from our RPC peer,
196    /// return that response.
197    fn error_response_as_ptr(&self) -> Option<*const c_char> {
198        self.error_response.as_ref().map(|response| {
199            let cstr: &CStr = response.as_ref();
200            cstr.as_ptr()
201        })
202    }
203}
204
205/// Convenience trait to help implement `Into<FfiError>`
206///
207/// Any error that implements this trait will be convertible into an [`FfiError`].
208// additional requirements: display doesn't make NULs.
209pub(crate) trait IntoFfiError: Display + Sized {
210    /// Return the status
211    fn status(&self) -> FfiStatus;
212    /// Return this type as an Error, if it is one.
213    fn as_error(&self) -> Option<&(dyn StdError + 'static)>;
214    /// Return a message for this error.
215    ///
216    /// By default, uses the Display of this error, and of its sources, to build a string.
217    /// The format and content of this string is not specified, and is not guaranteed
218    /// to remain stable.
219    fn message(&self) -> String {
220        use tor_error::ErrorReport as _;
221        match self.as_error() {
222            Some(e) => {
223                let msg = e.report().to_string();
224                // Note: Having to strip the prefix here is somewhat annoying.
225                msg.strip_prefix("error: ")
226                    .map(str::to_string)
227                    .unwrap_or_else(|| msg)
228            }
229            None => self.to_string(),
230        }
231    }
232    /// Return the OS error code (if any) underlying this error.
233    ///
234    /// On unix-like platforms, this is an `errno`; on Windows, it's a
235    /// code from `GetLastError.`
236    fn os_error_code(&self) -> Option<i32> {
237        let mut err = self.as_error()?;
238
239        loop {
240            if let Some(io_error) = err.downcast_ref::<IoError>() {
241                return io_error.raw_os_error() as Option<i32>;
242            }
243            err = err.source()?;
244        }
245    }
246    /// Consume this error and return an [`ErrorResponse`]
247    fn into_error_response(self) -> Option<ErrorResponse> {
248        None
249    }
250}
251impl<T: IntoFfiError> From<T> for FfiError {
252    fn from(value: T) -> Self {
253        let status = value.status() as u32;
254        let message = value
255            .message()
256            .try_into()
257            .expect("Error message had a NUL?");
258        let os_error_code = value.os_error_code();
259        let error_response = value.into_error_response();
260        Self {
261            status,
262            message,
263            error_response,
264            os_error_code,
265        }
266    }
267}
268impl From<void::Void> for FfiError {
269    fn from(value: void::Void) -> Self {
270        void::unreachable(value)
271    }
272}
273
274/// Tried to call a ffi function with a not-permitted argument.
275#[derive(Clone, Debug, thiserror::Error)]
276pub(super) enum InvalidInput {
277    /// Tried to convert a NULL pointer to an FFI object.
278    #[error("Provided argument was NULL.")]
279    NullPointer,
280
281    /// Tried to convert a non-UTF string.
282    #[error("Provided string was not UTF-8")]
283    BadUtf8,
284
285    /// Tried to use an invalid port.
286    #[error("Port was not in range 1..65535")]
287    BadPort,
288
289    /// Tried to use an invalid constant
290    #[error("Provided constant was not recognized")]
291    InvalidConstValue,
292}
293
294impl From<void::Void> for InvalidInput {
295    fn from(value: void::Void) -> Self {
296        void::unreachable(value)
297    }
298}
299
300impl IntoFfiError for InvalidInput {
301    fn status(&self) -> FfiStatus {
302        FfiStatus::InvalidInput
303    }
304    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
305        Some(self)
306    }
307}
308
309impl IntoFfiError for crate::ConnectError {
310    fn status(&self) -> FfiStatus {
311        use crate::ConnectError as E;
312        use FfiStatus as F;
313        match self {
314            E::CannotConnect(e) => e.status(),
315            E::AuthenticationFailed(_) => F::BadAuth,
316            E::InvalidBanner => F::PeerProtocolViolation,
317            E::BadMessage(_) => F::PeerProtocolViolation,
318            E::ProtoError(e) => e.status(),
319            E::BadEnvironment | E::RelativeConnectFile | E::CannotResolvePath(_) => {
320                F::BadConnectPointPath
321            }
322            E::CannotParse(_) | E::CannotResolveConnectPoint(_) => F::ConnectPointNotUsable,
323            E::AllAttemptsDeclined => F::AllConnectAttemptsFailed,
324            E::AuthenticationNotSupported => F::NotSupported,
325            E::ServerAddressMismatch { .. } => F::ConnectPointNotUsable,
326            E::CookieMismatch => F::ConnectPointNotUsable,
327            E::LoadCookie(_) => F::ConnectPointNotUsable,
328        }
329    }
330
331    fn into_error_response(self) -> Option<ErrorResponse> {
332        use crate::ConnectError as E;
333        match self {
334            E::AuthenticationFailed(msg) => Some(msg),
335            _ => None,
336        }
337    }
338    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
339        Some(self)
340    }
341}
342
343impl IntoFfiError for tor_rpc_connect::ConnectError {
344    fn status(&self) -> FfiStatus {
345        use tor_rpc_connect::ConnectError as E;
346        use FfiStatus as F;
347        match self {
348            E::Io(_) => F::ConnectIo,
349            E::ExplicitAbort => F::AllConnectAttemptsFailed,
350            E::LoadCookie(_)
351            | E::UnsupportedSocketType
352            | E::UnsupportedAuthType
353            | E::AfUnixSocketPathAccess(_) => F::ConnectPointNotUsable,
354            _ => F::Internal,
355        }
356    }
357
358    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
359        Some(self)
360    }
361}
362
363impl IntoFfiError for crate::conn::ConnectFailure {
364    fn status(&self) -> FfiStatus {
365        self.final_error.status()
366    }
367
368    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
369        Some(self)
370    }
371
372    fn message(&self) -> String {
373        self.display_verbose().to_string()
374    }
375}
376
377impl IntoFfiError for crate::StreamError {
378    fn status(&self) -> FfiStatus {
379        use crate::StreamError as E;
380        use FfiStatus as F;
381        match self {
382            E::RpcMethods(e) => e.status(),
383            E::ProxyInfoRejected(_) => F::RequestFailed,
384            E::NewStreamRejected(_) => F::RequestFailed,
385            E::StreamReleaseRejected(_) => F::RequestFailed,
386            E::NotAuthenticated => F::NotAuthenticated,
387            E::NoSession => F::NotSupported,
388            E::Internal(_) => F::Internal,
389            E::NoProxy => F::RequestFailed,
390            E::Io(_) => F::ProxyIo,
391            E::SocksRequest(_) => F::InvalidInput,
392            E::SocksProtocol(_) => F::PeerProtocolViolation,
393            E::SocksError(_status) => {
394                // TODO RPC: We should expose the actual failure type somehow,
395                // possibly with a different call.  See #1580.
396                F::ProxyStreamFailed
397            }
398        }
399    }
400
401    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
402        Some(self)
403    }
404}
405
406impl IntoFfiError for crate::ProtoError {
407    fn status(&self) -> FfiStatus {
408        use crate::ProtoError as E;
409        use FfiStatus as F;
410        match self {
411            E::Shutdown(_) => F::Shutdown,
412            E::InvalidRequest(_) => F::InvalidInput,
413            E::RequestIdInUse => F::InvalidInput,
414            E::RequestCompleted => F::RequestCompleted,
415            E::DuplicateWait => F::Internal,
416            E::CouldNotEncode(_) => F::Internal,
417            E::InternalRequestFailed(_) => F::PeerProtocolViolation,
418        }
419    }
420    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
421        Some(self)
422    }
423}
424
425impl IntoFfiError for crate::BuilderError {
426    fn status(&self) -> FfiStatus {
427        use crate::BuilderError as E;
428        use FfiStatus as F;
429        match self {
430            E::InvalidConnectString => F::InvalidInput,
431        }
432    }
433    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
434        Some(self)
435    }
436}
437
438impl IntoFfiError for ErrorResponse {
439    fn status(&self) -> FfiStatus {
440        FfiStatus::RequestFailed
441    }
442    fn into_error_response(self) -> Option<ErrorResponse> {
443        Some(self)
444    }
445    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
446        None
447    }
448}
449
450/// An error returned by the Arti RPC code, exposed as an object.
451///
452/// When a function returns an [`ArtiRpcStatus`] other than [`ARTI_RPC_STATUS_SUCCESS`],
453/// it will also expose a newly allocated value of this type
454/// via its `error_out` parameter.
455pub type ArtiRpcError = FfiError;
456
457/// Return the status code associated with a given error.
458///
459/// If `err` is NULL, return [`ARTI_RPC_STATUS_INVALID_INPUT`].
460#[allow(clippy::missing_safety_doc)]
461#[no_mangle]
462pub unsafe extern "C" fn arti_rpc_err_status(err: *const ArtiRpcError) -> ArtiRpcStatus {
463    ffi_body_raw!(
464        {
465            let err: Option<&ArtiRpcError> [in_ptr_opt];
466        } in {
467            err.map(|e| e.status)
468               .unwrap_or(ARTI_RPC_STATUS_INVALID_INPUT)
469            // Safety: Return value is ArtiRpcStatus; trivially safe.
470        }
471    )
472}
473
474/// Return the OS error code underlying `err`, if any.
475///
476/// This is typically an `errno` on unix-like systems , or the result of `GetLastError()`
477/// on Windows.  It is only present when `err` was caused by the failure of some
478/// OS library call, like a `connect()` or `read()`.
479///
480/// Returns 0 if `err` is NULL, or if `err` was not caused by the failure of an
481/// OS library call.
482#[allow(clippy::missing_safety_doc)]
483#[no_mangle]
484pub unsafe extern "C" fn arti_rpc_err_os_error_code(err: *const ArtiRpcError) -> c_int {
485    ffi_body_raw!(
486        {
487            let err: Option<&ArtiRpcError> [in_ptr_opt];
488        } in {
489            err.and_then(|e| e.os_error_code)
490               .unwrap_or(0)
491             // Safety: Return value is c_int; trivially safe.
492        }
493    )
494}
495
496/// Return a human-readable error message associated with a given error.
497///
498/// The format of these messages may change arbitrarily between versions of this library;
499/// it is a mistake to depend on the actual contents of this message.
500///
501/// Return NULL if the input `err` is NULL.
502///
503/// # Correctness requirements
504///
505/// The resulting string pointer is valid only for as long as the input `err` is not freed.
506#[allow(clippy::missing_safety_doc)]
507#[no_mangle]
508pub unsafe extern "C" fn arti_rpc_err_message(err: *const ArtiRpcError) -> *const c_char {
509    ffi_body_raw!(
510        {
511            let err: Option<&ArtiRpcError> [in_ptr_opt];
512        } in {
513            err.map(|e| e.message.as_ptr())
514               .unwrap_or(std::ptr::null())
515            // Safety: returned pointer is null, or semantically borrowed from `err`.
516            // It is only null if `err` was null.
517            // The caller is not allowed to modify it.
518        }
519    )
520}
521
522/// Return a Json-formatted error response associated with a given error.
523///
524/// These messages are full responses, including the `error` field,
525/// and the `id` field (if present).
526///
527/// Return NULL if the specified error does not represent an RPC error response.
528///
529/// Return NULL if the input `err` is NULL.
530///
531/// # Correctness requirements
532///
533/// The resulting string pointer is valid only for as long as the input `err` is not freed.
534#[allow(clippy::missing_safety_doc)]
535#[no_mangle]
536pub unsafe extern "C" fn arti_rpc_err_response(err: *const ArtiRpcError) -> *const c_char {
537    ffi_body_raw!(
538        {
539            let err: Option<&ArtiRpcError> [in_ptr_opt];
540        } in {
541            err.and_then(ArtiRpcError::error_response_as_ptr)
542               .unwrap_or(std::ptr::null())
543            // Safety: returned pointer is null, or semantically borrowed from `err`.
544            // It is only null if `err` was null, or if `err` contained no response field.
545            // The caller is not allowed to modify it.
546        }
547    )
548}
549
550/// Make and return copy of a provided error.
551///
552/// Return NULL if the input is NULL.
553///
554/// # Ownership
555///
556/// The caller is responsible for making sure that the returned object
557/// is eventually freed with `arti_rpc_err_free()`.
558#[allow(clippy::missing_safety_doc)]
559#[no_mangle]
560pub unsafe extern "C" fn arti_rpc_err_clone(err: *const ArtiRpcError) -> *mut ArtiRpcError {
561    ffi_body_raw!(
562        {
563            let err: Option<&ArtiRpcError> [in_ptr_opt];
564        } in {
565            err.map(|e| Box::into_raw(Box::new(e.clone())))
566               .unwrap_or(std::ptr::null_mut())
567            // Safety: returned pointer is null, or newly allocated via Box::new().
568            // It is only null if the input was null.
569        }
570    )
571}
572
573/// Release storage held by a provided error.
574#[allow(clippy::missing_safety_doc)]
575#[no_mangle]
576pub unsafe extern "C" fn arti_rpc_err_free(err: *mut ArtiRpcError) {
577    ffi_body_raw!(
578        {
579            let err: Option<Box<ArtiRpcError>> [in_ptr_consume_opt];
580        } in {
581            drop(err);
582            // Safety: Return value is (); trivially safe.
583            ()
584        }
585    );
586}
587
588/// Run `body` and catch panics.  If one occurs, return the result of `on_err` instead.
589///
590/// We wrap the body of every C ffi function with this function
591/// (or with `handle_errors`, which uses this function),
592/// even if we do not think that the body can actually panic.
593pub(super) fn abort_on_panic<F, T>(body: F) -> T
594where
595    F: FnOnce() -> T + UnwindSafe,
596{
597    #[allow(clippy::print_stderr)]
598    match catch_unwind(body) {
599        Ok(x) => x,
600        Err(_panic_info) => {
601            eprintln!("Internal panic in arti-rpc library: aborting!");
602            std::process::abort();
603        }
604    }
605}
606
607/// Call `body`, converting any errors or panics that occur into an FfiError,
608/// and storing that error in `error_out`.
609pub(super) fn handle_errors<F>(error_out: Option<OutPtr<FfiError>>, body: F) -> ArtiRpcStatus
610where
611    F: FnOnce() -> Result<(), FfiError> + UnwindSafe,
612{
613    match abort_on_panic(body) {
614        Ok(()) => ARTI_RPC_STATUS_SUCCESS,
615        Err(e) => {
616            // "body" returned an error.
617            let status = e.status;
618            error_out.write_boxed_value_if_ptr_set(e);
619            status
620        }
621    }
622}