1
//! Error handling logic for our ffi code.
2

            
3
use paste::paste;
4
use std::error::Error as StdError;
5
use std::ffi::{CStr, c_char, c_int};
6
use std::fmt::Display;
7
use std::io::Error as IoError;
8
use std::panic::{UnwindSafe, catch_unwind};
9

            
10
use crate::conn::ErrorResponse;
11
use crate::util::Utf8CString;
12

            
13
use super::ArtiRpcStatus;
14
use super::util::{OptOutPtrExt as _, OutBoxedPtr, ffi_body_raw};
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.
25
macro_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
        #[unsafe(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

            
65
define_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)]
73
pub(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
    /// because we were configured not to use it,
169
    /// or because we were not able to access some necessary file or directory.
170
    [c"Connect point was not usable"]
171
    ConnectPointNotUsable = 14,
172

            
173
    /// We were unable to parse or resolve an entry
174
    /// in our connect point search path.
175
    [c"Invalid connect point search path"]
176
    BadConnectPointPath = 15,
177
}
178
}
179

            
180
/// An error as returned by the Arti FFI code.
181
#[derive(Debug, Clone)]
182
pub struct FfiError {
183
    /// The status of this error messages
184
    pub(super) status: ArtiRpcStatus,
185
    /// A human-readable message describing this error
186
    message: Utf8CString,
187
    /// If present, a Json-formatted message from our peer that we are representing with this error.
188
    error_response: Option<ErrorResponse>,
189
    /// If present, the OS error code that caused this error.
190
    //
191
    // (Actually, this should be RawOsError, but that type isn't stable.)
192
    os_error_code: Option<i32>,
193
}
194

            
195
impl FfiError {
196
    /// Helper: If this error stems from a response from our RPC peer,
197
    /// return that response.
198
    fn error_response_as_ptr(&self) -> Option<*const c_char> {
199
        self.error_response.as_ref().map(|response| {
200
            let cstr: &CStr = response.as_ref();
201
            cstr.as_ptr()
202
        })
203
    }
204
}
205

            
206
/// Convenience trait to help implement `Into<FfiError>`
207
///
208
/// Any error that implements this trait will be convertible into an [`FfiError`].
209
// additional requirements: display doesn't make NULs.
210
pub(crate) trait IntoFfiError: Display + Sized {
211
    /// Return the status
212
    fn status(&self) -> FfiStatus;
213
    /// Return this type as an Error, if it is one.
214
    fn as_error(&self) -> Option<&(dyn StdError + 'static)>;
215
    /// Return a message for this error.
216
    ///
217
    /// By default, uses the Display of this error, and of its sources, to build a string.
218
    /// The format and content of this string is not specified, and is not guaranteed
219
    /// to remain stable.
220
    fn message(&self) -> String {
221
        use tor_error::ErrorReport as _;
222
        match self.as_error() {
223
            Some(e) => {
224
                let msg = e.report().to_string();
225
                // Note: Having to strip the prefix here is somewhat annoying.
226
                msg.strip_prefix("error: ")
227
                    .map(str::to_string)
228
                    .unwrap_or_else(|| msg)
229
            }
230
            None => self.to_string(),
231
        }
232
    }
233
    /// Return the OS error code (if any) underlying this error.
234
    ///
235
    /// On unix-like platforms, this is an `errno`; on Windows, it's a
236
    /// code from `GetLastError.`
237
    fn os_error_code(&self) -> Option<i32> {
238
        let mut err = self.as_error()?;
239

            
240
        // Note that we aren't using tor_basic_utils::ErrorSources here:
241
        // it exists to work around the case where an error is nested inside an IoError.
242
        // But in this code, we are only looking for the outermost IoError, so it isn't
243
        // necessary.
244
        loop {
245
            if let Some(io_error) = err.downcast_ref::<IoError>() {
246
                return io_error.raw_os_error() as Option<i32>;
247
            }
248
            err = err.source()?;
249
        }
250
    }
251
    /// Consume this error and return an [`ErrorResponse`]
252
    fn into_error_response(self) -> Option<ErrorResponse> {
253
        None
254
    }
255
}
256
impl<T: IntoFfiError> From<T> for FfiError {
257
    fn from(value: T) -> Self {
258
        let status = value.status() as u32;
259
        let message = value
260
            .message()
261
            .try_into()
262
            .expect("Error message had a NUL?");
263
        let os_error_code = value.os_error_code();
264
        let error_response = value.into_error_response();
265
        Self {
266
            status,
267
            message,
268
            error_response,
269
            os_error_code,
270
        }
271
    }
272
}
273
impl From<void::Void> for FfiError {
274
    fn from(value: void::Void) -> Self {
275
        void::unreachable(value)
276
    }
277
}
278

            
279
/// Tried to call a ffi function with a not-permitted argument.
280
#[derive(Clone, Debug, thiserror::Error)]
281
pub(super) enum InvalidInput {
282
    /// Tried to convert a NULL pointer to an FFI object.
283
    #[error("Provided argument was NULL.")]
284
    NullPointer,
285

            
286
    /// Tried to convert a non-UTF string.
287
    #[error("Provided string was not UTF-8")]
288
    BadUtf8,
289

            
290
    /// Tried to use an invalid port.
291
    #[error("Port was not in range 1..65535")]
292
    BadPort,
293

            
294
    /// Tried to use an invalid constant
295
    #[error("Provided constant was not recognized")]
296
    InvalidConstValue,
297
}
298

            
299
impl From<void::Void> for InvalidInput {
300
    fn from(value: void::Void) -> Self {
301
        void::unreachable(value)
302
    }
303
}
304

            
305
impl IntoFfiError for InvalidInput {
306
    fn status(&self) -> FfiStatus {
307
        FfiStatus::InvalidInput
308
    }
309
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
310
        Some(self)
311
    }
312
}
313

            
314
impl IntoFfiError for crate::ConnectError {
315
    fn status(&self) -> FfiStatus {
316
        use crate::ConnectError as E;
317
        use FfiStatus as F;
318
        match self {
319
            E::CannotConnect(e) => e.status(),
320
            E::AuthenticationFailed(_) => F::BadAuth,
321
            E::InvalidBanner => F::PeerProtocolViolation,
322
            E::BadMessage(_) => F::PeerProtocolViolation,
323
            E::ProtoError(e) => e.status(),
324
            E::BadEnvironment | E::RelativeConnectFile | E::CannotResolvePath(_) => {
325
                F::BadConnectPointPath
326
            }
327
            E::CannotParse(_) | E::CannotResolveConnectPoint(_) => F::ConnectPointNotUsable,
328
            E::AllAttemptsDeclined => F::AllConnectAttemptsFailed,
329
            E::AuthenticationNotSupported => F::NotSupported,
330
            E::ServerAddressMismatch { .. } => F::ConnectPointNotUsable,
331
            E::CookieMismatch => F::ConnectPointNotUsable,
332
            E::LoadCookie(_) => F::ConnectPointNotUsable,
333
            E::StreamTypeUnsupported => F::ConnectPointNotUsable,
334
            E::NoSuperuserPermission => F::ConnectPointNotUsable,
335
        }
336
    }
337

            
338
    fn into_error_response(self) -> Option<ErrorResponse> {
339
        use crate::ConnectError as E;
340
        match self {
341
            E::AuthenticationFailed(msg) => Some(msg),
342
            _ => None,
343
        }
344
    }
345
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
346
        Some(self)
347
    }
348
}
349

            
350
impl IntoFfiError for tor_rpc_connect::ConnectError {
351
    fn status(&self) -> FfiStatus {
352
        use FfiStatus as F;
353
        use tor_rpc_connect::ConnectError as E;
354
        match self {
355
            E::Io(_) => F::ConnectIo,
356
            E::ExplicitAbort => F::AllConnectAttemptsFailed,
357
            E::LoadCookie(_)
358
            | E::UnsupportedSocketType
359
            | E::UnsupportedAuthType
360
            | E::AfUnixSocketPathAccess(_) => F::ConnectPointNotUsable,
361
            _ => F::Internal,
362
        }
363
    }
364

            
365
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
366
        Some(self)
367
    }
368
}
369

            
370
impl IntoFfiError for crate::conn::ConnectFailure {
371
    fn status(&self) -> FfiStatus {
372
        self.final_error.status()
373
    }
374

            
375
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
376
        Some(self)
377
    }
378

            
379
    fn message(&self) -> String {
380
        self.display_verbose().to_string()
381
    }
382
}
383

            
384
impl IntoFfiError for crate::StreamError {
385
    fn status(&self) -> FfiStatus {
386
        use crate::StreamError as E;
387
        use FfiStatus as F;
388
        match self {
389
            E::RpcMethods(e) => e.status(),
390
            E::ProxyInfoRejected(_) => F::RequestFailed,
391
            E::NewStreamRejected(_) => F::RequestFailed,
392
            E::StreamReleaseRejected(_) => F::RequestFailed,
393
            E::NotAuthenticated => F::NotAuthenticated,
394
            E::NoSession => F::NotSupported,
395
            E::Internal(_) => F::Internal,
396
            E::NoProxy => F::RequestFailed,
397
            E::Io(_) => F::ProxyIo,
398
            E::SocksRequest(_) => F::InvalidInput,
399
            E::SocksProtocol(_) => F::PeerProtocolViolation,
400
            E::SocksError(_status) => {
401
                // TODO RPC: We should expose the actual failure type somehow,
402
                // possibly with a different call.  See #1580.
403
                F::ProxyStreamFailed
404
            }
405
        }
406
    }
407

            
408
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
409
        Some(self)
410
    }
411
}
412

            
413
impl IntoFfiError for crate::ProtoError {
414
    fn status(&self) -> FfiStatus {
415
        use crate::ProtoError as E;
416
        use FfiStatus as F;
417
        match self {
418
            E::Shutdown(_) => F::Shutdown,
419
            E::InvalidRequest(_) => F::InvalidInput,
420
            E::RequestIdInUse => F::InvalidInput,
421
            E::RequestCompleted => F::RequestCompleted,
422
            E::DuplicateWait => F::Internal,
423
            E::RequestNotWaitable => F::Internal,
424
            E::CouldNotEncode(_) => F::Internal,
425
            E::InternalRequestFailed(_) => F::PeerProtocolViolation,
426
        }
427
    }
428
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
429
        Some(self)
430
    }
431
}
432

            
433
impl IntoFfiError for crate::BuilderError {
434
    fn status(&self) -> FfiStatus {
435
        use crate::BuilderError as E;
436
        use FfiStatus as F;
437
        match self {
438
            E::InvalidConnectString => F::InvalidInput,
439
        }
440
    }
441
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
442
        Some(self)
443
    }
444
}
445

            
446
impl IntoFfiError for ErrorResponse {
447
    fn status(&self) -> FfiStatus {
448
        FfiStatus::RequestFailed
449
    }
450
    fn into_error_response(self) -> Option<ErrorResponse> {
451
        Some(self)
452
    }
453
    fn as_error(&self) -> Option<&(dyn StdError + 'static)> {
454
        None
455
    }
456
}
457

            
458
/// An error returned by the Arti RPC code, exposed as an object.
459
///
460
/// When a function returns an [`ArtiRpcStatus`] other than [`ARTI_RPC_STATUS_SUCCESS`],
461
/// it will also expose a newly allocated value of this type
462
/// via its `error_out` parameter.
463
pub type ArtiRpcError = FfiError;
464

            
465
/// Return the status code associated with a given error.
466
///
467
/// If `err` is NULL, return [`ARTI_RPC_STATUS_INVALID_INPUT`].
468
#[allow(clippy::missing_safety_doc)]
469
#[unsafe(no_mangle)]
470
pub unsafe extern "C" fn arti_rpc_err_status(err: *const ArtiRpcError) -> ArtiRpcStatus {
471
    ffi_body_raw!(
472
        {
473
            let err: Option<&ArtiRpcError> [in_ptr_opt];
474
        } in {
475
            err.map(|e| e.status)
476
               .unwrap_or(ARTI_RPC_STATUS_INVALID_INPUT)
477
            // Safety: Return value is ArtiRpcStatus; trivially safe.
478
        }
479
    )
480
}
481

            
482
/// Return the OS error code underlying `err`, if any.
483
///
484
/// This is typically an `errno` on unix-like systems , or the result of `GetLastError()`
485
/// on Windows.  It is only present when `err` was caused by the failure of some
486
/// OS library call, like a `connect()` or `read()`.
487
///
488
/// Returns 0 if `err` is NULL, or if `err` was not caused by the failure of an
489
/// OS library call.
490
#[allow(clippy::missing_safety_doc)]
491
#[unsafe(no_mangle)]
492
pub unsafe extern "C" fn arti_rpc_err_os_error_code(err: *const ArtiRpcError) -> c_int {
493
    ffi_body_raw!(
494
        {
495
            let err: Option<&ArtiRpcError> [in_ptr_opt];
496
        } in {
497
            err.and_then(|e| e.os_error_code)
498
               .unwrap_or(0)
499
             // Safety: Return value is c_int; trivially safe.
500
        }
501
    )
502
}
503

            
504
/// Return a human-readable error message associated with a given error.
505
///
506
/// The format of these messages may change arbitrarily between versions of this library;
507
/// it is a mistake to depend on the actual contents of this message.
508
///
509
/// Return NULL if the input `err` is NULL.
510
///
511
/// # Correctness requirements
512
///
513
/// The resulting string pointer is valid only for as long as the input `err` is not freed.
514
#[allow(clippy::missing_safety_doc)]
515
#[unsafe(no_mangle)]
516
pub unsafe extern "C" fn arti_rpc_err_message(err: *const ArtiRpcError) -> *const c_char {
517
    ffi_body_raw!(
518
        {
519
            let err: Option<&ArtiRpcError> [in_ptr_opt];
520
        } in {
521
            err.map(|e| e.message.as_ptr())
522
               .unwrap_or(std::ptr::null())
523
            // Safety: returned pointer is null, or semantically borrowed from `err`.
524
            // It is only null if `err` was null.
525
            // The caller is not allowed to modify it.
526
        }
527
    )
528
}
529

            
530
/// Return a Json-formatted error response associated with a given error.
531
///
532
/// These messages are full responses, including the `error` field,
533
/// and the `id` field (if present).
534
///
535
/// Return NULL if the specified error does not represent an RPC error response.
536
///
537
/// Return NULL if the input `err` is NULL.
538
///
539
/// # Correctness requirements
540
///
541
/// The resulting string pointer is valid only for as long as the input `err` is not freed.
542
#[allow(clippy::missing_safety_doc)]
543
#[unsafe(no_mangle)]
544
pub unsafe extern "C" fn arti_rpc_err_response(err: *const ArtiRpcError) -> *const c_char {
545
    ffi_body_raw!(
546
        {
547
            let err: Option<&ArtiRpcError> [in_ptr_opt];
548
        } in {
549
            err.and_then(ArtiRpcError::error_response_as_ptr)
550
               .unwrap_or(std::ptr::null())
551
            // Safety: returned pointer is null, or semantically borrowed from `err`.
552
            // It is only null if `err` was null, or if `err` contained no response field.
553
            // The caller is not allowed to modify it.
554
        }
555
    )
556
}
557

            
558
/// Make and return copy of a provided error.
559
///
560
/// Return NULL if the input is NULL.
561
///
562
/// # Ownership
563
///
564
/// The caller is responsible for making sure that the returned object
565
/// is eventually freed with `arti_rpc_err_free()`.
566
#[allow(clippy::missing_safety_doc)]
567
#[unsafe(no_mangle)]
568
pub unsafe extern "C" fn arti_rpc_err_clone(err: *const ArtiRpcError) -> *mut ArtiRpcError {
569
    ffi_body_raw!(
570
        {
571
            let err: Option<&ArtiRpcError> [in_ptr_opt];
572
        } in {
573
            err.map(|e| Box::into_raw(Box::new(e.clone())))
574
               .unwrap_or(std::ptr::null_mut())
575
            // Safety: returned pointer is null, or newly allocated via Box::new().
576
            // It is only null if the input was null.
577
        }
578
    )
579
}
580

            
581
/// Release storage held by a provided error.
582
#[allow(clippy::missing_safety_doc)]
583
#[unsafe(no_mangle)]
584
pub unsafe extern "C" fn arti_rpc_err_free(err: *mut ArtiRpcError) {
585
    ffi_body_raw!(
586
        {
587
            let err: Option<Box<ArtiRpcError>> [in_ptr_consume_opt];
588
        } in {
589
            drop(err);
590
            // Safety: Return value is (); trivially safe.
591
            ()
592
        }
593
    );
594
}
595

            
596
/// Run `body` and catch panics.  If one occurs, return the result of `on_err` instead.
597
///
598
/// We wrap the body of every C ffi function with this function
599
/// (or with `handle_errors`, which uses this function),
600
/// even if we do not think that the body can actually panic.
601
pub(super) fn abort_on_panic<F, T>(body: F) -> T
602
where
603
    F: FnOnce() -> T + UnwindSafe,
604
{
605
    #[allow(clippy::print_stderr)]
606
    match catch_unwind(body) {
607
        Ok(x) => x,
608
        Err(_panic_info) => {
609
            eprintln!("Internal panic in arti-rpc library: aborting!");
610
            std::process::abort();
611
        }
612
    }
613
}
614

            
615
/// Call `body`, converting any errors or panics that occur into an FfiError,
616
/// and storing that error in `error_out`.
617
pub(super) fn handle_errors<F>(error_out: Option<OutBoxedPtr<FfiError>>, body: F) -> ArtiRpcStatus
618
where
619
    F: FnOnce() -> Result<(), FfiError> + UnwindSafe,
620
{
621
    match abort_on_panic(body) {
622
        Ok(()) => ARTI_RPC_STATUS_SUCCESS,
623
        Err(e) => {
624
            // "body" returned an error.
625
            let status = e.status;
626
            error_out.write_boxed_value_if_ptr_set(e);
627
            status
628
        }
629
    }
630
}