doom_fish_utils/
panic_safe.rs1use std::any::Any;
25use std::panic::AssertUnwindSafe;
26
27pub fn catch_user_panic<F: FnOnce()>(site: &str, f: F) {
50 let _ = catch_user_panic_result(site, f);
51}
52
53#[must_use = "return an ABI-safe fallback when the callback panics"]
67pub fn catch_user_panic_result<R, F: FnOnce() -> R>(site: &str, f: F) -> Option<R> {
68 let boundary_result = std::panic::catch_unwind(AssertUnwindSafe(|| {
69 match std::panic::catch_unwind(AssertUnwindSafe(f)) {
70 Ok(result) => Some(result),
71 Err(payload) => {
72 log_callback_panic(site, payload.as_ref());
73 drop(payload);
74 None
75 }
76 }
77 }));
78
79 match boundary_result {
80 Ok(result) => result,
81 Err(payload) => {
82 log_callback_panic(site, payload.as_ref());
83 drop_payload_best_effort(payload);
84 None
85 }
86 }
87}
88
89#[must_use = "return an ABI-safe fallback when any protected phase panics"]
109pub fn catch_user_panic_result_with_cleanup<S, R, F, C>(
110 site: &str,
111 mut state: S,
112 mut f: F,
113 mut cleanup: C,
114) -> Option<R>
115where
116 F: FnMut(&mut S) -> R,
117 C: FnMut(&mut S),
118{
119 let callback_result = catch_user_panic_result(site, || f(&mut state));
120 let cleanup_succeeded = catch_user_panic_result(site, || cleanup(&mut state)).is_some();
121 let callback_drop_succeeded = catch_user_panic_result(site, || drop(f)).is_some();
122 let cleanup_drop_succeeded = catch_user_panic_result(site, || drop(cleanup)).is_some();
123 let state_drop_succeeded = catch_user_panic_result(site, || drop(state)).is_some();
124
125 if callback_drop_succeeded
126 && cleanup_succeeded
127 && cleanup_drop_succeeded
128 && state_drop_succeeded
129 {
130 callback_result
131 } else {
132 if let Some(result) = callback_result {
133 catch_user_panic(site, || drop(result));
134 }
135 None
136 }
137}
138
139pub fn log_callback_panic(site: &str, payload: &(dyn Any + Send)) {
145 let result = std::panic::catch_unwind(AssertUnwindSafe(|| {
146 let message = payload.downcast_ref::<&'static str>().map_or_else(
147 || {
148 payload
149 .downcast_ref::<String>()
150 .map_or("<non-string panic payload>", String::as_str)
151 },
152 |message| *message,
153 );
154 eprintln!("doom-fish-utils: panic in {site} caught at C ABI boundary: {message}");
155 }));
156 if let Err(payload) = result {
157 drop_payload_best_effort(payload);
158 }
159}
160
161fn drop_payload_best_effort(payload: Box<dyn Any + Send>) {
162 if let Err(undroppable_payload) = std::panic::catch_unwind(AssertUnwindSafe(|| drop(payload))) {
163 std::mem::forget(undroppable_payload);
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use std::panic::{catch_unwind, panic_any, AssertUnwindSafe};
173 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
174 use std::sync::Arc;
175
176 use super::{catch_user_panic, catch_user_panic_result, catch_user_panic_result_with_cleanup};
177
178 struct DropFlag(Arc<AtomicBool>);
179
180 impl Drop for DropFlag {
181 fn drop(&mut self) {
182 self.0.store(true, Ordering::SeqCst);
183 }
184 }
185
186 #[test]
187 fn normal_panic_payload_is_dropped() {
188 let dropped = Arc::new(AtomicBool::new(false));
189 let payload = DropFlag(Arc::clone(&dropped));
190
191 catch_user_panic("normal_panic_payload_is_dropped", move || {
192 panic_any(payload);
193 });
194
195 assert!(dropped.load(Ordering::SeqCst));
196 }
197
198 #[test]
199 fn result_helper_preserves_success_and_maps_panic_to_none() {
200 assert_eq!(catch_user_panic_result("result success", || 42), Some(42));
201 assert_eq!(
202 catch_user_panic_result("result panic", || -> u32 {
203 panic!("result callback panic");
204 }),
205 None
206 );
207 }
208
209 #[test]
210 fn cleanup_helper_preserves_successful_result() {
211 let cleanup_ran = Arc::new(AtomicBool::new(false));
212 let cleanup_ran_in_closure = Arc::clone(&cleanup_ran);
213
214 let result = catch_user_panic_result_with_cleanup(
215 "cleanup_helper_preserves_successful_result",
216 40_u32,
217 |state| *state + 2,
218 move |_state| {
219 cleanup_ran_in_closure.store(true, Ordering::SeqCst);
220 },
221 );
222
223 assert_eq!(result, Some(42));
224 assert!(cleanup_ran.load(Ordering::SeqCst));
225 }
226
227 #[test]
228 fn captured_release_runs_during_callback_unwind() {
229 let released = Arc::new(AtomicBool::new(false));
230 let guard = DropFlag(Arc::clone(&released));
231
232 catch_user_panic("captured_release_runs_during_callback_unwind", move || {
233 let _guard = guard;
234 panic!("callback panic");
235 });
236
237 assert!(released.load(Ordering::SeqCst));
238 }
239
240 struct PanicOnDrop;
241
242 impl Drop for PanicOnDrop {
243 fn drop(&mut self) {
244 panic!("panic while dropping panic payload");
245 }
246 }
247
248 #[test]
249 fn single_panic_payload_destructor_is_contained() {
250 let result = catch_unwind(AssertUnwindSafe(|| {
251 catch_user_panic("single_panic_payload_destructor_is_contained", || {
252 panic_any(PanicOnDrop);
253 });
254 }));
255
256 assert!(result.is_ok());
257 }
258
259 struct ReleaseGuard {
260 released: Arc<AtomicBool>,
261 }
262
263 impl Drop for ReleaseGuard {
264 fn drop(&mut self) {
265 self.released.store(true, Ordering::SeqCst);
266 panic!("panic in captured release");
267 }
268 }
269
270 #[test]
271 fn single_capture_drop_panic_after_normal_return_is_contained() {
272 let released = Arc::new(AtomicBool::new(false));
273 let guard = ReleaseGuard {
274 released: Arc::clone(&released),
275 };
276
277 let result = catch_unwind(AssertUnwindSafe(|| {
278 catch_user_panic(
279 "single_capture_drop_panic_after_normal_return_is_contained",
280 move || {
281 let _guard = guard;
282 },
283 );
284 }));
285
286 assert!(result.is_ok());
287 assert!(released.load(Ordering::SeqCst));
288 }
289
290 struct OrderedPanicOnDrop {
291 sequence: Arc<AtomicUsize>,
292 drop_order: Arc<AtomicUsize>,
293 }
294
295 impl Drop for OrderedPanicOnDrop {
296 fn drop(&mut self) {
297 let order = self.sequence.fetch_add(1, Ordering::SeqCst) + 1;
298 self.drop_order.store(order, Ordering::SeqCst);
299 panic!("single callback closure drop panic");
300 }
301 }
302
303 #[test]
304 fn cleanup_runs_before_single_callback_closure_drop_panic() {
305 let sequence = Arc::new(AtomicUsize::new(0));
306 let cleanup_order = Arc::new(AtomicUsize::new(0));
307 let drop_order = Arc::new(AtomicUsize::new(0));
308 let guard = OrderedPanicOnDrop {
309 sequence: Arc::clone(&sequence),
310 drop_order: Arc::clone(&drop_order),
311 };
312 let cleanup_sequence = Arc::clone(&sequence);
313 let cleanup_order_in_closure = Arc::clone(&cleanup_order);
314
315 let survived = catch_unwind(AssertUnwindSafe(|| {
316 let result = catch_user_panic_result_with_cleanup(
317 "cleanup_runs_before_single_callback_closure_drop_panic",
318 (),
319 move |_state| -> u32 {
320 let _guard = &guard;
321 panic!("callback panic");
322 },
323 move |_state| {
324 let order = cleanup_sequence.fetch_add(1, Ordering::SeqCst) + 1;
325 cleanup_order_in_closure.store(order, Ordering::SeqCst);
326 },
327 );
328 assert_eq!(result, None);
329 }));
330
331 assert!(survived.is_ok());
332 assert_eq!(cleanup_order.load(Ordering::SeqCst), 1);
333 assert_eq!(drop_order.load(Ordering::SeqCst), 2);
334 }
335}