//! Error handling logic for our ffi code. use paste::paste; use std::error::Error as StdError; use std::ffi::{CStr, c_char, c_int}; use std::fmt::Display; use std::io::Error as IoError; use std::panic::{UnwindSafe, catch_unwind}; use crate::conn::ErrorResponse; use crate::util::Utf8CString; use super::ArtiRpcStatus; use super::util::{OptOutPtrExt as _, OutBoxedPtr, ffi_body_raw}; /// Helper: /// Given a restricted enum defining FfiStatus, also define a series of constants for its variants, /// and a string conversion function. // // NOTE: I tried to use derive_deftly here, but ran into trouble when defining the constants. // I wanted to have them be "pub const ARTI_FOO = FfiStatus::$vname", // but that doesn't work with cbindgen, which won't expose a constant unless it is a public type // it can recognize. // There is no way to use derive_deftly to look at the explicit discriminant of an enum. macro_rules! define_ffi_status { { $(#[$tm:meta])* pub(crate) enum FfiStatus { $( $(#[$m:meta])* [$s:expr] $id:ident = $e:expr, )+ } } => {paste!{ $(#[$tm])* pub(crate) enum FfiStatus { $( $(#[$m])* $id = $e, )+ } $( $(#[$m])* pub const [] : ArtiRpcStatus = $e; )+ /// Return a string representing the meaning of a given `ArtiRpcStatus`. /// /// The result will always be non-NULL, even if the status is unrecognized. #[unsafe(no_mangle)] pub extern "C" fn arti_rpc_status_to_str(status: ArtiRpcStatus) -> *const c_char { match status { $( [] => $s, )+ _ => c"(unrecognized status)", }.as_ptr() } }} } define_ffi_status! { /// View of FFI status as rust enumeration. /// /// Not exposed in the FFI interfaces, except via cast to ArtiStatus. /// /// We define this as an enumeration so that we can treat it exhaustively in Rust. #[derive(Copy, Clone, Debug)] #[repr(u32)] pub(crate) enum FfiStatus { /// The function has returned successfully. #[allow(dead_code)] [c"Success"] Success = 0, /// One or more of the inputs to a library function was invalid. /// /// (This error was generated by the library, before any request was sent.) [c"Invalid input"] InvalidInput = 1, /// Tried to use some functionality /// (for example, an authentication method or connection scheme) /// that wasn't available on this platform or build. /// /// (This error was generated by the library, before any request was sent.) [c"Not supported"] NotSupported = 2, /// Tried to connect to Arti, but an IO error occurred. /// /// This may indicate that Arti wasn't running, /// or that Arti was built without RPC support, /// or that Arti wasn't running at the specified location. /// /// (This error was generated by the library.) [c"An IO error occurred while connecting to Arti"] ConnectIo = 3, /// We tried to authenticate with Arti, but it rejected our attempt. /// /// (This error was sent by the peer.) [c"Authentication rejected"] BadAuth = 4, /// Our peer has, in some way, violated the Arti-RPC protocol. /// /// (This error was generated by the library, /// based on a response from Arti that appeared to be invalid.) [c"Peer violated the RPC protocol"] PeerProtocolViolation = 5, /// The peer has closed our connection; possibly because it is shutting down. /// /// (This error was generated by the library, /// based on the connection being closed or reset from the peer.) [c"Peer has shut down"] Shutdown = 6, /// An internal error occurred in the arti rpc client. /// /// (This error was generated by the library. /// If you see it, there is probably a bug in the library.) [c"Internal error; possible bug?"] Internal = 7, /// The peer reports that one of our requests has failed. /// /// (This error was sent by the peer, in response to one of our requests. /// No further responses to that request will be received or accepted.) [c"Request has failed"] RequestFailed = 8, /// Tried to check the status of a request and found that it was no longer running. [c"Request has already completed (or failed)"] RequestCompleted = 9, /// An IO error occurred while trying to negotiate a data stream /// using Arti as a proxy. [c"IO error while connecting to Arti as a Proxy"] ProxyIo = 10, /// An attempt to negotiate a data stream through Arti failed, /// with an error from the proxy protocol. // // TODO RPC: expose the actual error type; see #1580. [c"Data stream failed"] ProxyStreamFailed = 11, /// Some operation failed because it was attempted on an unauthenticated channel. /// /// (At present (Sep 2024) there is no way to get an unauthenticated channel from this library, /// but that may change in the future.) [c"Not authenticated"] NotAuthenticated = 12, /// All of our attempts to connect to Arti failed, /// or we reached an explicit instruction to "abort" our connection attempts. [c"All attempts to connect to Arti RPC failed"] AllConnectAttemptsFailed = 13, /// We tried to connect to Arti at a given connect point, /// but it could not be used: /// either because we don't know how, /// because we were configured not to use it, /// or because we were not able to access some necessary file or directory. [c"Connect point was not usable"] ConnectPointNotUsable = 14, /// We were unable to parse or resolve an entry /// in our connect point search path. [c"Invalid connect point search path"] BadConnectPointPath = 15, } } /// An error as returned by the Arti FFI code. #[derive(Debug, Clone)] pub struct FfiError { /// The status of this error messages pub(super) status: ArtiRpcStatus, /// A human-readable message describing this error message: Utf8CString, /// If present, a Json-formatted message from our peer that we are representing with this error. error_response: Option, /// If present, the OS error code that caused this error. // // (Actually, this should be RawOsError, but that type isn't stable.) os_error_code: Option, } impl FfiError { /// Helper: If this error stems from a response from our RPC peer, /// return that response. fn error_response_as_ptr(&self) -> Option<*const c_char> { self.error_response.as_ref().map(|response| { let cstr: &CStr = response.as_ref(); cstr.as_ptr() }) } } /// Convenience trait to help implement `Into` /// /// Any error that implements this trait will be convertible into an [`FfiError`]. // additional requirements: display doesn't make NULs. pub(crate) trait IntoFfiError: Display + Sized { /// Return the status fn status(&self) -> FfiStatus; /// Return this type as an Error, if it is one. fn as_error(&self) -> Option<&(dyn StdError + 'static)>; /// Return a message for this error. /// /// By default, uses the Display of this error, and of its sources, to build a string. /// The format and content of this string is not specified, and is not guaranteed /// to remain stable. fn message(&self) -> String { use tor_error::ErrorReport as _; match self.as_error() { Some(e) => { let msg = e.report().to_string(); // Note: Having to strip the prefix here is somewhat annoying. msg.strip_prefix("error: ") .map(str::to_string) .unwrap_or_else(|| msg) } None => self.to_string(), } } /// Return the OS error code (if any) underlying this error. /// /// On unix-like platforms, this is an `errno`; on Windows, it's a /// code from `GetLastError.` fn os_error_code(&self) -> Option { let mut err = self.as_error()?; // Note that we aren't using tor_basic_utils::ErrorSources here: // it exists to work around the case where an error is nested inside an IoError. // But in this code, we are only looking for the outermost IoError, so it isn't // necessary. loop { if let Some(io_error) = err.downcast_ref::() { return io_error.raw_os_error() as Option; } err = err.source()?; } } /// Consume this error and return an [`ErrorResponse`] fn into_error_response(self) -> Option { None } } impl From for FfiError { fn from(value: T) -> Self { let status = value.status() as u32; let message = value .message() .try_into() .expect("Error message had a NUL?"); let os_error_code = value.os_error_code(); let error_response = value.into_error_response(); Self { status, message, error_response, os_error_code, } } } impl From for FfiError { fn from(value: void::Void) -> Self { void::unreachable(value) } } /// Tried to call a ffi function with a not-permitted argument. #[derive(Clone, Debug, thiserror::Error)] pub(super) enum InvalidInput { /// Tried to convert a NULL pointer to an FFI object. #[error("Provided argument was NULL.")] NullPointer, /// Tried to convert a non-UTF string. #[error("Provided string was not UTF-8")] BadUtf8, /// Tried to use an invalid port. #[error("Port was not in range 1..65535")] BadPort, /// Tried to use an invalid constant #[error("Provided constant was not recognized")] InvalidConstValue, } impl From for InvalidInput { fn from(value: void::Void) -> Self { void::unreachable(value) } } impl IntoFfiError for InvalidInput { fn status(&self) -> FfiStatus { FfiStatus::InvalidInput } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } } impl IntoFfiError for crate::ConnectError { fn status(&self) -> FfiStatus { use crate::ConnectError as E; use FfiStatus as F; match self { E::CannotConnect(e) => e.status(), E::AuthenticationFailed(_) => F::BadAuth, E::InvalidBanner => F::PeerProtocolViolation, E::BadMessage(_) => F::PeerProtocolViolation, E::ProtoError(e) => e.status(), E::BadEnvironment | E::RelativeConnectFile | E::CannotResolvePath(_) => { F::BadConnectPointPath } E::CannotParse(_) | E::CannotResolveConnectPoint(_) => F::ConnectPointNotUsable, E::AllAttemptsDeclined => F::AllConnectAttemptsFailed, E::AuthenticationNotSupported => F::NotSupported, E::ServerAddressMismatch { .. } => F::ConnectPointNotUsable, E::CookieMismatch => F::ConnectPointNotUsable, E::LoadCookie(_) => F::ConnectPointNotUsable, E::StreamTypeUnsupported => F::ConnectPointNotUsable, E::NoSuperuserPermission => F::ConnectPointNotUsable, } } fn into_error_response(self) -> Option { use crate::ConnectError as E; match self { E::AuthenticationFailed(msg) => Some(msg), _ => None, } } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } } impl IntoFfiError for tor_rpc_connect::ConnectError { fn status(&self) -> FfiStatus { use FfiStatus as F; use tor_rpc_connect::ConnectError as E; match self { E::Io(_) => F::ConnectIo, E::ExplicitAbort => F::AllConnectAttemptsFailed, E::LoadCookie(_) | E::UnsupportedSocketType | E::UnsupportedAuthType | E::AfUnixSocketPathAccess(_) => F::ConnectPointNotUsable, _ => F::Internal, } } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } } impl IntoFfiError for crate::conn::ConnectFailure { fn status(&self) -> FfiStatus { self.final_error.status() } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } fn message(&self) -> String { self.display_verbose().to_string() } } impl IntoFfiError for crate::StreamError { fn status(&self) -> FfiStatus { use crate::StreamError as E; use FfiStatus as F; match self { E::RpcMethods(e) => e.status(), E::ProxyInfoRejected(_) => F::RequestFailed, E::NewStreamRejected(_) => F::RequestFailed, E::StreamReleaseRejected(_) => F::RequestFailed, E::NotAuthenticated => F::NotAuthenticated, E::NoSession => F::NotSupported, E::Internal(_) => F::Internal, E::NoProxy => F::RequestFailed, E::Io(_) => F::ProxyIo, E::SocksRequest(_) => F::InvalidInput, E::SocksProtocol(_) => F::PeerProtocolViolation, E::SocksError(_status) => { // TODO RPC: We should expose the actual failure type somehow, // possibly with a different call. See #1580. F::ProxyStreamFailed } } } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } } impl IntoFfiError for crate::ProtoError { fn status(&self) -> FfiStatus { use crate::ProtoError as E; use FfiStatus as F; match self { E::Shutdown(_) => F::Shutdown, E::InvalidRequest(_) => F::InvalidInput, E::RequestIdInUse => F::InvalidInput, E::RequestCompleted => F::RequestCompleted, E::DuplicateWait => F::Internal, E::RequestNotWaitable => F::Internal, E::CouldNotEncode(_) => F::Internal, E::InternalRequestFailed(_) => F::PeerProtocolViolation, } } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } } impl IntoFfiError for crate::BuilderError { fn status(&self) -> FfiStatus { use crate::BuilderError as E; use FfiStatus as F; match self { E::InvalidConnectString => F::InvalidInput, } } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { Some(self) } } impl IntoFfiError for ErrorResponse { fn status(&self) -> FfiStatus { FfiStatus::RequestFailed } fn into_error_response(self) -> Option { Some(self) } fn as_error(&self) -> Option<&(dyn StdError + 'static)> { None } } /// An error returned by the Arti RPC code, exposed as an object. /// /// When a function returns an [`ArtiRpcStatus`] other than [`ARTI_RPC_STATUS_SUCCESS`], /// it will also expose a newly allocated value of this type /// via its `error_out` parameter. pub type ArtiRpcError = FfiError; /// Return the status code associated with a given error. /// /// If `err` is NULL, return [`ARTI_RPC_STATUS_INVALID_INPUT`]. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_err_status(err: *const ArtiRpcError) -> ArtiRpcStatus { ffi_body_raw!( { let err: Option<&ArtiRpcError> [in_ptr_opt]; } in { err.map(|e| e.status) .unwrap_or(ARTI_RPC_STATUS_INVALID_INPUT) // Safety: Return value is ArtiRpcStatus; trivially safe. } ) } /// Return the OS error code underlying `err`, if any. /// /// This is typically an `errno` on unix-like systems , or the result of `GetLastError()` /// on Windows. It is only present when `err` was caused by the failure of some /// OS library call, like a `connect()` or `read()`. /// /// Returns 0 if `err` is NULL, or if `err` was not caused by the failure of an /// OS library call. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_err_os_error_code(err: *const ArtiRpcError) -> c_int { ffi_body_raw!( { let err: Option<&ArtiRpcError> [in_ptr_opt]; } in { err.and_then(|e| e.os_error_code) .unwrap_or(0) // Safety: Return value is c_int; trivially safe. } ) } /// Return a human-readable error message associated with a given error. /// /// The format of these messages may change arbitrarily between versions of this library; /// it is a mistake to depend on the actual contents of this message. /// /// Return NULL if the input `err` is NULL. /// /// # Correctness requirements /// /// The resulting string pointer is valid only for as long as the input `err` is not freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_err_message(err: *const ArtiRpcError) -> *const c_char { ffi_body_raw!( { let err: Option<&ArtiRpcError> [in_ptr_opt]; } in { err.map(|e| e.message.as_ptr()) .unwrap_or(std::ptr::null()) // Safety: returned pointer is null, or semantically borrowed from `err`. // It is only null if `err` was null. // The caller is not allowed to modify it. } ) } /// Return a Json-formatted error response associated with a given error. /// /// These messages are full responses, including the `error` field, /// and the `id` field (if present). /// /// Return NULL if the specified error does not represent an RPC error response. /// /// Return NULL if the input `err` is NULL. /// /// # Correctness requirements /// /// The resulting string pointer is valid only for as long as the input `err` is not freed. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_err_response(err: *const ArtiRpcError) -> *const c_char { ffi_body_raw!( { let err: Option<&ArtiRpcError> [in_ptr_opt]; } in { err.and_then(ArtiRpcError::error_response_as_ptr) .unwrap_or(std::ptr::null()) // Safety: returned pointer is null, or semantically borrowed from `err`. // It is only null if `err` was null, or if `err` contained no response field. // The caller is not allowed to modify it. } ) } /// Make and return copy of a provided error. /// /// Return NULL if the input is NULL. /// /// # Ownership /// /// The caller is responsible for making sure that the returned object /// is eventually freed with `arti_rpc_err_free()`. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_err_clone(err: *const ArtiRpcError) -> *mut ArtiRpcError { ffi_body_raw!( { let err: Option<&ArtiRpcError> [in_ptr_opt]; } in { err.map(|e| Box::into_raw(Box::new(e.clone()))) .unwrap_or(std::ptr::null_mut()) // Safety: returned pointer is null, or newly allocated via Box::new(). // It is only null if the input was null. } ) } /// Release storage held by a provided error. #[allow(clippy::missing_safety_doc)] #[unsafe(no_mangle)] pub unsafe extern "C" fn arti_rpc_err_free(err: *mut ArtiRpcError) { ffi_body_raw!( { let err: Option> [in_ptr_consume_opt]; } in { drop(err); // Safety: Return value is (); trivially safe. () } ); } /// Run `body` and catch panics. If one occurs, return the result of `on_err` instead. /// /// We wrap the body of every C ffi function with this function /// (or with `handle_errors`, which uses this function), /// even if we do not think that the body can actually panic. pub(super) fn abort_on_panic(body: F) -> T where F: FnOnce() -> T + UnwindSafe, { #[allow(clippy::print_stderr)] match catch_unwind(body) { Ok(x) => x, Err(_panic_info) => { eprintln!("Internal panic in arti-rpc library: aborting!"); std::process::abort(); } } } /// Call `body`, converting any errors or panics that occur into an FfiError, /// and storing that error in `error_out`. pub(super) fn handle_errors(error_out: Option>, body: F) -> ArtiRpcStatus where F: FnOnce() -> Result<(), FfiError> + UnwindSafe, { match abort_on_panic(body) { Ok(()) => ARTI_RPC_STATUS_SUCCESS, Err(e) => { // "body" returned an error. let status = e.status; error_out.write_boxed_value_if_ptr_set(e); status } } }