Skip to main content

native_ossl/
error.rs

1//! OpenSSL error queue — `Error`, `ErrorStack`, `ErrState` (OpenSSL 3.2+).
2//!
3//! Every OpenSSL operation that can fail pushes one or more records onto a
4//! thread-local error queue.  `ErrorStack::drain()` pops the entire queue and
5//! returns it as a `Vec<Error>`.  Every public function in this crate that can
6//! fail returns `Result<T, ErrorStack>`.
7
8use native_ossl_sys as sys;
9use std::ffi::CStr;
10use std::fmt;
11
12// ── Single error record ───────────────────────────────────────────────────────
13
14/// A single record from the OpenSSL error queue.
15#[derive(Debug, Clone)]
16pub struct Error {
17    /// Raw packed error code (use `lib()` / `reason()` to decompose).
18    code: u64,
19    /// Human-readable reason string, if OpenSSL knows one.
20    reason: Option<String>,
21    /// Library name string, if OpenSSL knows one.
22    lib: Option<String>,
23    /// Source file (from the error record, not Rust).
24    file: Option<String>,
25    /// Function name (from the error record).
26    func: Option<String>,
27    /// Caller-supplied data string (e.g. key file path).
28    data: Option<String>,
29}
30
31impl Error {
32    /// The packed error code.
33    #[must_use]
34    pub fn code(&self) -> u64 {
35        self.code
36    }
37
38    /// Library component that generated this error, if known.
39    #[must_use]
40    pub fn lib(&self) -> Option<&str> {
41        self.lib.as_deref()
42    }
43
44    /// Reason string, if known.
45    #[must_use]
46    pub fn reason(&self) -> Option<&str> {
47        self.reason.as_deref()
48    }
49
50    /// C source file where the error was raised (may be absent in release builds).
51    #[must_use]
52    pub fn file(&self) -> Option<&str> {
53        self.file.as_deref()
54    }
55
56    /// C function where the error was raised.
57    #[must_use]
58    pub fn func(&self) -> Option<&str> {
59        self.func.as_deref()
60    }
61
62    /// Caller-supplied data string attached to the error record.
63    #[must_use]
64    pub fn data(&self) -> Option<&str> {
65        self.data.as_deref()
66    }
67}
68
69impl fmt::Display for Error {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        if let Some(r) = &self.reason {
72            write!(f, "{r}")?;
73        } else {
74            write!(f, "error:{:#010x}", self.code)?;
75        }
76        if let Some(lib) = &self.lib {
77            write!(f, " (lib:{lib})")?;
78        }
79        if let Some(func) = &self.func {
80            write!(f, " in {func}")?;
81        }
82        if let Some(data) = &self.data {
83            write!(f, ": {data}")?;
84        }
85        Ok(())
86    }
87}
88
89// ── Error queue drain ─────────────────────────────────────────────────────────
90
91/// A snapshot of all records that were on the thread-local OpenSSL error queue.
92///
93/// Returned as the `Err` variant of every `Result<T, ErrorStack>` in this crate.
94/// The queue is cleared when this is constructed.
95#[derive(Debug, Clone)]
96pub struct ErrorStack(Vec<Error>);
97
98impl ErrorStack {
99    /// Drain the current thread's OpenSSL error queue into a new `ErrorStack`.
100    ///
101    /// After this call the queue is empty.  This is the canonical way to
102    /// turn an OpenSSL failure into a Rust error value.
103    #[must_use]
104    pub fn drain() -> Self {
105        let mut errors = Vec::new();
106
107        loop {
108            let mut file: *const std::os::raw::c_char = std::ptr::null();
109            let mut func: *const std::os::raw::c_char = std::ptr::null();
110            let mut data: *const std::os::raw::c_char = std::ptr::null();
111            let mut line: std::os::raw::c_int = 0;
112            let mut flags: std::os::raw::c_int = 0;
113
114            let code = unsafe {
115                sys::ERR_get_error_all(
116                    std::ptr::addr_of_mut!(file),
117                    std::ptr::addr_of_mut!(line),
118                    std::ptr::addr_of_mut!(func),
119                    std::ptr::addr_of_mut!(data),
120                    std::ptr::addr_of_mut!(flags),
121                )
122            };
123
124            if code == 0 {
125                break;
126            }
127
128            // Reason and lib strings — static C strings, safe to borrow briefly.
129            let reason = unsafe {
130                let p = sys::ERR_reason_error_string(code);
131                if p.is_null() {
132                    None
133                } else {
134                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
135                }
136            };
137
138            let lib_name = unsafe {
139                let p = sys::ERR_lib_error_string(code);
140                if p.is_null() {
141                    None
142                } else {
143                    Some(CStr::from_ptr(p).to_string_lossy().into_owned())
144                }
145            };
146
147            let file_str = unsafe {
148                if file.is_null() {
149                    None
150                } else {
151                    Some(CStr::from_ptr(file).to_string_lossy().into_owned())
152                }
153            };
154
155            let func_str = unsafe {
156                if func.is_null() {
157                    None
158                } else {
159                    Some(CStr::from_ptr(func).to_string_lossy().into_owned())
160                }
161            };
162
163            // `ERR_TXT_STRING` flag means data is a human-readable string.
164            let data_str = unsafe {
165                if data.is_null() || (flags & 0x02) == 0 {
166                    None
167                } else {
168                    Some(CStr::from_ptr(data).to_string_lossy().into_owned())
169                }
170            };
171
172            errors.push(Error {
173                code,
174                reason,
175                lib: lib_name,
176                file: file_str,
177                func: func_str,
178                data: data_str,
179            });
180        }
181
182        ErrorStack(errors)
183    }
184
185    /// Returns `true` if the stack contains no errors.
186    #[must_use]
187    pub fn is_empty(&self) -> bool {
188        self.0.is_empty()
189    }
190
191    /// Number of error records.
192    #[must_use]
193    pub fn len(&self) -> usize {
194        self.0.len()
195    }
196
197    /// Iterate over the individual error records (oldest first).
198    pub fn errors(&self) -> impl Iterator<Item = &Error> {
199        self.0.iter()
200    }
201}
202
203impl fmt::Display for ErrorStack {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        for (i, e) in self.0.iter().enumerate() {
206            if i > 0 {
207                f.write_str("; ")?;
208            }
209            fmt::Display::fmt(e, f)?;
210        }
211        Ok(())
212    }
213}
214
215impl std::error::Error for ErrorStack {}
216
217// ── Cross-thread error state ──────────────────────────────────────────────────
218
219/// A snapshot of the thread-local error queue, suitable for moving across
220/// thread boundaries.
221///
222/// Use this when an OpenSSL error occurs on a worker thread and needs to be
223/// reported to the caller thread.
224///
225/// Requires OpenSSL 3.2+ (`OSSL_ERR_STATE_new/save/restore/free`).
226#[cfg(ossl320)]
227pub struct ErrState {
228    ptr: *mut sys::ERR_STATE,
229}
230
231#[cfg(ossl320)]
232impl ErrState {
233    /// Capture the current thread's error queue into a new `ErrState`.
234    ///
235    /// Returns `None` if OpenSSL cannot allocate the state object.
236    #[must_use]
237    pub fn capture() -> Option<Self> {
238        let ptr = unsafe { sys::OSSL_ERR_STATE_new() };
239        if ptr.is_null() {
240            return None;
241        }
242        unsafe { sys::OSSL_ERR_STATE_save(ptr) };
243        Some(ErrState { ptr })
244    }
245
246    /// Restore this state onto the current thread's error queue, then drain it.
247    ///
248    /// Consumes `self`.
249    #[must_use]
250    pub fn restore_and_drain(self) -> ErrorStack {
251        unsafe { sys::OSSL_ERR_STATE_restore(self.ptr) };
252        // Prevent Drop from double-freeing — we handle free here.
253        let ptr = self.ptr;
254        std::mem::forget(self);
255        unsafe { sys::OSSL_ERR_STATE_free(ptr) };
256        ErrorStack::drain()
257    }
258}
259
260#[cfg(ossl320)]
261impl Drop for ErrState {
262    fn drop(&mut self) {
263        unsafe { sys::OSSL_ERR_STATE_free(self.ptr) };
264    }
265}
266
267// SAFETY: `OSSL_ERR_STATE` is designed for cross-thread transfer.
268#[cfg(ossl320)]
269unsafe impl Send for ErrState {}
270
271// ── Convenience macros ────────────────────────────────────────────────────────
272
273/// Call an OpenSSL function that returns 1 on success.
274///
275/// On failure drains the error queue and returns `Err(ErrorStack)`.
276///
277/// ```ignore
278/// ossl_call!(EVP_DigestInit_ex2(ctx.ptr, alg.as_ptr(), params_ptr))?;
279/// ```
280// SAFETY rationale for `macro_metavars_in_unsafe`: both `ossl_call!` and
281// `ossl_ptr!` are crate-internal wrappers for OpenSSL FFI functions.  Every
282// macro invocation in this crate passes a literal `sys::*` FFI call; no
283// caller-controlled safe expression is ever expanded inside `unsafe {}`.
284#[macro_export]
285macro_rules! ossl_call {
286    ($expr:expr) => {{
287        #[allow(clippy::macro_metavars_in_unsafe)]
288        let rc = unsafe { $expr };
289        if rc == 1 {
290            Ok(())
291        } else {
292            Err($crate::error::ErrorStack::drain())
293        }
294    }};
295}
296
297/// Call an OpenSSL function that returns a non-null pointer on success.
298///
299/// On null returns `Err(ErrorStack)`.
300///
301/// ```ignore
302/// let ptr = ossl_ptr!(EVP_MD_CTX_new())?;
303/// ```
304#[macro_export]
305macro_rules! ossl_ptr {
306    ($expr:expr) => {{
307        #[allow(clippy::macro_metavars_in_unsafe)]
308        let ptr = unsafe { $expr };
309        if ptr.is_null() {
310            Err($crate::error::ErrorStack::drain())
311        } else {
312            Ok(ptr)
313        }
314    }};
315}
316
317// ── Tests ─────────────────────────────────────────────────────────────────────
318
319#[cfg(test)]
320mod tests {
321    use super::*;
322
323    #[test]
324    fn drain_empty_queue_gives_empty_stack() {
325        // Clear any residual errors from previous tests.
326        unsafe { sys::ERR_clear_error() };
327        let stack = ErrorStack::drain();
328        assert!(stack.is_empty());
329    }
330
331    #[test]
332    fn failed_fetch_populates_error_stack() {
333        unsafe { sys::ERR_clear_error() };
334
335        // EVP_MD_fetch with a nonexistent algorithm always fails and pushes errors.
336        let ptr = unsafe {
337            sys::EVP_MD_fetch(
338                std::ptr::null_mut(),
339                c"NONEXISTENT_ALGO_XYZ".as_ptr(),
340                std::ptr::null(),
341            )
342        };
343        assert!(ptr.is_null());
344
345        let stack = ErrorStack::drain();
346        assert!(!stack.is_empty(), "expected at least one error record");
347        // After drain the queue must be empty again.
348        let second = ErrorStack::drain();
349        assert!(second.is_empty());
350    }
351
352    #[cfg(ossl320)]
353    #[test]
354    fn err_state_round_trip() {
355        unsafe { sys::ERR_clear_error() };
356
357        // Generate an error on this thread.
358        unsafe {
359            sys::EVP_MD_fetch(
360                std::ptr::null_mut(),
361                c"NONEXISTENT_ALGO_XYZ".as_ptr(),
362                std::ptr::null(),
363            );
364        }
365
366        let state = ErrState::capture().expect("OSSL_ERR_STATE_new failed");
367        // After capture, this thread's queue may or may not be cleared depending
368        // on OpenSSL version.  At minimum the restore must succeed.
369        let stack = state.restore_and_drain();
370        assert!(!stack.is_empty());
371    }
372}