Skip to main content

mysql_handler/
panic_guard.rs

1// Copyright (C) 2026 ren-yamanashi
2//
3// This program is free software; you can redistribute it and/or modify
4// it under the terms of the GNU General Public License, version 2.0,
5// as published by the Free Software Foundation.
6//
7// This program is designed to work with certain software (including
8// but not limited to OpenSSL) that is licensed under separate terms,
9// as designated in a particular file or component or in included license
10// documentation. The authors of this program hereby grant you an additional
11// permission to link the program and your derivative works with the
12// separately licensed software that they have either included with
13// the program or referenced in the documentation.
14//
15// This program is distributed in the hope that it will be useful,
16// but WITHOUT ANY WARRANTY; without even the implied warranty of
17// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18// GNU General Public License for more details.
19//
20// You should have received a copy of the GNU General Public License
21// along with this program; if not, see <https://www.gnu.org/licenses/>.
22
23//! FFI boundary panic safety. Every `extern "C"` callback exposed to the C++
24//! shim must funnel its body through one of these helpers so that a Rust panic
25//! cannot unwind across the language boundary and abort the MySQL server.
26
27use std::panic::{AssertUnwindSafe, catch_unwind};
28
29use crate::engine::EngineError;
30
31const PANIC_LOG_MSG: &str = "ffi boundary caught panic from storage engine";
32
33/// Panic-safe entry point shared by every `extern "C"` callback this crate
34/// exposes. Zero-sized; the methods are associated functions grouped here by
35/// responsibility.
36#[derive(Debug)]
37#[non_exhaustive]
38pub struct FfiBoundary;
39
40impl FfiBoundary {
41    /// Run `f` inside `catch_unwind` and project the outcome to a MySQL
42    /// `HA_ERR_*` integer suitable for an `extern "C"` return:
43    ///
44    /// - `Ok(Ok(()))` → `0`
45    /// - `Ok(Err(e))` → `e.to_mysql_errno()`
46    /// - `Err(_)` (panic) → `HA_ERR_INTERNAL_ERROR`
47    ///
48    /// `#[inline]` lets the per-callback wrapping fold into each
49    /// `rust__handler__*` site; see `benches/callback_overhead/` for
50    /// the per-call measurements that justify the hint.
51    #[inline]
52    pub fn run<F>(f: F) -> i32
53    where
54        F: FnOnce() -> Result<(), EngineError>,
55    {
56        match catch_unwind(AssertUnwindSafe(f)) {
57            Ok(Ok(())) => 0,
58            Ok(Err(e)) => e.to_mysql_errno(),
59            Err(_) => {
60                tracing::error!("{PANIC_LOG_MSG}");
61                EngineError::Internal.to_mysql_errno()
62            }
63        }
64    }
65
66    /// Variant for callbacks whose C++ signature returns void. Panics are
67    /// swallowed (and logged) so the server stays alive; errors cannot be
68    /// reported back to MySQL through a void return.
69    ///
70    /// `#[inline]` lets the per-callback wrapping fold into each
71    /// `rust__handler__*` site; see `benches/callback_overhead/` for
72    /// the per-call measurements that justify the hint.
73    #[inline]
74    pub fn run_void<F>(f: F)
75    where
76        F: FnOnce(),
77    {
78        match catch_unwind(AssertUnwindSafe(f)) {
79            Ok(()) => {}
80            Err(_) => tracing::error!("{PANIC_LOG_MSG}"),
81        }
82    }
83
84    /// Variant for callbacks that return a non-`Result` value (pointer, flag
85    /// bitfield, etc.). Returns `default` on panic so the C++ side always
86    /// observes a well-defined value rather than an unwound stack.
87    ///
88    /// `#[inline]` lets the per-callback wrapping fold into each
89    /// `rust__handler__*` site; see `benches/callback_overhead/` for
90    /// the per-call measurements that justify the hint.
91    #[inline]
92    pub fn run_default<T, F>(default: T, f: F) -> T
93    where
94        F: FnOnce() -> T,
95    {
96        match catch_unwind(AssertUnwindSafe(f)) {
97            Ok(v) => v,
98            Err(_) => {
99                tracing::error!("{PANIC_LOG_MSG}");
100                default
101            }
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use crate::sys;
110
111    #[test]
112    fn ok_closure_returns_zero() {
113        assert_eq!(FfiBoundary::run(|| Ok(())), 0);
114    }
115
116    #[test]
117    fn err_closure_returns_mapped_errno() {
118        assert_eq!(
119            FfiBoundary::run(|| Err(EngineError::EndOfFile)),
120            sys::HA_ERR_END_OF_FILE
121        );
122    }
123
124    #[test]
125    fn panicking_closure_returns_internal_error() {
126        let prev_hook = std::panic::take_hook();
127        std::panic::set_hook(Box::new(|_| {}));
128        let result = FfiBoundary::run(|| panic!("intentional panic for test"));
129        std::panic::set_hook(prev_hook);
130        assert_eq!(result, sys::HA_ERR_INTERNAL_ERROR);
131    }
132
133    #[test]
134    fn panicking_void_closure_does_not_abort() {
135        let prev_hook = std::panic::take_hook();
136        std::panic::set_hook(Box::new(|_| {}));
137        FfiBoundary::run_void(|| panic!("intentional panic for test"));
138        std::panic::set_hook(prev_hook);
139    }
140}