fix: scope panic suppression to individual jobs and threads

This commit is contained in:
Momoko-Ayase committed 2026-11-04 12:34:41 +08:00
1 parent db2729737f
commit 312a70b7c6
5 files changed
+273 -143

No files matched your search

+68
View File
@@ -0,0 +1,68 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Barrier};
#[test]
fn folder_runs_preserve_the_host_panic_hook() {
const CHILD: &str = "SENBEI_PANIC_HOOK_TEST_CHILD";
if std::env::var_os(CHILD).is_none() {
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"folder_runs_preserve_the_host_panic_hook",
"--nocapture",
])
.env(CHILD, "1")
.output()
.unwrap();
assert!(
output.status.success(),
"child failed: {}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
return;
}
static CALLS: AtomicUsize = AtomicUsize::new(0);
std::panic::set_hook(Box::new(|_| {
CALLS.fetch_add(1, Ordering::SeqCst);
}));
let root = tempfile::tempdir().unwrap();
// Exercise repeated job lifetimes both before and after engine capture.
for _ in 0..2 {
senbei_io::job::run_folder_opts(root.path(), None, 2, false, true, false).unwrap();
assert!(senbei_engine::catch_unwind_quiet(|| panic!("captured")).is_err());
assert!(std::panic::catch_unwind(|| panic!("host")).is_err());
}
assert_eq!(CALLS.load(Ordering::SeqCst), 2);
let barrier = Arc::new(Barrier::new(2));
let worker_barrier = barrier.clone();
let worker = std::thread::spawn(move || {
senbei_engine::catch_unwind_quiet(|| {
worker_barrier.wait();
worker_barrier.wait();
panic!("captured in another thread");
})
.unwrap_err();
});
barrier.wait();
// A quiet capture in another thread must not silence unrelated panics.
assert!(std::panic::catch_unwind(|| panic!("unrelated host panic")).is_err());
barrier.wait();
worker.join().unwrap();
assert_eq!(CALLS.load(Ordering::SeqCst), 3);
std::thread::scope(|scope| {
for _ in 0..4 {
let path = root.path();
scope.spawn(move || {
senbei_io::job::run_folder_opts(path, None, 2, false, true, false).unwrap();
assert!(
senbei_engine::catch_unwind_quiet(|| panic!("after concurrent job")).is_err()
);
});
}
});
assert_eq!(CALLS.load(Ordering::SeqCst), 3);
}
+3
View File
@@ -1,8 +1,11 @@
//! Platform-specific unpacking engines.
pub mod android;
mod panic_capture;
pub mod windows;
pub use panic_capture::catch_unwind_quiet;
pub use windows::{
Detected, IntegrityReport, Kind, UnpackError, check_integrity, detect, unpack_auto,
unpack_auto_v, unpack_dll, unpack_dll_v, unpack_exe, unpack_exe_v,
+182
View File
@@ -0,0 +1,182 @@
//! Thread-scoped panic capture shared by the engines and native orchestration.
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
#[derive(Clone)]
pub(crate) struct PanicCapture(Arc<Mutex<Option<PanicDetails>>>);
#[derive(Clone)]
pub(crate) struct PanicDetails {
pub message: String,
pub file: String,
pub line: u32,
pub column: u32,
}
thread_local! {
static ACTIVE_PANIC_CAPTURE: RefCell<Option<PanicCapture>> = const { RefCell::new(None) };
}
struct PanicCaptureGuard(Option<PanicCapture>);
impl Drop for PanicCaptureGuard {
fn drop(&mut self) {
ACTIVE_PANIC_CAPTURE.with(|slot| {
slot.replace(self.0.take());
});
}
}
impl PanicCapture {
pub(crate) fn new() -> Self {
Self(Arc::new(Mutex::new(None)))
}
fn record(&self, info: &std::panic::PanicHookInfo<'_>) {
let location = info.location();
let details = PanicDetails {
message: panic_message(info.payload()),
file: location
.map(|value| value.file().to_owned())
.unwrap_or_else(|| "<unknown>".to_owned()),
line: location.map_or(0, std::panic::Location::line),
column: location.map_or(0, std::panic::Location::column),
};
let mut captured = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if captured.is_none() {
*captured = Some(details);
}
}
pub(crate) fn details(&self, payload: &(dyn std::any::Any + Send)) -> PanicDetails {
self.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_else(|| PanicDetails {
message: panic_message(payload),
file: "<unknown>".to_owned(),
line: 0,
column: 0,
})
}
pub(crate) fn merge_from(&self, other: &Self) {
let details = other
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let Some(details) = details else { return };
let mut captured = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if captured.is_none() {
*captured = Some(details);
}
}
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_owned()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"non-string panic payload".to_owned()
}
}
pub(crate) fn install_hook() {
static INSTALL: std::sync::Once = std::sync::Once::new();
INSTALL.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let capture = ACTIVE_PANIC_CAPTURE
.try_with(|slot| slot.try_borrow().ok().and_then(|slot| slot.clone()))
.ok()
.flatten();
if let Some(capture) = capture {
capture.record(info);
// WebAssembly unwinding is unavailable: retain the browser's
// diagnostic hook before the unrecoverable trap.
#[cfg(target_arch = "wasm32")]
previous(info);
} else {
previous(info);
}
}));
});
}
pub(crate) fn current_panic_capture() -> Option<PanicCapture> {
ACTIVE_PANIC_CAPTURE.with(|slot| slot.borrow().clone())
}
pub(crate) fn with_panic_capture<R>(capture: Option<PanicCapture>, f: impl FnOnce() -> R) -> R {
let previous = ACTIVE_PANIC_CAPTURE.with(|slot| slot.replace(capture));
let _guard = PanicCaptureGuard(previous);
f()
}
/// Catch a native per-file panic without printing the process panic hook.
///
/// Suppression applies only to this thread and scope. The process hook is
/// installed once, chains to the host hook outside captures, and is never
/// replaced or restored by individual jobs. Nested engine captures and their
/// worker-thread diagnostics continue to work independently.
///
/// Allocation aborts and WebAssembly traps cannot be caught.
pub fn catch_unwind_quiet<R>(f: impl FnOnce() -> R) -> std::thread::Result<R> {
install_hook();
with_panic_capture(Some(PanicCapture::new()), || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nested_captures_restore_the_outer_scope() {
install_hook();
let outer = PanicCapture::new();
with_panic_capture(Some(outer.clone()), || {
assert!(catch_unwind_quiet(|| panic!("inner panic")).is_err());
let payload = std::panic::catch_unwind(|| panic!("outer panic")).unwrap_err();
assert_eq!(outer.details(payload.as_ref()).message, "outer panic");
});
assert!(current_panic_capture().is_none());
}
#[test]
fn captures_are_thread_local_and_unwind_safe() {
install_hook();
let workers: Vec<_> = (0..8)
.map(|index| {
std::thread::spawn(move || {
let capture = PanicCapture::new();
let payload = with_panic_capture(Some(capture.clone()), || {
std::panic::catch_unwind(|| panic!("worker {index}"))
})
.unwrap_err();
assert!(current_panic_capture().is_none());
let details = capture.details(payload.as_ref());
assert_eq!(details.message, format!("worker {index}"));
assert!(details.file.ends_with("panic_capture.rs"));
assert!(details.line > 0);
assert!(details.column > 0);
})
})
.collect();
for worker in workers {
worker.join().unwrap();
}
}
}
+12 -128
View File
@@ -8,8 +8,8 @@ mod layout;
pub(crate) mod parallel;
use senbei_crypto::primitives;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
pub(crate) use crate::panic_capture::{PanicCapture, current_panic_capture, with_panic_capture};
pub use crate::thread_cap;
pub use dll::{unpack_dll, unpack_dll_v};
@@ -26,130 +26,6 @@ pub use integrity::{
/// Real protected binaries are far below this.
pub(crate) const MAX_IMAGE_SIZE: u64 = senbei_crypto::MAX_IMAGE_SIZE;
#[derive(Clone)]
pub(crate) struct PanicCapture(Arc<Mutex<Option<PanicDetails>>>);
#[derive(Clone)]
struct PanicDetails {
message: String,
file: String,
line: u32,
column: u32,
}
thread_local! {
static ACTIVE_PANIC_CAPTURE: RefCell<Option<PanicCapture>> = const { RefCell::new(None) };
}
struct PanicCaptureGuard(Option<PanicCapture>);
impl Drop for PanicCaptureGuard {
fn drop(&mut self) {
ACTIVE_PANIC_CAPTURE.with(|slot| {
slot.replace(self.0.take());
});
}
}
impl PanicCapture {
fn new() -> Self {
Self(Arc::new(Mutex::new(None)))
}
fn record(&self, info: &std::panic::PanicHookInfo<'_>) {
let location = info.location();
let details = PanicDetails {
message: panic_message(info.payload()),
file: location
.map(|value| value.file().to_owned())
.unwrap_or_else(|| "<unknown>".to_owned()),
line: location.map_or(0, std::panic::Location::line),
column: location.map_or(0, std::panic::Location::column),
};
let mut captured = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if captured.is_none() {
*captured = Some(details);
}
}
fn into_error(self, payload: &(dyn std::any::Any + Send)) -> UnpackError {
let details = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_else(|| PanicDetails {
message: panic_message(payload),
file: "<unknown>".to_owned(),
line: 0,
column: 0,
});
UnpackError::InternalPanic {
message: details.message,
file: details.file,
line: details.line,
column: details.column,
}
}
fn merge_from(&self, other: &Self) {
let details = other
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
let Some(details) = details else { return };
let mut captured = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if captured.is_none() {
*captured = Some(details);
}
}
}
fn panic_message(payload: &(dyn std::any::Any + Send)) -> String {
if let Some(message) = payload.downcast_ref::<&str>() {
(*message).to_owned()
} else if let Some(message) = payload.downcast_ref::<String>() {
message.clone()
} else {
"non-string panic payload".to_owned()
}
}
fn install_panic_capture_hook() {
static INSTALL: std::sync::Once = std::sync::Once::new();
INSTALL.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let capture = ACTIVE_PANIC_CAPTURE
.try_with(|slot| slot.borrow().clone())
.ok()
.flatten();
if let Some(capture) = capture {
capture.record(info);
} else {
previous(info);
}
}));
});
}
pub(crate) fn current_panic_capture() -> Option<PanicCapture> {
ACTIVE_PANIC_CAPTURE.with(|slot| slot.borrow().clone())
}
pub(crate) fn with_panic_capture<R>(capture: Option<PanicCapture>, f: impl FnOnce() -> R) -> R {
let previous = ACTIVE_PANIC_CAPTURE.with(|slot| slot.replace(capture));
let _guard = PanicCaptureGuard(previous);
f()
}
/// Run an unpack pipeline, converting any internal panic into a clean
/// [`UnpackError::InternalPanic`] so the public API stays panic-free on any input
/// (truncated/garbled files chase offsets out of bounds). The panic location and
@@ -161,14 +37,22 @@ pub(crate) fn catch_unpack<F>(f: F) -> Result<Vec<u8>, UnpackError>
where
F: FnOnce() -> Result<Vec<u8>, UnpackError>,
{
install_panic_capture_hook();
crate::panic_capture::install_hook();
let capture = PanicCapture::new();
let r = with_panic_capture(Some(capture.clone()), || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
});
match r {
Ok(result) => result,
Err(payload) => Err(capture.into_error(payload.as_ref())),
Err(payload) => {
let details = capture.details(payload.as_ref());
Err(UnpackError::InternalPanic {
message: details.message,
file: details.file,
line: details.line,
column: details.column,
})
},
}
}
+8 -15
View File
@@ -165,10 +165,6 @@ pub fn run_folder_opts(
..Summary::default()
};
// Silence the default panic hook's stderr spew during per-file processing.
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {})); // suppress "thread panicked" messages
for input in candidates {
let rel = rel_in_tree(root, input);
let dest = out_root.join(out_name(&rel));
@@ -176,9 +172,9 @@ pub fn run_folder_opts(
// Wrap in catch_unwind so a single bad file never aborts the folder run.
let input_owned = input.clone();
let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let result = senbei_engine::catch_unwind_quiet(|| {
unpack_one_v(&input_owned, &dest_owned, verbose_steps)
}));
});
match result {
Ok(Ok((kind, report))) => {
@@ -246,14 +242,14 @@ pub fn run_folder_opts(
}
let input_owned = input.clone();
let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let result = senbei_engine::catch_unwind_quiet(|| {
crate::android::restore_so_file_with_method_index_module(
&input_owned,
&dest_owned,
verbose_steps,
&mut android_method_index_module,
)
}));
});
match result {
Ok(Ok(embedded)) => {
s.unpacked += 1;
@@ -325,7 +321,7 @@ pub fn run_folder_opts(
let rel_owned = rel.clone().into_owned();
let out_root_owned = out_root.clone();
let mut seen_taken = std::mem::take(&mut android_seen);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let result = senbei_engine::catch_unwind_quiet(|| {
let outcomes = crate::android::restore_package_with_method_index_module(
&package_owned,
&rel_owned,
@@ -335,7 +331,7 @@ pub fn run_folder_opts(
&mut android_method_index_module,
);
(outcomes, seen_taken)
}));
});
match result {
Ok((Ok(outcomes), seen_back)) => {
android_seen = seen_back;
@@ -377,14 +373,14 @@ pub fn run_folder_opts(
let dest = out_root.join(out_name(&rel));
let meta_owned = meta.clone();
let dest_owned = dest.clone();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let result = senbei_engine::catch_unwind_quiet(|| {
deobfuscate_metadata_to_with_module(
&meta_owned,
&dest_owned,
verbose_steps,
android_method_index_module.as_deref(),
)
}));
});
match result {
Ok(Ok(report)) if report.remapped > 0 => {
s.metadata += 1;
@@ -453,9 +449,6 @@ pub fn run_folder_opts(
}
}
// Restore the original panic hook.
std::panic::set_hook(default_hook);
bar.finish_and_clear();
s.duration_ms = t0.elapsed().as_millis();
if let Some(log) = &log {