Some thoughts on a less slippery catch_unwind

std::panic::catch_unwind() has the well-known[1] panic-on-drop payload footgun, a consequence of the returned Box<dyn Any + Send> payload being very easy to implicitly drop while processing the return value. I've seen a few different solutions proposed for this, including modifying the vtable and making all panicking Drop impls abort. However, these both feel like lang solutions to a library issue, which is never ideal. Instead, I've been wondering if we can take this as an opportunity to improve the catch_unwind interface. To sketch out my thoughts in the form of an API:

use std::{any::Any, panic::UnwindSafe};

// The same as `catch_unwind`, but returning an `UnwindResult` instead of a
// `thread::Result`.
pub fn catch_unwind_v2<F: FnOnce() -> R + UnwindSafe, R>(f: F) -> UnwindResult<R>;

// Effectively a `thread::Result<T>`, but with fewer ways to extract the
// `Box<dyn Any + Send>` payload by value, and with methods to assist common
// error-handling behavior.
#[must_use]
pub struct UnwindResult<T> { ... }

impl<T> UnwindResult<T> {
    /* UNWINDING FUNCTIONS */

    // Convert this `UnwindResult` into a regular `Result`. We should
    // clearly point out the panic-on-drop footgun in this function's
    // documentation.
    pub fn into_result(self) -> Result<T, Box<dyn Any + Send>>;

    // If there is a `T`, return it; otherwise, `drop` the payload and return
    // `None`, propagating any panics. We should point out in this function's
    // documentation that this may panic.
    pub fn drop_err(self) -> Option<T>;

    // If there is a `T`, return it; otherwise, call `resume_unwind` with the
    // payload.
    pub fn resume_unwind(self) -> T;

    /* UNWIND-FREE FUNCTIONS */

    // Access the payload by immutable reference, if there is one. If the
    // closure panics, propagate it.
    pub fn inspect_err<F>(self, f: F) -> UnwindResult<T>
    where
        F: FnOnce(&(dyn Any + Send + 'static));

    // Access the payload by mutable reference, if there is one. If the
    // closure panics, propagate it.
    pub fn inspect_err_mut<F>(mut self, f: F) -> UnwindResult<T>
    where
        F: FnOnce(&mut (dyn Any + Send + 'static));

    // If there is a `T`, return it; otherwise, `forget` the payload and
    // return `None`.
    pub fn forget_err(self) -> Option<T>;

    // If there is a `T`, return it; otherwise, `abort` the current process.
    pub fn unwrap_or_abort(self) -> T;

    // If there is a `T`, return it; otherwise, `drop` the payload within
    // `catch_unwind_v2` and return the result of that.
    pub fn try_drop_err(self) -> Result<T, UnwindResult<()>>;

    // If there is a `T`, return it; otherwise, try to `drop` the payload,
    // and if that panics, `forget` the next payload and return `None`.
    pub fn drop_err_or_forget(self) -> Option<T>;

    // If there is a `T`, return it; otherwise, try to `drop` the payload,
    // and if that panics, `abort` the current process.
    pub fn drop_err_or_abort(self) -> Option<T>;

    // Add `new()`, `map()`, etc. as desired with the semantics of `Result`,
    // but be careful with any methods that allow the payload to be extracted
    // by value.
}

(I'm not particularly attached to these names, I just picked something that makes sense based on Result's API. All the implementations are fairly trivial.)

The idea is, in a nounwind context, most correct programs will handle a catch_unwind payload in one of a few ways:

  • Output a (possibly payload-dependent) message, then resume_unwind the payload.
  • Output a message, move the payload across an unwind-sensitive boundary, then resume_unwind it.
  • Output a message and abort.
  • Output a message and forget the payload.
  • Output a message, try to drop the payload, and if that fails, (output a message and) abort.
  • Output a message, try to drop the payload, and if that fails, (output a message and) forget the next payload.

However, these patterns can be tedious to implement correctly, since the writer must keep very careful track of what happens to the payload. The idea of an UnwindResult is to provide safe non-panicking primitives that can be chained to provide common use cases, alongside an into_result() escape hatch for when full control is needed, and a drop_err() for contexts where further panicking is unimportant (e.g., tests that only check if a panic occurred). It would effectively act as a consuming builder API for handling the payload.

A related alternative that I've considered would be a special Payload type such that catch_unwind_v2() returns a Result<T, Payload>. This type would have direct inspect[_mut](), try_drop(), drop_or_forget(), etc. It would remove some of the indirection of an UnwindResult, but it would also be very easy to implicitly drop by calling Result::ok() or related methods. Alternatively, a Payload could have ManuallyDrop semantics, but this would have the parallel pitfall of implicit memory leaks that would show up on Miri and Valgrind. Thus, I believe an UnwindResult type with #[must_use] would be less error-prone, especially with a readily available into_result() method.

One remaining question is the proper return value of try_drop_err(). Both Result<T, UnwindResult<()>> and UnwindResult<Option<T>> make sense, but the former has the Result::ok() pitfall, and the latter can't be repeated without creating an UnwindResult<Option<Option<T>>>. Perhaps an UnwindResult<Option<T>>UnwindResult<Option<T>> method would help with the latter, but I have no clue what to name it.


Right now, the main limitation is that I don't have many examples of real catch_unwind usage, so I can only guess what patterns might be useful. What current patterns would be amenable to an approach like this, and what patterns wouldn't be? Has anyone else thought of something like this before? I'd like to hear others' thoughts on this silly idea.[2]


  1. in these circles, at least ↩︎

  2. As I understand it, the standard procedure is to start these things as third-party libraries. But I doubt I have the charisma or persistence to convince anyone to use mine, so this probably won't go anywhere. Such is life, I suppose; at least I'll have gotten this off my mind. ↩︎

6 Likes

I remember seeing it being suggested in this context. The main problem is that catch_unwind() is already too widely used, so we have to do something with this version.

Other than that,

You still have the problem with UnwindResult, even if #[must_use] helps (and Result is already #[must_use]). I think it is perfectly fine for the destructor of Payload to have drop_err_or_forget() semantics. Panic-on-payload is exceedingly rare, and leaking on that case is fine.

The problem with a regular Result is how easy it is to implicitly drop the Err while still "using" it; for instance:

  • Call result.is_ok(), then never touch it again. It falls out of scope and drops the Err.
  • Destructure with match result { Ok(v) => ..., Err(e) => ... }, then only use e by reference. e falls out of scope and implicitly drops. (A #[must_use] Payload would help with this.)
  • Call result.unwrap_or_else(|_| ...). The closure implicitly drops the Err.

The idea is to give the #[must_use] teeth, by avoiding &self functions, and not allowing trivial by-value access to the Err. This way, the only way to use an UnwindResult without warnings is to use it in a chain terminating in into_result(), drop_err(), resume_unwind(), forget_err(), etc.

1 Like

It's been a few weeks, but I've finished my survey of how crates currently use catch_unwind() in practice. To summarize:

Clean handling methods

26 crates:

  1. Move the payload into a provided field, pointer, global variable, or thread-local variable.
  2. Optionally, set some flag to inform the caller that a panic occurred.
  3. Later, on the caller's side of the boundary, take the payload and resume_unwind.

22 crates:

  1. If a panic occurred, optionally log a message, then abort or exit the process (or equivalent).

19 crates:

  1. Do some additional processing or cleaning up, then resume_unwind.

6 crates:

  1. Optionally do some additional processing.
  2. Directly return the thread::Result (or equivalent) to the user.

3 crates:

  1. Create a panic-on-drop guard before the payload is processed.
  2. Only forget the guard after the payload has been dropped.

2 crates:

  1. If a panic occurred, independently panic again, resulting in an abort if the payload panics.

Leaky handling methods*

69 crates (at least 24 unsound):

  1. Produce a value, set some flags, or call some functions based on whether or not a panic occurred.
  2. Optionally, use downcast or downcast_ref on the payload to extract the panic message, and either log it or embed it in a value.
  3. Return and implicitly drop the payload.

13 crates (at least 5 unsound):

  1. Use let _ = catch_unwind(...); or catch_unwind(...).ok(); in an attempt to ignore all panics, which implicitly drops the payload.

4 crates:

  1. Direct control flow based on whether or not a panic occurred.
  2. At any point, implicitly drop the payload.

1 crate:

  1. Move the payload into a provided field, pointer, global variable, or thread-local variable, to be processed later.
  2. Implicitly drop the prior payload in that place.

* These methods can be permissible when the panic comes from a controlled source, e.g., from a unit test.

Methodology

I only included uses of catch_unwind() in library crates published to crates.io. I did not include uses within test cases, but I did include uses within proc macros and testing support crates. Initially, I just searched for lang:rust /\bcatch_unwind\b/ on GitHub Code Search, and added the crates from the 100 results it returned. Then, I scraped the crates.io registry directly, and I added all crate versions where:

  • the version is the latest non-prerelease, non-yanked version of the crate as of 2022-07-12;
  • the version has at least 10,000 total downloads on crates.io; and
  • the source repo is publicly available, usually on GitHub or a GitLab instance.

Many of the crates are old and unmaintained, but some are still frequently downloaded.

Raw results

These are quite messy, since I didn't have any consistent coding process. I just looked for enough details to categorize the uses and to judge their soundness.

abi_stable v0.10.4 (abi_stable_crates/abi_stable at 0dcb0681cd9ef94978150b1a68f988d780fc9f61 · rodrimati1992/abi_stable_crates · GitHub):
abi_stable::external_types::parking_lot::once::Closure::call_with_closure(): match with ref, return RResult, implicit drop (impossible?)
abi_stable::external_types::parking_lot::once::Closure::run_call()/abi_stable::external_types::parking_lot::once::ROnce::call_once(): match with ref, move into field, cross boundary, resume_unwind
abi_stable::library::__call_root_module_loader(): unwrap_or, return Result (uncontrolled, unsound!)

abi_stable_shared v0.10.3 (abi_stable_crates/abi_stable_shared at ec2638d8c37329e6bb7d47a25bee7fba2b79010b · rodrimati1992/abi_stable_crates · GitHub):
abi_stable_shared::test_utils::must_panic(): match Err(e), return Result

allo-isolate v0.1.12 (GitHub - sunshine-protocol/allo-isolate at ee2a2cb0bc1dc8a5006ced8b3c9b93343f8c084a):
allo_isolate::Isolate::catch_unwind()/<allo_isolate::catch_unwind::CatchUnwind as Future>::poll(): return Result

alpm v2.2.1 (alpm.rs/alpm at alpm-v2.2.1 · archlinux/alpm.rs · GitHub):
alpm::cb::dlcb(): let _ (uncontrolled, unsound!)
alpm::cb::eventcb(): let _ (uncontrolled, unsound!)
alpm::cb::fetchcb(): unwrap_or, return c_int (uncontrolled, unsound!)
alpm::cb::logcb(): let _ (uncontrolled, unsound!)
alpm::cb::progresscb(): let _ (uncontrolled, unsound!)
alpm::cb::questioncb(): let _ (uncontrolled, unsound!)

async-global-executor v2.2.0 (GitHub - async-rs/async-global-executor at v2.2.0):
async_global_executor::threading::thread_main_loop(): if is_ok, break, implicit drop (uncontrolled!)
async_global_executor::threading::wait_for_local_executor_completion(): if is_ok, break, implicit drop (uncontrolled!)

async-io v1.7.0 (GitHub - smol-rs/async-io at v1.7.0):
async_io::src::reactor::ReactorLock::react(): ok (uncontrolled!)
async_io::src::reactor::Source::poll_ready(): ok (uncontrolled!)

bcc v0.0.32 (GitHub - rust-bpf/rust-bcc at v0.0.32):
bcc::ring_buf::callback::raw_callback(): let _ (uncontrolled, unsound!)
bcc::perf_event::callback::raw_callback(): let _ (uncontrolled, unsound!)

blocking v1.2.0 (GitHub - smol-rs/blocking at v1.2.0):
blocking::Executor::main_loop(): ok (uncontrolled!)

boring v2.0.0 (boring/boring at db6867b794303c7666c922c7f8ef5cf3d5f58396 · cloudflare/boring · GitHub):
boring::ssl::bio::bread()/boring::ssl::SslStream::check_panic(): match Err(err), move into field, cross boundary, resume_unwind
boring::ssl::bio::bwrite()/boring::ssl::SslStream::check_panic(): match Err(err), move into field, cross boundary, resume_unwind
boring::ssl::bio::ctrl()/boring::ssl::SslStream::check_panic(): match Err(err), move into field, cross boundary, resume_unwind
boring::util::invoke_passwd_cb()/<boring::util::CallbackState as Drop>::drop(): match Err(err), move into field, cross boundary, resume_unwind

brotli v3.3.4 (https://github.com/dropbox/rust-brotli/tree/3.3.4):
brotli::ffi::compressor::catch_panic()/brotli::ffi::compressor::BrotliEncoderCompress(): match Err(panic_err), call function with ref, implicit drop (controlled)
brotli::ffi::compressor::catch_panic()/brotli::ffi::compressor::BrotliEncoderCompressStream(): match Err(panic_err), call function with ref, implicit drop (controlled)
brotli::ffi::compressor::catch_panic()/brotli::ffi::compressor::BrotliEncoderSetCustomDictionary(): if let Err(panic_err), call function with ref, implicit drop (impossible)
brotli::ffi::compressor::catch_panic()/brotli::ffi::multicompress::BrotliEncoderCompressMulti(): match Err(panic_err), call function with ref, implicit drop (controlled)
brotli::ffi::compressor::catch_panic()/brotli::ffi::multicompress::BrotliEncoderCompressWorkPool(): match Err(panic_err), call function with ref, implicit drop (controlled)
brotli::ffi::compressor::catch_panic()/brotli::ffi::multicompress::BrotliEncoderDestroyWorkPool(): if let Err(panic_err), call function with ref, implicit drop (controlled)
brotli::ffi::compressor::catch_panic_cstate()/brotli::ffi::compressor::BrotliEncoderCreateInstance(): match Err(err), call function with ref, implicit drop (controlled)
brotli::ffi::multicompress::catch_panic_wstate()/brotli::ffi::multicompress::BrotliEncoderCreateWorkPool(): match Err(err), call function with ref, implicit drop (controlled)

brotli-decompressor v2.3.2 (GitHub - dropbox/rust-brotli-decompressor at 67d75006199b2d062f8e74706cb104add012a96e):
brotli_decompressor::ffi::catch_panic()/brotli_decompressor::ffi::BrotliDecoderDecompressStream(): match Err(mut readable_err), call function with ref, set indep. field, return BrotliDecoderResult, implicit drop (impossible)
brotli_decompressor::ffi::catch_panic_state()/brotli_decompressor::ffi::BrotliDecoderCreateInstance(): match Err(mut e), call function with ref, return *mut BrotliDecoderState, implicit drop (controlled)

cairo-rs v0.15.12 (gtk-rs-core/cairo at 0.15.12 · gtk-rs/gtk-rs-core · GitHub):
cairo_rs::image_surface_png::read_func()/cairo_rs::image_surface_png::ImageSurface::create_from_png(): match Err(payload), move into field, cross boundary, resume_unwind
cairo_rs::image_surface_png::write_func()/cairo_rs::image_surface_png::ImageSurface::write_to_png(): match Err(payload), move into field, cross boundary, resume_unwind
cairo_rs::stream::write_callback()/cairo_rs::stream::Surface::finish_output_stream(): match Err(payload), move into field, cross boundary, resume_unwind

civet v0.11.0 (GitHub - jtgeibel/rust-civet at v0.11.0) [old]:
civet::raw::raw_handler(): match Err(..), return i32 (uncontrolled, unsound!)

cool_asserts v2.0.3 (GitHub - Lucretiel/cool_asserts at 2.0.3):
cool_asserts::assert_panics::assert_panics!: match Err($panic), maybe call function with ref, implicit drop (controlled)

cpython v0.7.0 (GitHub - dgrunwald/rust-cpython at 0.7.0):
cpython::function::handle_callback(): match Err(err), create AbortOnDrop guard, implicit drop, forget guard

credibility v0.1.3 (GitHub - antifuchs/credibility at v0.1.3):
credibility::test_block::TestBlock::eval_aver()/<credibility::reporter::DefaultTestReporter as TestReporter>::averred(): if is_err, set indep. field, implicit drop (controlled)
credibility::test_block::TestBlock::eval_aver()/<credibility::selftest::TestTracker as TestReporter>::averred(): call indep. function, match Err(_), set indep. field, implicit drop (controlled)
credibility::test_block::TestBlock::eval_aver(): move into trait function

crossbeam-utils v0.8.10 (crossbeam/crossbeam-utils at crossbeam-utils-0.8.10 · crossbeam-rs/crossbeam · GitHub):
crossbeam_utils::thread::scope(): call indep. functions, match Err(err), resume_unwind

cryptobox-c v1.1.3 (GitHub - wireapp/cryptobox-c at v1.1.3) [old]:
cryptobox::catch_unwind(): match Err(_), return CBoxResult, implicit drop (controlled)

csound v0.1.8 (GitHub - neithanmo/csound-rs at v0.1.8):
csound::callbacks::Trampoline::catch(): match Err(_), exit

cucumber v0.13.0 (GitHub - cucumber-rs/cucumber at v0.13.0):
cucumber::runner::basic::Executor::run_before_hook()/cucumber::runner::basic::Executor::run_scenario()/cucumber::runner::basic::Executor::emit_failed_events(): match Err((panic_info, world)), move into ExecutionFailure, call function with ref, call indep. functions, err, if let Some(exec_err), match ExecutionFailure::BeforeHookPanicked { panic_info, meta, .. }, move into Cucumber, move into channel (uncontrolled?)

curl v0.4.43 (GitHub - alexcrichton/curl-rust at 0.4.43):
curl::panic::catch()/curl::panic::propagate(): match Err(e), move into global, cross boundary, resume_unwind

directwrite v0.1.4 (GitHub - Connicpu/directwrite-rs at 39c4dea396e833920cdd4eff20138944f067a7ff):
directwrite::inline_object::vtbl::draw(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::inline_object::vtbl::get_break_conditions(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::inline_object::vtbl::get_metrics(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::inline_object::vtbl::get_overhand_metrics(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::draw_glyph_run(): match _, return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::draw_inline_object(): match _, return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::draw_strikethrough(): match _, return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::draw_underline(): match _, return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::get_current_transform(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::get_pixels_per_dip(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)
directwrite::text_renderer::vtbl::comref::is_pixel_snapping_disabled(): match Err(_), return HRESULT, implicit drop (uncontrolled, unsound!)

easy-parallel v3.2.0 (GitHub - smol-rs/easy-parallel at v3.2.0):
easy_parallel::Parallel::finish(): call indep. functions, if let Some(err), resume_unwind

emacs v0.18.0 (GitHub - ubolonton/emacs-module-rs at a9b573c3ef443e9adf04cfc349ac7b3bc91b28e2):
<emacs::func::CallEnv as HandleCall>::handle_call()/emacs::env::Env::handle_panic(): match Err(error), attempt to downcast, call function with ref, implicit drop (uncontrolled, unwound! incidentally unsound!)
emacs::init::initialize(): match Err(e), call function with ref, return c_int, implicit drop (uncontrolled, unsound! incidentally unsound!)

ert v0.2.2 (GitHub - YushiOMOTE/ert at 0fbcee6b3c377d06aada4199d28f63445e255234):
ert::router::Via::new()/<ert::router::Via as Future>::poll(): expect

execution-context v0.1.0 (GitHub - mitsuhiko/rust-execution-context at 0.1.0) [old]:
execution_context::ctx::ExecutionContext::run(): set indep. field, match Err(err), resume_unwind

festive v0.2.2 (GitHub - estk/festive at c69ce3ab08196a678d632ada7425b497b5c7c0ba):
festive::fork::fork_impl(): match Err(_), exit

ffi-support v0.4.4 (GitHub - mozilla/ffi-support at v0.4.4):
ffi_support::call_with_result_impl()/<ffi_support::error::ExternError as From<Box<dyn Any + Send + 'static>>>::from(): match Err(e), call functions with ref, return ExternError, implicit drop (uncontrolled, unsound!)

ffi-toolkit v0.5.0 (rust-fil-ffi-toolkit/ffi-toolkit at ffi-toolkit-v0.5.0 · filecoin-project/rust-fil-ffi-toolkit · GitHub):
ffi_toolkit::catch_panic_response(): match Err(panic), call function with ref, call indep. functions, return *mut T, implicit drop (uncontrolled, unsound!)

ffms2 v0.2.0 (GitHub - rust-av/ffms2-rs at 52a43f3566dfa55784fe7fc5f41d2a2cf7ac0241):
ffms2::index::Indexer::ProgressCallback::IndexCallback(): if is_err, abort

fil-rustacuda v0.1.3 (GitHub - filecoin-project/fil-rustacuda at v0.1.3):
fil_rustacuda::stream::callback_wrapper(): let _ (uncontrolled, unsound!)

findshlibs v0.10.2 (GitHub - gimli-rs/findshlibs at 0.10.2):
findshlibs::linux::SharedLibrary::callback()/findshlibs::linux::SharedLibrary::each(): match Err(panicked), move into Option field, cross boundary, if let Some(panic), resume_unwind

flaky_test v0.1.0 (GitHub - denoland/flaky_test at v0.1.0):
flaky_test::flaky_test::#name(): if is_ok, else if i == 2, (unwrap_err, resume_unwind), else implicit drop (controlled)

frame-benchmarking v3.1.0 (substrate/frame/benchmarking at f6de92e1f353be3b88a575187a22a3827a823bf2 · paritytech/substrate · GitHub):
frame_benchmarking::impl_benchmark_test_suite!::benchmark_tests::test_benchmarks(): if let Err(err), call function with ref, set indep. field, implicit drop (controlled)

fruity v0.3.0 (GitHub - nvzqz/fruity at v0.3.0):
fruity::dispatch::queue::DispatchQueue::apply(): match Err(_error), abort
fruity::dispatch::queue::DispatchQueue::apply_auto(): match Err(_error), abort
fruity::dispatch::queue::DispatchQueue::spawn_async(): match Err(_error), abort
fruity::dispatch::queue::DispatchQueue::spawn_sync(): cross boundary, match Err(error), resume_unwind

fruity__bbqsrc v0.2.0 (GitHub - nvzqz/fruity at eadc4f576936c554ac2b9935679281230040c644):
fruity__bbqsrc::dispatch::queue::DispatchQueue::apply(): match Err(_error), abort
fruity__bbqsrc::dispatch::queue::DispatchQueue::apply_auto(): match Err(_error), abort
fruity__bbqsrc::dispatch::queue::DispatchQueue::spawn_async(): match Err(_error), abort
fruity__bbqsrc::dispatch::queue::DispatchQueue::spawn_sync(): cross boundary, match Err(error), resume_unwind

futures-cpupool v0.1.8 (futures-rs/futures-cpupool at futures-cpupool-0.1.8 · rust-lang/futures-rs · GitHub):
futures_cpupool::CpuPool::spawn()/<futures_cpupool::CpuFuture as Future>::poll(): resume_unwind

futures-executor-preview v0.2.2 (futures-rs/futures-executor at 0.2.2 · rust-lang/futures-rs · GitHub):
<futures_executor_preview::spawn::SpawnWithHandle as Future>::poll()/<futures_executor_preview::spawn::JoinHandle as Future>::poll(): cross boundary, match Err(e), resume_unwind

futures-lite v1.12.0 (GitHub - smol-rs/futures-lite at v1.12.0):
futures_lite::future::FutureExt::catch_unwind()/<futures_lite::future::CatchUnwind as Future>::poll(): return Result

futures-util v0.3.21 (futures-rs/futures-util at 0.2.2 · rust-lang/futures-rs · GitHub):
futures_util::future::future::FutureExt::catch_unwind()/<futures_util::future::future::catch_unwind::CatchUnwind as Future>::poll(): return Result
futures_util::future::future::FutureExt::remote_handle()/<futures_util::future::future::remote_handle::RemoteHandle as Future>::poll(): resume_unwind
futures_util::stream::stream::StreamExt::catch_unwind()/<futures_util::stream::stream::catch_unwind::CatchUnwind as Stream>::poll_next(): set indep. field, return Result

futures-util-preview v0.2.2 (futures-rs/futures-util at 0.3.21 · rust-lang/futures-rs · GitHub):
futures_util::future::FutureExt::catch_unwind()/<futures_util::future::catch_unwind::CatchUnwind as Future>::poll(): return Result
futures_util::stream::StreamExt::catch_unwind()/<futures_util::stream::catch_unwind::CatchUnwind as Stream>::poll_next(): return Result

galvanic-assert v0.8.7 (GitHub - mindsbackyard/galvanic-assert at 33855f4a2ef829ddd5652e7edbbf0249dddf77b6):
galvanic_assert::assert_that!($actual:expr, does not panic): if is_err, call indep. function, implicit drop (controlled)
galvanic_assert::assert_that!($actual:expr, panics): if is_ok, else implicit drop (controlled)
galvanic_assert::get_expectation_for!($actual:expr, does not panic): if is_err, else return Expectation, implicit drop (controlled)
galvanic_assert::get_expectation_for!($actual:expr, panics): if is_ok, else return Expectation, implicit drop (controlled)

galvanic-test v0.2.0 (GitHub - mindsbackyard/galvanic-test at v0.2.0):
galvanic_test::test!: if is_err, call indep. functions, implicit drop (controlled)

generator v0.7.0 (GitHub - Xudong-Huang/generator-rs at 0.7.0):
generator::gen_impl::gen_init()/generator::gen_impl::GeneratorImpl::resume_gen(): if let Err(cause), call function with ref, call indep. function, move into local, cross boundary, if let Some(err), resume_unwind

gio v0.15.12 (gtk-rs-core/gio at 0.15.12 · gtk-rs/gtk-rs-core · GitHub):
gio::read_input_stream::AnyReader::with_inner()/gio::read_input_stream::ReadInputStream::close_and_take(): match Err(panic), move into field, cross boundary, resume_unwind
gio::write_output_stream::AnyWriter::with_inner()/gio::write_output_stream::WriteOutputStream::close_and_take(): match Err(panic), move into field, cross boundary, resume_unwind

git2 v0.14.4 (GitHub - rust-lang/git2-rs at 0.14.4):
git2::panic::wrap()/git2::panic::check(): match Err(e), move into global Option, cross boundary, if let Some(err), resume_unwind

glow v0.11.2 (GitHub - grovesNL/glow at 3bf1f8acef58edaeda3275b53b3cc00d7615d7f0):
glow::native::raw_debug_message_callback(): ok (uncontrolled, unsound!)

gstreamer v0.18.7 (gstreamer · 0.18.7 · GStreamer / gstreamer-rs · GitLab):
gstreamer::subclass::error::panic_to_error!: match Err(err), call functions with ref, return $ret, implicit drop (uncontrolled, unsound!)
gstreamer::subclass::plugin_1_12::plugin_define!::plugin_desc::plugin_init_trampoline(): match Err(err), call functions with ref, return gboolean, implicit drop (uncontrolled, unsound!)
gstreamer::subclass::plugin_1_14::plugin_define!::plugin_desc::plugin_init_trampoline(): match Err(err), call functions with ref, return gboolean, implicit drop (uncontrolled, unsound!)

gstreamer-app v0.18.7 (gstreamer-app · 0.18.7 · GStreamer / gstreamer-rs · GitLab):
gstreamer_app::app_sink::trampoline_eos(): match Err(err), set indep. field, call function with ref, implicit drop (uncontrolled, unsound!)
gstreamer_app::app_sink::trampoline_new_event(): match Err(err), set indep. field, call function with ref, return gboolean, implicit drop (uncontrolled, unsound!)
gstreamer_app::app_sink::trampoline_new_preroll(): match Err(err), set indep. field, call function with ref, return GstFlowReturn, implicit drop (uncontrolled, unsound!)
gstreamer_app::app_sink::trampoline_new_sample(): match Err(err), set indep. field, call function with ref, return GstFlowReturn, implicit drop (uncontrolled, unsound!)
gstreamer_app::app_src::trampoline_enough_data(): match Err(err), set indep. field, call function with ref, implicit drop (uncontrolled, unsound!)
gstreamer_app::app_src::trampoline_need_data(): match Err(err), set indep. field, call function with ref, implicit drop (uncontrolled, unsound!)
gstreamer_app::app_src::trampoline_seek_data(): match Err(err), set indep. field, call function with ref, implicit drop (uncontrolled, unsound!)

guillotiere v0.6.2 (GitHub - nical/guillotiere at bf7e9464390485a96399f77e9b1bff3d442b8258):
guillotiere::recording::Recording::replay()/guillotiere::recording::Recording::find_reduced_testcase(): is_ok, implicit drop (controlled)

gurobi v0.3.4 (https://github.com/ubnt-intrepid/rust-gurobi/tree/v0.3.4) [old]:
gurobi::model::callback_wrapper(): match Err(e), return c_int, implicit drop (uncontrolled, unsound!)

hdf5 v0.8.1 (https://github.com/aldanor/hdf5-rust/tree/v0.8.1):
hdf5::error::ErrorStack::expand::callback(): unwrap_or, return herr_t (controlled)

helix v0.7.5 (https://github.com/tildeio/helix/tree/v0.7.5):
helix::macros::init::handle_exception!/helix::errors::Error::from_any(): map_err(|e|), attempt to downcast, unwrap_or_else(|any|), call function with ref, return Error, implicit drop (uncontrolled, unsound!)

honggfuzz v0.5.54 (https://github.com/rust-fuzz/honggfuzz-rs/tree/v0.5.54):
honggfuzz::fuzz(): is_err, implicit drop (uncontrolled!)

httpbis v0.9.1 (https://github.com/stepancheg/rust-http2/tree/v0.9.1):
httpbis::client_died_error_holder::SomethingDiedErrorHolder::wrap_future()/httpbis::misc::any_to_string(): if let Err(e), attempt to downcast, else implicit drop (uncontrolled!)

hyper v0.14.20 (https://github.com/hyperium/hyper/tree/v0.14.20):
hyper::ffi::macros::ffi_fn!::$name(): match Err(_), return default value, implicit drop (uncontrolled, unsound!)

imgui v0.8.2 (https://github.com/imgui-rs/imgui-rs/tree/v0.8.2/imgui):
imgui::clipboard::get_clipboard_text(): unwrap_or_else(|_|), call function, abort (incidentally unsound!)
imgui::clipboard::set_clipboard_text(): unwrap_or_else(|_|), call function, abort (incidentally unsound!)

interprocess v1.1.1 (https://github.com/kotauskas/interprocess/tree/1.1.1):
interprocess::os::unix::signal::signal_receiver(): unwrap_or_else(|_|), abort
interprocess::os::windows::signal::signal_receiver(): unwrap_or_else(|_|), abort

ladspa v0.3.4 (https://github.com/nwoeanhinnogaehr/ladspa.rs/tree/e8c899912225ec6b8c93146aa886c09c1846d766) [old]:
ladspa::fii::call_user_code!: match Err(_), call indep. function, return Option, implicit drop (uncontrolled, unsound!)

lambda_runtime v0.5.1 (https://github.com/awslabs/aws-lambda-rust-runtime/tree/v0.5.1-runtime/lambda-runtime):
lambda_runtime::Runtime::run(): match Err(err), call functions with ref, return Result, implicit drop (uncontrolled!)

libfuzzer-sys v0.4.3 (https://github.com/rust-fuzz/libfuzzer/tree/0.4.3):
libfuzzer_sys::test_input_wrap(): err, if is_some, abort (incidentally unsound!)

liblightning v0.0.2 (https://github.com/losfair/liblightning/tree/41056bb1cfd2b870ac8b62b9c64b1890a7be8650):
liblightning::co::CoState::co_initializer()/<liblightning::co::CoState as CommonCoState>::resume(): if let Err(e), move into field, cross boundary, resume_unwind
liblightning::scheduler::SharedSchedState::prepare_coroutine(): move into field, cross boundary, return Result

libpulse-binding v2.26.0 (https://github.com/jnqnfe/pulse-binding-rust/tree/v2.26.0/pulse-binding):
libpulse_binding::context::event_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::ext_device_manager::read_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::ext_device_restore::ext_subscribe_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::ext_device_restore::read_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::ext_stream_restore::read_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::ext_subscribe_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::ext_test_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::context_index_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_card_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_client_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_sample_info_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_server_info_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_sink_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_sink_input_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_source_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_source_output_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::get_stat_info_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::mod_info_list_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::introspect::send_message_to_object_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::notify_cb_proxy_multi(): let _ (uncontrolled, unsound!)
libpulse_binding::context::notify_cb_proxy_single(): let _ (uncontrolled, unsound!)
libpulse_binding::context::scache::play_sample_success_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::subscribe::cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::context::success_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::mainloop::api::once_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::mainloop::events::deferred::event_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::mainloop::events::io::event_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::mainloop::events::timer::event_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::mainloop::signal::signal_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::operation::notify_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::stream::event_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::stream::notify_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::stream::request_cb_proxy(): let _ (uncontrolled, unsound!)
libpulse_binding::stream::success_cb_proxy(): let _ (uncontrolled, unsound!)

libtest v0.0.1 (https://github.com/rust-lang/libtest/tree/72798cdede83eead387f32875d3ea77c85e29276/libtest):
libtest::bench::benchmark(): match Err(_), return TestResult, implicit drop (controlled)
libtest::run_test::run_test_inner()/libtest::calc_result(): call indep. functions, match, maybe call functions with ref, return TestResult, implicit drop (controlled)

lightproc v0.3.5 (https://github.com/bastion-rs/bastion/tree/lightproc-v0.3.5/src/lightproc):
lightproc::lightproc::LightProc::recoverable()/<lightproc::recoverable_handle::RecoverableHandle as Future>::poll(): match Err(_), call indep. functions, return Option, implicit drop (uncontrolled!)

lignin v0.1.0 (https://github.com/Tamschi/lignin/tree/v0.1.0):
lignin::callback_registry::callbacks_on::invoke(): match Err(panic), call indep. function, resume_unwind

mendes v0.0.64 (https://github.com/djc/mendes/tree/8766d9f6f18df902281cb9ad6235cd8fe9399ba5/mendes):
<mendes::hyper::ConnectionService as Service<Request<Body>>>::call()/mendes::hyper::panic_response(), match Err(e), maybe call functions with ref, return Result`, implicit drop (uncontrolled!)

mini-v8 v0.3.0 (https://github.com/SkylerLipthay/mini-v8/tree/39a44d1d78c8293da7be83751da907f18a90eeb5) [tough to build]:
mini_v8::ffi::callback_wrapper(): match Err(err), call function with ref, abort
(contains another unsound function)

miniz_oxide_c_api v0.3.0 (https://github.com/Frommi/miniz_oxide/tree/cff503a88812b4c467d9859ca7603cc623815d6b):
miniz_oxide_c_api::mz_deflateInit2(): match Err(_), call indep. function, return c_int, implicit drop (controlled)
miniz_oxide_c_api::mz_inflateInit2(): match Err(_), call indep. function, return c_int, implicit drop (controlled)
miniz_oxide_c_api::oxidize!::$mz_func(): match Err(_), call indep. function, return c_int, implicit drop (controlled)
miniz_oxide_c_api::tdef::tdefl_init(): match Err(_), call indep. function, return tdefl_status, implicit drop (controlled)

mlua v0.8.0 (https://github.com/khvzak/mlua/tree/v0.8.0):
mlua::lua::callback_error_ext()/mlua::util::pop_error(): match Err(p), move into field, cross boundary, resume_unwind
mlua::util::callback_error()/mlua::util::pop_error(): match Err(p), move into field, cross boundary, resume_unwind

mocktopus_macros v0.7.11 (https://github.com/CodeSandwich/Mocktopus/tree/aa93669b731074e47bf47628af1f299752f69901/macros):
mocktopus_macros::header_builder::FnHeaderBuilder::build(): match Err({unwind}), call indep. functions, resume_unwind

mozjs v0.10.1 (https://github.com/servo/rust-mozjs/tree/v0.10.1):
mozjs::panic::wrap_panic()/mozjs::panic::maybe_resume_unwind(): match Err(error), call indep. functions, move into global, implicit drop of prior value (uncontrolled!)

neon v0.10.1 (https://github.com/neon-bindings/neon/tree/0.10.1):
neon::context::internal::try_catch_glue()/neon::context::internal::ContextInternal::try_catch_internal(): match Err(err), move into pointer, cross boundary, resume_unwind
neon::types::error::convert_panics(): match Err(panic), call functions with ref, call indep. functions, return Result, implicit drop (uncontrolled, unsound!)

neon-runtime v0.10.1 (https://github.com/neon-bindings/neon/tree/0.10.1/crates/neon-runtime):
neon_runtime::napi::async_work::call_execute()/neon_runtime::napi::async_work::call_complete(), move into field, cross boundary, resume_unwind
neon_runtime::napi::no_panic::FailureBoundary::catch_failure()/neon_runtime::napi::error::fatal_error(): if let Err(panic), call function with ref, napi::fatal_error, unreachable!()

ntest v0.8.0 (https://github.com/becheran/ntest/tree/v0.8.0/ntest):
ntest::assert_panics(): if not is_err, panic!(), else implicit drop (controlled)

openssl v0.10.41 (https://github.com/sfackler/rust-openssl/tree/openssl-v0.10.41/openssl):
openssl::util::bio::bread()/openssl::ssl::SslStream::check_panic(): match Err(err), move into field, cross boundary, resume_unwind
openssl::util::bio::bwrite()/openssl::ssl::SslStream::check_panic(): match Err(err), move into field, cross boundary, resume_unwind
openssl::util::bio::ctrl()/openssl::ssl::SslStream::check_panic(): match Err(err), move into field, cross boundary, resume_unwind
openssl::util::invoke_passwd_cb()/<openssl::util::CallbackState as Drop>::drop(): match Err(err), move into field, cross boundary, resume_unwind

openvpn-plugin v0.4.1 (https://github.com/mullvad/openvpn-plugin-rs/tree/v0.4.1):
openvpn_plugin::openvpn_plugin_close(): if let Err(e), call function with ref, implicit drop (uncontrolled, unsound!)
openvpn_plugin::openvpn_plugin_func(): match Err(e), call function with ref, return c_int, implicit drop (uncontrolled, unsound!)
openvpn_plugin::openvpn_plugin_open(): match Err(e), call function with ref, return c_int, implicit drop (uncontrolled, unsound!)

paste v1.0.7 (https://github.com/dtolnay/paste/tree/1.0.7):
paste::pasted_to_tokens(): match Err(_), return Result, implicit drop (controlled)

paste-impl v0.1.18 (https://github.com/dtolnay/paste/tree/0.1.18/impl):
paste_impl::paste_segments(): match Err(_), return Result, implicit drop (controlled)

proc-macro-error v1.0.4 (https://gitlab.com/CreepySkeleton/proc-macro-error/-/tree/v1.0.4):
proc_macro_error::entry_point(): match Err(boxed), attempt to downcast, resume_unwind

proc-macro2 v1.0.40 (https://github.com/dtolnay/proc-macro2/tree/1.0.40):
proc_macro2::detection::initialize(): is_ok, implicit drop (controlled)
proc_macro2::wrapper::proc_macro_parse(): unwrap_or_else(|_|), return Result, implicit drop (controlled)

proc-macro2-diagnostics v0.9.1 (https://github.com/SergioBenitez/proc-macro2-diagnostics/tree/8a739598692e195858249fb3ad89370dca893371):
proc_macro2_diagnostics::nightly_works(): is_ok, implicit drop (controlled)

procspawn v0.10.1 (https://github.com/mitsuhiko/procspawn/tree/0.10.1):
procspawn::core::run_func(): match Err(panic), call function with ref, return Result, implicit drop (uncontrolled, sound)

proptest v1.0.0 (https://github.com/AltSysrq/proptest/tree/1.0.0/proptest):
proptest::test_runner::runner::call_test(): match Err(what), attempt to downcast, unwrap_or_else(|_|), return String, implicit drop (uncontrolled!)

pyo3 v0.16.5 (https://github.com/PyO3/pyo3/tree/v0.16.5):
pyo3::callback::handle_panic()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (uncontrolled, unsound?)
pyo3::impl_::pyclass::generate_pyclass_getattro_slot!::__wrap()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (controlled?)
pyo3::impl_::pymodule::ModuleDef::module_init()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (controlled?)
pyo3::types::function::drop_closure(): if let Err(err), call function with ref, implicit drop (uncontrolled, unsound!); let _ (controlled)

pyo3-macros-backend v0.16.5 (https://github.com/PyO3/pyo3/tree/v0.16.5/pyo3-macros-backend):
pyo3_macros_backend::method::FnSpec::get_wrapper_function::#ident()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (controlled?)
pyo3_macros_backend::pymethod::impl_py_getter_def::__wrap()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (controlled?)
pyo3_macros_backend::pymethod::impl_py_setter_def::__wrap()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (uncontrolled, unsound!)
pyo3_macros_backend::pymethod::impl_traverse_slot::__wrap_()/pyo3::callback::abort_on_traverse_panic(): match Err(_payload), call indep. function, abort
pyo3_macros_backend::pymethod::SlotDef::generate_type_slot::__wrap()/pyo3::callback::panic_result_into_callback_output()/pyo3::panic::PanicException::from_panic_payload(): match Err(payload), call functions by ref, implicit drop (controlled?)

quick-js v0.4.1 (https://github.com/theduke/quickjs-rs/tree/quick-js-v0.4.1):
quick_js::bindings::ContextWrapper::exec_callback(): match Err(_e), return indep. Result, implicit drop (uncontrolled, unsound!)

quickcheck v1.0.3 (https://github.com/BurntSushi/quickcheck/tree/1.0.3):
quickcheck::tester::safe(): map_err(|any_err|), call functions with ref, return String, implicit drop (uncontrolled!)
quickcheck::tester::TestResult::must_fail(): is_err, return TestResult, implicit drop (uncontrolled!)

rayon-core v1.9.3 (https://github.com/rayon-rs/rayon/tree/rayon-core-v1.9.3/rayon-core):
<rayon_core::job::StackJob as Job>::execute()/rayon_core::job::JobResult::into_return_value(): match Err(x), move into field, cross boundary, resume_unwind
rayon_core::join::join_context()/rayon_core::join::join_recover_from_panic(): match Err(err), call indep. function, resume_unwind
rayon_core::scope::ScopeBase::execute_job_closure()/rayon_core::scope::ScopeBase::job_panicked()/rayon_core::scope::ScopeBase::maybe_propagate_panic(): match Err(err), move into field, cross boundary, resume_unwind (incidentally unsound?)
rayon_core::spawn::spawn_job()/rayon_core::registry::Registry::handle_panic(): match Err(err), move into function (abort on panic) OR abort

riker v0.4.2 (https://github.com/riker-rs/riker/tree/2fbd243ce477839c15d74e22fceed10ee4a97f3b):
riker::kernel::kernel(): let _ (uncontrolled!)
riker::kernel::start_actor(): map_err(|_|), return CreateError, implicit drop (uncontrolled!)

rlua v0.19.2 (https://github.com/amethyst/rlua/tree/v0.19.2):
rlua::util::callback_error()/rlua::util::pop_error(): match Err(p), move into field, cross boundary, resume_unwind

rouille v3.5.0 (https://github.com/tomaka/rouille/tree/v3.5.0):
rouille::log::log(): match Err(payload), call indep. function, resume_unwind
rouille::log::log_custom(): match Err(payload), call indep. function, resume_unwind rouille::Server::process(): match Err(_), return Response`, implicit drop (uncontrolled!)

rusb v0.9.1 (https://github.com/a1ien/rusb/tree/v0.9.1-rusb):
rusb::hotplug::hotplug_callback(): match Err(_), return c_int, implicit drop (uncontrolled, unsound!)

ruscii v0.3.2 (https://github.com/lemunozm/ruscii/tree/v0.3.2):
ruscii::app::App::run(): if let Err(_), call indep. functions, implicit drop (uncontrolled!)

rusqlite v0.27.0 (https://github.com/rusqlite/rusqlite/tree/v0.27.0):
rusqlite::Connection::busy_handler::busy_handler_callback(): if let Ok(true), else return c_int (uncontrolled, unsound!)
rusqlite::Connection::trace(): drop (uncontrolled, unsound!)
rusqlite::functions::call_boxed_final(): match Err(_), call function, return, implicit drop (uncontrolled, unsound!)
rusqlite::functions::call_boxed_inverse(): match Err(_), call function, return, implicit drop (uncontrolled, unsound!)
rusqlite::functions::call_boxed_step(): match Err(_), call function, return, implicit drop (uncontrolled, unsound!)
rusqlite::functions::call_boxed_value(): match Err(_), call function, return, implicit drop (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::authorizer::call_boxed_closure(): map_or_else(|_|), return c_int, implicit drop (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::collation_needed::collation_needed_callback(): is_err, return, implicit drop (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::commit_hook::call_boxed_closure(): drop (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::create_collation::call_boxed_closure(): match Err(_), return c_int, implicit drop (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::create_scalar_function::call_boxed_closure(): match Err(_), call function, return, implicit drop (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::progress_handler::call_boxed_closure(): if let Ok(true), else return c_int (uncontrolled, unsound!)
rusqlite::inner_connection::InnerConnection::rollback_hook::call_boxed_closure(): drop (uncontrolled, unsound!)
rusqlite::session::call_conflict(): if let Ok(action), else return c_int (uncontrolled, unsound!)
rusqlite::session::call_filter(): if let Ok(true), else return c_int (uncontrolled, unsound!)
rusqlite::session::Session::table_filter::call_boxed_closure(): if let Ok(true), else return c_int (uncontrolled, unsound!)
rusqlite::trace::config_log(): drop (uncontrolled, unsound!)
rusqlite::unlock_notify::unlock_notify_cb(): drop (impossible)

rustc-rayon v0.4.0 (https://github.com/rust-lang/rustc-rayon/tree/v0.4.0):
rustc_rayon::job::JobImpl::run_result()/rustc_rayon::job::JobImpl::into_result(): match Err(x), move into field, cross boundary, resume_unwind

rustc-rayon-core v0.4.1 (https://github.com/rust-lang/rustc-rayon/tree/e70e468c72fc6832f53ed3fbe88bd24d77f7ccb9/rayon-core):
<rustc_rayon_core::job::StackJob as Job>::execute()/rustc_rayon_core::job::JobResult::into_return_value(): match Err(x), move into field, cross boundary, resume_unwind
rustc_rayon_core::join::join_context()/rustc_rayon_core::join::join_recover_from_panic(): match Err(err), call indep. function, resume_unwind
rustc_rayon_core::scope::ScopeBase::execute_job_closure()/rustc_rayon_core::scope::ScopeBase::job_panicked()/rustc_rayon_core::scope::ScopeBase::maybe_propagate_panic(): match Err(err), move into field, cross boundary, resume_unwind (incidentally unsound?)
rustc_rayon_core::spawn::spawn_job()/rustc_rayon_core::registry::Registry::handle_panic(): match Err(err), move into function (abort on panic) OR abort
rustc_rayon_core::ThreadPoolBuilder::build_scoped(): call indep. function, match Err(err), resume_unwind
rustc_rayon_core::registry::main_loop()/rustc_rayon_core::registry::Registry::handle_panic(): create AbortIfPanic guard, match Err(err), move into trait function, forget guard

rustler v0.25.0 (https://github.com/rusterlium/rustler/tree/rustler-0.25.0/rustler):
rustler::thread::spawn(): match Err(err), call functions with ref, implicit drop (uncontrolled, unsound?)

rustler_codegen v0.25.0 (https://github.com/rusterlium/rustler/tree/rustler-0.25.0/rustler_codegen):
rustler_codegen::nif::transcoder_decorator::<#name as Nif>::RAW_FUNC::nif_func::wrapper()/rustler::codegen_runtime::handle_nif_result(): match Err(err), downcast, match Err(_), return NifReturned, implicit drop (uncontrolled, unsound!)

rustls-ffi v0.9.1 (https://github.com/rustls/rustls-ffi/tree/v0.9.1):
rustls_ffi::panic::ffi_panic_boundary!: match Err(_), return error, implicit drop (uncontrolled, unsound?)

rusty-fork v0.3.0 (https://github.com/AltSysrq/rusty-fork/tree/0.3.0):
rusty_fork::fork::fork_impl(): match Err(_), exit

rutie-serde v0.3.0 (https://github.com/deliveroo/rutie-serde/tree/0.3.0):
rutie_serde::panics::catch_and_raise(): match Err(_), call functions, call nonreturning function, unreachable!()

safe-proc-macro2 v1.0.36 (https://gitlab.com/leonhard-llc/safe-regex-rs/-/tree/safe-proc-macro2-v1.0.36/safe-proc-macro2):
safe_proc_macro2::detection::initialize(): is_ok, implicit drop (controlled)
safe_proc_macro2::wrapper::proc_macro_parse(): unwrap_or_else(|_|), return Result, implicit drop (controlled)

sc-executor v0.9.0 (https://github.com/paritytech/substrate/tree/v3.0.0/client/executor):
sc_executor::native_executor::with_externalities_safe(): map_err(|e|), call functions with ref, return Error, implicit drop (uncontrolled!)

sc-executor-wasmtime v0.9.0 (https://github.com/paritytech/substrate/tree/v3.0.0/client/executor/wasmtime):
sc_executor_wasmtime::imports::call_static()/sc_executor_wasmtime::imports::stringify_panic_payload(): match Err(err), attempt to downcast, implicit drop (uncontrolled!)

sc-service v0.9.0 (https://github.com/paritytech/substrate/tree/v3.0.0/client/service):
sc_service::task_manager::SpawnTaskHandle::spawn_inner(): match Err(payload), call indep. function, resume_unwind

scheduled-thread-pool v0.2.6 (https://github.com/sfackler/scheduled-thread-pool/tree/v0.2.6):
scheduled_thread_pool::Worker::run(): let _ (uncontrolled, sound)

security-framework v2.6.1 (https://github.com/kornelski/rust-security-framework/tree/v2.6.1/security-framework):
security_framework::secure_transport::read_func()/security_framework::secure_transport::SslStream::check_panic(): match Err(e), move into field, cross boundary, resume_unwind security_framework::secure_transport::write_func()/security_framework::secure_transport::SslStream::check_panic(): match Err(e), move into field, cross boundary, resume_unwind

sentry-core v0.27.0 (https://github.com/getsentry/sentry-rust/tree/0.27.0/sentry-core):
sentry_core::hub::Hub::run(): call indep. functions, match Err(err), resume_unwind

serial_test v0.8.0 (https://github.com/palfrey/serial_test/tree/v0.8.0/serial_test):
serial_test::parallel_code_lock::local_async_parallel_core(): call indep. function, if let Err(err), resume_unwind
serial_test::parallel_code_lock::local_async_parallel_core_with_return(): call indep. function, match Err(err), resume_unwind
serial_test::parallel_code_lock::local_parallel_core(): call indep. function, if let Err(err), resume_unwind
serial_test::parallel_code_lock::local_parallel_core_with_return(): call indep. function, match Err(err), resume_unwind
serial_test::parallel_file_lock::fs_async_parallel_core(): call indep. function, if let Err(err), resume_unwind
serial_test::parallel_file_lock::fs_async_parallel_core_with_return(): call indep. function, match Err(err), resume_unwind
serial_test::parallel_file_lock::fs_parallel_core(): call indep. function, if let Err(err), resume_unwind
serial_test::parallel_file_lock::fs_parallel_core_with_return(): call indep. function, match Err(err), resume_unwind

sled v0.34.7 (https://github.com/spacejam/sled/tree/v0.34.7):
sled::threadpool::spawn_new_thread(): set indep. global, if is_err, call function with ref, set indep. global, return, implicit drop (uncontrolled, sound)

smbc v0.1.0 (https://github.com/smbc-rs/smbc/tree/f12958560d6179b10eb0f91971ca0c13c8b01aa8):
smbc::smbc::SmbClient::auth_wrapper(): unwrap_or, return default values (uncontrolled, unsound!)

smol v1.2.5 (https://github.com/smol-rs/smol/tree/v1.2.5):
smol::spawn::spawn(): ok (uncontrolled!)

sp-state-machine (https://github.com/paritytech/substrate/tree/d14784fee8eddead26efc8617c512cc8775bfde5/primitives/state-machine):
sp_state_machine::testing::TestExternalities::execute_with_safe(): map_err(|e|), call function with ref, return String, implicit drop (controlled)

sp-tasks v3.0.0 (https://github.com/paritytech/substrate/tree/v3.0.0/primitives/tasks):
sp_tasks::inner::spawn(): match Err(panic), call function with ref, return, implicit drop (uncontrolled!)

ssi v0.4.0 (https://github.com/spruceid/ssi/tree/v0.4.0):
ssi::jws::verify_bytes_warnable(): map_err(|e|), return indep. Error, implicit drop (controlled)

stacker v0.1.14 (https://github.com/rust-lang/stacker/tree/stacker-0.1.14):
stacker::_grow(): err, cross boundary, call indep. function, if let Some(p), resume_unwind
stacker::fiber_proc()/stacker::_grow(): err, move into field, cross boundary, call indep. functions, if let Some(p), resume_unwind

static_init v1.0.2 (https://gitlab.com/okannen/static_init/-/tree/6f2b42eed0015f394998482f5b28eb18395f39d2):
static_init::generic_lazy::may_debug(): match Err(x), if is::<CyclicPanic>, panic!(), else resume_unwind

steamworks v0.9.0 (https://github.com/Noxime/steamworks-rs/tree/v0.9.0):
steamworks::networking_types::free_rust_message_buffer(): if let Err(e), call function with ref, implicit drop (impossible, incidentally unsound!)
steamworks::utils::c_warning_callback(): if let Err(err), call functions with ref, abort

take_mut v0.2.2 (https://github.com/Sgeo/take_mut/tree/v0.2.2):
take_mut::scoped::scope(): call indep. function, abort OR (match Err(p), resume_unwind)
take_mut::take(): unwrap_or_else(|_|), abort
take_mut::take_or_recover(): match Err(err), call indep. functions, resume_unwind; unwrap_or_else(|_|), abort

temp-env v0.2.0 (https://github.com/vmx/temp-env/tree/v0.2.0):
temp_env::with_vars(): match Err(err), call indep. functions, resume_unwind

test-context-macros v0.1.1 (https://github.com/markhildreth/test-context/tree/v0.1.1/macros):
test_context_macros::test_context(): call indep. function, match Err(err), resume_unwind

tester v0.9.0 (https://github.com/messense/rustc-test/tree/v0.9.0):
tester::bench::benchmark(): call indep. function, match Err(_), return TestResult, implicit drop (uncontrolled, sound)
tester::run_test_in_process(): call indep. functions, match Err(e), call function with ref, implicit drop (uncontrolled, sound)

thread-pool v0.1.1 (https://github.com/carllerche/thread-pool/tree/4f3599617fc14a7d83b4f936ceca99d7af98a6eb):
thread_pool::thread_pool::Worker::run(): let _ (uncontrolled!)

tokio v1.19.2 (https://github.com/tokio-rs/tokio/tree/tokio-1.19.2/tokio):
tokio::runtime::task::harness::cancel_task(): match Err(panic), move into field
tokio::runtime::task::harness::Harness::complete(): let _ (uncontrolled!)
tokio::runtime::task::harness::Harness::drop_join_handle_slow(): let _ (uncontrolled!)
tokio::runtime::task::harness::poll_future(): match Err(panic), move into Result, move into field; let _ (controlled?)
tokio::signal::reusable_box::ReusableBoxFuture::set_same_layout(): call indep. functions, match Err(payload), resume_unwind
tokio::sync::task::atomic_waker::AtomicWaker::do_register(): match Err(panic), move into Option, call functions with is_some, if let Some(panic), resume_unwind; let _ (uncontrolled!); let _ (uncontrolled!)
tokio::sync::watch::Sender::send_if_modified(): match Err(panicked), call indep. function, resume_unwind

tokio-threadpool v0.1.17 (https://github.com/tokio-rs/tokio/tree/tokio-threadpool-0.1.17/tokio-threadpool):
tokio_threadpool::task::Task::run(): match Err(_), call functions, return Run, implicit drop (uncontrolled!)

tokio-unix-ipc v0.2.0 (https://github.com/mitsuhiko/tokio-unix-ipc/tree/0.2.0):
tokio_unix_ipc::panic::catch_panic(): match Err(panic), call function with ref, return Result, implicit drop (uncontrolled!)

tower-http v0.3.4 (https://github.com/tower-rs/tower-http/tree/tower-http-0.3.4/tower-http):
<tower_http::catch_panic::CatchPanic as Service<Request<_>>>::call()/<tower_http::catch_panic::ResponseFuture as Future>::poll(): return Result

tracing-gstreamer v0.3.2 (https://github.com/standard-ai/tracing-gstreamer/tree/v0.3.2):
tracing_gstreamer::log::log_callback(): unwrap_or_else(|_e|), abort

trybuild v1.0.63 (https://github.com/dtolnay/trybuild/tree/1.0.63):
trybuild::diff::r#impl::Diff::compute(): ok, ? (controlled)

twinstar v0.4.0 (https://github.com/panicbit/twinstar/tree/v0.4.0):
twinstar::Server::serve_client(): unwrap_or_else(|_|), return Response, implicit drop (uncontrolled!)

two-rusty-forks v0.4.0 (https://github.com/inikulin/two-rusty-forks/tree/9346a869dab75ce6dad915b088231b4911ef1d99):
two_rusy_forks::fork::fork_impl(): match Err(_), exit

uppercut v0.4.0 (https://github.com/sergey-melnychuk/uppercut/tree/0.4):
uppercut::core::worker_loop(): if is_err, err, unwrap, move into user-supplied trait method, default implicit drop (uncontrolled!)

vapoursynth v0.3.0 (https://github.com/YaLTeR/vapoursynth-rs/tree/v0.3.0/vapoursynth):
vapoursynth::api::API::add_message_handler::c_callback(): if is_err, abort
vapoursynth::api::API::add_message_handler_trivial::c_callback(): if is_err, call indep. function, abort (incidentally unsound!)
vapoursynth::api::API::set_message_handler::c_callback(): if is_err, abort
vapoursynth::api::API::set_message_handler_trivial::c_callback(): if is_err, call indep. function, abort (incidentally unsound!)
vapoursynth::node::Node::get_frame_async::c_callback(): if is_err, abort
vapoursynth::plugins::ffi::create(): if is_err, abort
vapoursynth::plugins::ffi::export_vapoursynth_plugin!::VapourSynthPluginInit(): if is_err, abort
vapoursynth::plugins::ffi::free(): if is_err, abort
vapoursynth::plugins::ffi::get_frame(): match Err(_), abort
vapoursynth::plugins::ffi::init(): if is_err, call indep. functions, implicit drop (uncontrolled, unsound!); if is_err, abort

wasmer v2.3.0 (https://github.com/wasmerio/wasmer/tree/2.3.0/lib/api):
<wasmer_vm::VMDynamicFunctionContext as VMDynamicFunctionCall<_>>::func_wrapper()/wasmer_vm::trap::traphandlers::resume_panic()/wasmer_fm::trap::traphandlers::unwind_with()/wasmer_fm::trap::traphandlers::UnwindReason::to_trap(): match Err(panic), move into UnwindReason, cross boundary, resume_unwind
wasmer::js::externals::function::inner::impl_host_function!::HostFunction::function_body_pointer::func_wrapper(): match _, unimplemented!()
wasmer::sys::externals::function::inner::impl_host_function!::HostFunction::function_body_ptr()/wasmer_vm::trap::traphandlers::resume_panic()/wasmer_fm::trap::traphandlers::unwind_with()/wasmer_fm::trap::traphandlers::UnwindReason::to_trap(): match Err(panic), move into UnwindReason, cross boundary, resume_unwind
wasmer::sys::native::impl_native_traits!::NativeFunc::call(): map_err(|e|), call function with ref, return RuntimeError, implicit drop (uncontrolled, unsound?)

wasmer-runtime-core v0.17.1 (https://github.com/wasmerio/wasmer/tree/0.17.1/lib/runtime-core):
wasmer_runtime_core::typed_func::DynamicFunc::new::do_enter_host_polymorphic()/wasmer_runtime_core::tiering::run_tiering(): match Err(e), move into RuntimeError, cross boundary, if let Err(e), match _, return Result, implicit drop (uncontrolled!)
wasmer_runtime_core::typed_func::impl_traits!::HostFunction::to_raw::wrap()/wasmer_runtime_core::tiering::run_tiering(): match Err(err), move into RuntimeError, cross boundary, if let Err(e), match _, return Result, implicit drop (uncontrolled!)

wayland-client v0.29.4 (https://github.com/Smithay/wayland-rs/tree/39a24803925a6831959229e85faffdab39552a61/wayland-client):
wayland_client::native_lib::proxy::proxy_dispatcher(): match Err(_), call function, libc::abort

webview2 v0.1.4 (https://github.com/sopium/webview2/tree/v0.1.4):
webview2::callback!::<Impl as $name>::invoke(): match Err(_), call indep. function, abort (incidentally unsound!)

winit v0.26.1 (https://github.com/rust-windowing/winit/tree/v0.26.1):
winit::platform_impl::macos::event_loop::stop_app_on_panic()/winit::platform_impl::macos::event_loop::EventLoop::run_return(): match Err(e), move into Rc, call indep. functions, cross boundary, if let Some(panic), call indep. function, resume_unwind
winit::platform_impl::windows::event_loop::runner::EventLoopRunner::catch_unwind(): on condition call indep. function, return Option, implicit drop (uncontrolled, unsound!)

with_locals-proc_macros v0.3.0 (https://github.com/danielhenrymantilla/with_locals.rs/tree/v0.3.0/src/proc_macros):
with_locals_proc_macros::handle_let_bindings::handle_let_bindings(): if let Err(panic), (return Result, implicit drop) OR resume_unwind (controlled)
with_locals_proc_macros::wrap_statements_inside_closure_body::wrap_statements_inside_closure_body(): if let Err(panic), (return Result, implicit drop) OR resume_unwind (controlled)

wstp v0.2.2 (https://github.com/WolframResearch/wstp-rs/tree/v0.2.2):
wstp::wait::link_wait_callback_trampoline(): match Err(_), return i32, implicit drop (uncontrolled, unsound!)


TL;DR: catch_unwind() is a horrible interface for preventing unwinding. It works well when paired with resume_unwind() or when followed by abort(), but nearly every crate that handles it otherwise ends up leaking panics from dropping the payload. Something like drop_err_or_{forget, abort}() would be a safe and reasonable behavior (IMHO), but no currently widespread crate implements that logic.

Regarding UnwindResult, crates often move a value or access a mutable reference depending on whether a panic occurred. inspect_err() is insufficient to handle this, so into_result() would probably be more commonly used than I initially expected. Also, a very frequent pattern is to ignore the result and just to extract the payload for later. Therefore, it would likely need an err() method, with the same footgun warnings as into_result():

impl<T> UnwindResult<T> {
    // If there is a payload, return it; otherwise, return `None`. Be careful
    // when dropping it.
    pub fn err(self) -> Option<Box<dyn Any + Send>>;
}
9 Likes

Many of these I've held off on fixing because they become sound as soon as C-unwind stabilizes, and currently according to the reference, is currently well defined (even if in practice there are ways for a maliciously written impl to subvert it in the current implementation, in which case we'll probably cleanly unwind over SQLite frames): Functions - The Rust Reference

Functions with an ABI that differs from "Rust" do not support unwinding in the exact same way that Rust does. Therefore, unwinding past the end of functions with such ABIs causes the process to abort.

1 Like

A lot of the Reference's information on FFI and unwinding is inaccurate, including the snippet you posted. Behavior considered undefined best describes the current rule:

  • Calling a function with the wrong call ABI or unwinding from a function with the wrong unwind ABI.

Since the "C" ABI does not support unwinding (unlike "Rust"), it is considered UB to unwind out of an extern "C" function, even into another Rust function. In practice, the current behavior is to longjmp through any C frames on the way to the nearest catch_unwind(). After RFC 2945 (c_unwind), this de-facto behavior will be flipped: extern "C-unwind" will be defined to unwind through C frames, and extern "C" will be defined to immediately abort.[1]

Even after c_unwind is stabilized, the de-facto longjmping will still occur for users on older versions of the Rust compiler. From what I can tell, your MSRV policy is "latest stable", but there's no rust-version tag to enforce this. So unless you proactively bump the required version once c_unwind lands, this will remain an issue for months or years to come.[2] Of course, it will still become a non-issue after a sufficient length of time, which is why I've refrained so far from mass-filing issues on rusqlite and the other affected projects. There are also some open PRs that change catch_unwind itself to make panic-on-drop payloads abort, but they suffer from the same limitation of not fixing older versions.


  1. I think this future behavior is what your linked snippet might be referring to. ↩︎

  2. For example, suppose that c_unwind were to be stabilized right now. It would come with Rust 1.64.0 on 2022-09-16, but it would only reach Debian Stable around 2023-08, and Ubuntu LTS on 2024-04-18. ↩︎

1 Like

It's only UB if someone goes out of their way to panic_any with something that panics in it's drop implementation. While I welcome this becoming well-defined behavior, I don't loose any sleep over this, since I don't believe it happens in the wild and in a future version will automatically become well-defined, matching the documentation in the reference (which at one point I asked for clarification on, and was told that it basically is considered a rustc bug that it does not match reality at the moment).

That said, I would accept a PR to fix this, should someone feel motivated.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.