1use std::panic::{AssertUnwindSafe, catch_unwind};
23
24use crate::boundary::{PluginError, PluginErrorCode, PluginResult};
25
26pub fn guard<T>(f: impl FnOnce() -> Result<T, PluginError>) -> PluginResult<T> {
31 let result = catch_unwind(AssertUnwindSafe(f));
32 match result {
33 Ok(Ok(t)) => PluginResult::Ok(t),
34 Ok(Err(e)) => PluginResult::Err(e),
35 Err(payload) => {
36 let message = panic_message(payload.as_ref());
37 drop_payload(payload);
38 PluginResult::Err(PluginError::new(PluginErrorCode::Panic, message))
39 }
40 }
41}
42
43pub fn guard_infallible<T>(thunk_name: &str, f: impl FnOnce() -> T) -> T {
51 match catch_unwind(AssertUnwindSafe(f)) {
52 Ok(t) => t,
53 Err(payload) => {
54 let msg = panic_message(payload.as_ref());
55 drop_payload(payload);
56 log::error!(
57 target: "nautilus_plugin",
58 "plug-in panicked in `{thunk_name}` thunk; aborting process: {msg}",
59 );
60 std::process::abort();
61 }
62 }
63}
64
65pub fn guard_or_null<T>(thunk_name: &str, f: impl FnOnce() -> *mut T) -> *mut T {
71 match catch_unwind(AssertUnwindSafe(f)) {
72 Ok(ptr) => ptr,
73 Err(payload) => {
74 let msg = panic_message(payload.as_ref());
75 drop_payload(payload);
76 catch_unwind(AssertUnwindSafe(|| {
79 log::error!(
80 target: "nautilus_plugin",
81 "plug-in panicked in `{thunk_name}` thunk; returning null: {msg}",
82 );
83 }))
84 .unwrap_or_else(drop_payload);
85 std::ptr::null_mut()
86 }
87 }
88}
89
90pub fn guard_drop(thunk_name: &str, f: impl FnOnce()) {
96 if let Err(payload) = catch_unwind(AssertUnwindSafe(f)) {
97 let msg = panic_message(payload.as_ref());
98 drop_payload(payload);
99 catch_unwind(AssertUnwindSafe(|| {
102 log::error!(
103 target: "nautilus_plugin",
104 "plug-in panicked in `{thunk_name}` thunk; value leaked: {msg}",
105 );
106 }))
107 .unwrap_or_else(drop_payload);
108 }
109}
110
111pub fn drop_payload(payload: Box<dyn std::any::Any + Send>) {
120 if let Err(nested) = catch_unwind(AssertUnwindSafe(move || drop(payload))) {
121 #[allow(
123 clippy::mem_forget,
124 reason = "the replacement payload can also panic on drop"
125 )]
126 std::mem::forget(nested);
127 }
128}
129
130pub(crate) fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
131 if let Some(s) = payload.downcast_ref::<&'static str>() {
132 (*s).to_string()
133 } else if let Some(s) = payload.downcast_ref::<String>() {
134 s.clone()
135 } else {
136 "plug-in panicked with non-string payload".to_string()
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use std::sync::atomic::{AtomicUsize, Ordering};
143
144 use rstest::rstest;
145
146 use super::*;
147
148 #[rstest]
149 fn returns_ok_on_success() {
150 let r = guard(|| Ok::<u32, PluginError>(7));
151 assert_eq!(r.into_result().unwrap(), 7);
152 }
153
154 #[rstest]
155 fn returns_err_on_returned_error() {
156 let r = guard(|| Err::<u32, _>(PluginError::generic("boom")));
157 let e = r.into_result().unwrap_err();
158 assert_eq!(e.code, PluginErrorCode::Generic);
159 assert_eq!(e.message_string(), "boom");
160 }
161
162 #[rstest]
163 fn returns_err_on_string_panic() {
164 let r = guard(|| -> Result<u32, PluginError> { panic!("oops") });
165 let e = r.into_result().unwrap_err();
166 assert_eq!(e.code, PluginErrorCode::Panic);
167 assert!(e.message_string().contains("oops"));
168 }
169
170 #[rstest]
171 fn returns_err_on_non_string_panic() {
172 let r = guard(|| -> Result<u32, PluginError> {
173 std::panic::panic_any(42_u32);
174 });
175 let e = r.into_result().unwrap_err();
176 assert_eq!(e.code, PluginErrorCode::Panic);
177 assert!(e.message_string().contains("non-string"));
178 }
179
180 #[rstest]
181 fn guard_infallible_returns_inner_on_success() {
182 let v = guard_infallible("test", || 42u64);
183 assert_eq!(v, 42);
184 }
185
186 #[rstest]
187 fn guard_or_null_returns_inner_on_success() {
188 let boxed = Box::into_raw(Box::new(7u32));
189 let v = guard_or_null("test", || boxed);
190 assert_eq!(v, boxed);
191 unsafe { drop(Box::from_raw(boxed)) };
193 }
194
195 #[rstest]
196 fn guard_or_null_returns_null_on_panic() {
197 let v: *mut u32 = guard_or_null("test", || panic!("create panic"));
198 assert!(v.is_null());
199 }
200
201 #[rstest]
202 fn guard_drop_runs_inner_on_success() {
203 let mut ran = false;
204 guard_drop("test", || ran = true);
205 assert!(ran);
206 }
207
208 #[rstest]
209 fn guard_drop_swallows_panic() {
210 guard_drop("test", || panic!("drop panic"));
211 }
212
213 #[rstest]
214 fn drop_payload_swallows_panicking_drop() {
215 use std::{
220 any::Any,
221 sync::atomic::{AtomicUsize, Ordering},
222 };
223
224 static DROPS_OBSERVED: AtomicUsize = AtomicUsize::new(0);
225 struct Bomb;
226 impl Drop for Bomb {
227 fn drop(&mut self) {
228 DROPS_OBSERVED.fetch_add(1, Ordering::SeqCst);
229 panic!("drop panic");
230 }
231 }
232 DROPS_OBSERVED.store(0, Ordering::SeqCst);
233
234 let payload: Box<dyn Any + Send> = Box::new(Bomb);
235 drop_payload(payload);
236 assert_eq!(DROPS_OBSERVED.load(Ordering::SeqCst), 1);
237 }
238
239 #[rstest]
240 fn guard_survives_panic_any_with_panicking_drop() {
241 static DROPS_OBSERVED: AtomicUsize = AtomicUsize::new(0);
246 struct Bomb;
247 impl Drop for Bomb {
248 fn drop(&mut self) {
249 DROPS_OBSERVED.fetch_add(1, Ordering::SeqCst);
250 panic!("drop panic");
251 }
252 }
253
254 DROPS_OBSERVED.store(0, Ordering::SeqCst);
255 let r = guard(|| -> Result<u32, PluginError> {
256 std::panic::panic_any(Bomb);
257 });
258 let e = r.into_result().unwrap_err();
259 assert_eq!(e.code, PluginErrorCode::Panic);
260
261 assert_eq!(DROPS_OBSERVED.load(Ordering::SeqCst), 1);
263 }
264
265 #[rstest]
266 fn guard_contains_successive_panicking_payload_destructors() {
267 use std::sync::Arc;
268
269 struct Payload {
270 drops: Arc<AtomicUsize>,
271 }
272
273 impl Drop for Payload {
274 fn drop(&mut self) {
275 self.drops.fetch_add(1, Ordering::SeqCst);
276 std::panic::panic_any(Self {
277 drops: Arc::clone(&self.drops),
278 });
279 }
280 }
281
282 let drops = Arc::new(AtomicUsize::new(0));
283 let result = catch_unwind(AssertUnwindSafe(|| {
284 guard(|| -> Result<(), PluginError> {
285 std::panic::panic_any(Payload {
286 drops: Arc::clone(&drops),
287 });
288 })
289 }));
290 let result = match result {
291 Ok(result) => Some(result.into_result().unwrap_err()),
292 Err(payload) => {
293 #[allow(
295 clippy::mem_forget,
296 reason = "preserve the assertion failure without another panic"
297 )]
298 std::mem::forget(payload);
299 None
300 }
301 };
302
303 assert_eq!(drops.load(Ordering::SeqCst), 1);
304 let error = result.expect("panic cleanup must not unwind out of the guard");
305 assert_eq!(error.code, PluginErrorCode::Panic);
306 assert_eq!(
307 error.message_string(),
308 "plug-in panicked with non-string payload"
309 );
310 }
311
312 #[rstest]
313 fn guards_contain_panicking_logger_payloads() {
314 const CHILD: &str = "NAUTILUS_TEST_PANIC_LOGGER_CHILD";
315 static DROPS: AtomicUsize = AtomicUsize::new(0);
316 struct Payload;
317 impl Drop for Payload {
318 fn drop(&mut self) {
319 DROPS.fetch_add(1, Ordering::SeqCst);
320 std::panic::panic_any(Self);
321 }
322 }
323 struct Logger;
324 impl log::Log for Logger {
325 fn enabled(&self, _: &log::Metadata<'_>) -> bool {
326 true
327 }
328 fn log(&self, _: &log::Record<'_>) {
329 std::panic::panic_any(Payload);
330 }
331 fn flush(&self) {}
332 }
333 static LOGGER: Logger = Logger;
334 extern "C" fn exercise() {
335 let pointer = guard_or_null::<u8>("logger", || panic!("constructor panic"));
336 assert!(pointer.is_null());
337 guard_drop("logger", || panic!("destructor panic"));
338 }
339
340 if std::env::var_os(CHILD).is_none() {
341 let current = std::thread::current();
342 let name = current.name().expect("the test harness names its thread");
343 let output = std::process::Command::new(std::env::current_exe().unwrap())
344 .args(["--exact", name])
345 .env(CHILD, "1")
346 .output()
347 .unwrap();
348 assert!(
349 output.status.success(),
350 "{}",
351 String::from_utf8_lossy(&output.stderr)
352 );
353 return;
354 }
355
356 log::set_logger(&LOGGER).unwrap();
357 log::set_max_level(log::LevelFilter::Error);
358
359 exercise();
360
361 assert_eq!(DROPS.load(Ordering::SeqCst), 2);
362 }
363}