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}