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}