mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-09-19 03:57:59 -04:00
feat: Optimize error reporting
This commit is contained in:
+7
-2
@@ -12,7 +12,7 @@ The crate is split into a pure core and a thin CLI shell:
|
||||
- **`src/unpacker/`** — the core. Pure functions over byte slices: no file
|
||||
I/O, no environment access (beyond a few debugging overrides, see
|
||||
[development.md](development.md)), panic-free at the public boundary (all
|
||||
internal panics are trapped and converted to `UnpackError::Corrupt`). This
|
||||
internal panics are trapped and converted to `UnpackError::InternalPanic`). This
|
||||
is what the WebAssembly build embeds.
|
||||
- **`src/` (top level)** — the CLI shell: argument parsing, recursive folder
|
||||
scanning, per-run log file, progress bar, Explorer-friendly exit pause, and
|
||||
@@ -120,7 +120,12 @@ threads (WebAssembly) the sequential path is used automatically.
|
||||
|
||||
The public API never panics: every pipeline runs under a `catch_unwind`
|
||||
wrapper (`catch_unpack`) that converts a trapped panic to
|
||||
`UnpackError::Corrupt`, with the default panic hook transiently suppressed.
|
||||
`UnpackError::InternalPanic`, including the Rust source location and panic
|
||||
payload. The capture context is propagated into section worker threads; panics
|
||||
outside an active unpack continue through the previously installed panic hook.
|
||||
Expected validation failures use structured variants carrying the failed stage,
|
||||
block index, table kind, or invalid range instead of collapsing unrelated causes
|
||||
into a generic corruption error.
|
||||
Size requests are bounds-checked against a 1 GiB `MAX_IMAGE_SIZE` before
|
||||
allocation so a crafted header cannot abort the process with a huge
|
||||
allocation. In folder mode each file is isolated: one file's failure is logged
|
||||
|
||||
@@ -61,8 +61,8 @@ impl OpsLut {
|
||||
pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> {
|
||||
// Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the
|
||||
// alignment fallback) must yield `None`, not an out-of-bounds panic — the
|
||||
// panic path would surface as a misleading `UnpackError::Corrupt` instead
|
||||
// of the precise `BytecodeGenFailed`, and any future caller without a
|
||||
// panic path would surface as a misleading `UnpackError::InternalPanic` instead
|
||||
// of the precise `BytecodeGenerationFailed`, and any future caller without a
|
||||
// `catch_unwind` wrapper would abort outright.
|
||||
let mut pos = offset as usize;
|
||||
let mut next = move || {
|
||||
|
||||
+61
-25
@@ -13,9 +13,12 @@
|
||||
//! CalculateChecksumWithSizeXor -> primitives::calculate_checksum
|
||||
//! CalculateCrc32 -> crc32::compute (via above)
|
||||
|
||||
use super::UnpackError;
|
||||
use super::bytecode::{Op, OpsLut, generate};
|
||||
use super::primitives::{self, *};
|
||||
use super::{
|
||||
BufferOperation, BytecodeStage, DecompressionStage, DescriptorTable, SectionPipeline,
|
||||
UnpackError,
|
||||
};
|
||||
|
||||
/// Read a signed 32-bit little-endian value.
|
||||
fn get_i32(d: &[u8], offset: i32) -> i32 {
|
||||
@@ -68,6 +71,7 @@ fn decrypt_data4(
|
||||
key: i32,
|
||||
decomp_params: &[i32; 4],
|
||||
transform: Option<&[Op]>,
|
||||
stage: DecompressionStage,
|
||||
) -> Result<(), UnpackError> {
|
||||
let addr = get_i32(d, offset);
|
||||
let size = get_i32(d, offset + 4);
|
||||
@@ -95,7 +99,7 @@ fn decrypt_data4(
|
||||
size as u32,
|
||||
decompressed_size as u32,
|
||||
) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(stage));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -218,7 +222,11 @@ fn decrypt_and_decompress_data(
|
||||
// Guard: need 16 bytes at section_data_offset in `d`
|
||||
let off = section_data_offset as usize;
|
||||
if off.saturating_add(16) > d.len() {
|
||||
return Err(UnpackError::OutOfBounds(off));
|
||||
return Err(UnpackError::DescriptorOutOfBounds {
|
||||
table: DescriptorTable::DllSectionBlocks,
|
||||
offset: off,
|
||||
image_len: d.len(),
|
||||
});
|
||||
}
|
||||
decrypt_data6_shift6(d, section_data_offset, 16);
|
||||
let dest_offset = get_i32(d, section_data_offset);
|
||||
@@ -246,10 +254,10 @@ fn decrypt_and_decompress_data(
|
||||
let lut = OpsLut::new(decrypt_func);
|
||||
let ko0 = decomp_params[0];
|
||||
let ko2 = decomp_params[2];
|
||||
let ks_snap =
|
||||
primitives::aes_schedule_snapshot(d, ko2 as u32).ok_or(UnpackError::Corrupt)?;
|
||||
let ks_snap = primitives::aes_schedule_snapshot(d, ko2 as u32)
|
||||
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko2 as u32 })?;
|
||||
let tab_snap = primitives::huffman_table_snapshot(d, ko0 as u32)
|
||||
.ok_or(UnpackError::DecompressFailed)?;
|
||||
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko0 as u32 })?;
|
||||
let spans: Vec<(usize, usize)> = blocks
|
||||
.iter()
|
||||
.map(|b| {
|
||||
@@ -277,7 +285,10 @@ fn decrypt_and_decompress_data(
|
||||
b.size as u32,
|
||||
b.expected_crc as u32,
|
||||
) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::SectionDecompressionFailed {
|
||||
pipeline: SectionPipeline::Dll,
|
||||
block: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -292,7 +303,11 @@ fn decrypt_and_decompress_data(
|
||||
// decrypts 16 too, so guard 16 (an 8-byte guard would let
|
||||
// decrypt_data6_shift6 index past the end of a truncated descriptor).
|
||||
if off.saturating_add(16) > d.len() {
|
||||
return Err(UnpackError::OutOfBounds(off));
|
||||
return Err(UnpackError::DescriptorOutOfBounds {
|
||||
table: DescriptorTable::DllZeroFill,
|
||||
offset: off,
|
||||
image_len: d.len(),
|
||||
});
|
||||
}
|
||||
decrypt_data6_shift6(d, section_data_offset, 16);
|
||||
let zero_offset = get_i32(d, section_data_offset);
|
||||
@@ -305,7 +320,12 @@ fn decrypt_and_decompress_data(
|
||||
for i in 0..zero_size {
|
||||
let idx = (zero_offset + i) as usize;
|
||||
if idx >= d.len() {
|
||||
return Err(UnpackError::OutOfBounds(idx));
|
||||
return Err(UnpackError::BufferRangeOutOfBounds {
|
||||
operation: BufferOperation::ZeroFill,
|
||||
offset: idx,
|
||||
size: 1,
|
||||
buffer_len: d.len(),
|
||||
});
|
||||
}
|
||||
d[idx] = 0;
|
||||
}
|
||||
@@ -328,8 +348,12 @@ pub fn unpack_dll_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
}
|
||||
|
||||
fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||
if input.len() < 4096 {
|
||||
return Err(UnpackError::InputTooShort(input.len()));
|
||||
const HEADER_LEN: usize = 4128;
|
||||
if input.len() < HEADER_LEN {
|
||||
return Err(UnpackError::InputTooShort {
|
||||
actual: input.len(),
|
||||
required: HEADER_LEN,
|
||||
});
|
||||
}
|
||||
|
||||
// `file_data` and `original_file_data` both borrow the same protected input.
|
||||
@@ -348,14 +372,17 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
}
|
||||
|
||||
if !super::is_supported_magic(keys[1] as u32) {
|
||||
return Err(UnpackError::DllUnpack(
|
||||
"Not a Crackproof protected file (KONN magic mismatch)".into(),
|
||||
));
|
||||
return Err(UnpackError::HeaderMagicMismatch {
|
||||
found: keys[1] as u32,
|
||||
});
|
||||
}
|
||||
|
||||
let pe_offset = get_i32(file_data, 60);
|
||||
if pe_offset < 0 || (pe_offset as usize).saturating_add(84) > file_data.len() {
|
||||
return Err(UnpackError::DllUnpack("implausible PE offset".into()));
|
||||
return Err(UnpackError::InvalidPeOffset {
|
||||
offset: i64::from(pe_offset),
|
||||
input_len: file_data.len(),
|
||||
});
|
||||
}
|
||||
// This pipeline is PE32+-only: its header fixups write the data
|
||||
// directories at PE32+ offsets (pe+144..180, pe+136 for the DD blob). On a
|
||||
@@ -363,14 +390,18 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
// structurally plausible but unloadable file. Reject early with a clear
|
||||
// error so `unpack_auto`'s EXE-pipeline fallback handles PE32 DLLs (that
|
||||
// path is PE32-aware — see run_pe32), instead of us mangling them here.
|
||||
if get_i32(file_data, pe_offset + 24) & 0xFFFF != 0x20B {
|
||||
return Err(UnpackError::DllUnpack(
|
||||
"not a PE32+ image (the DLL pipeline handles 64-bit only)".into(),
|
||||
));
|
||||
let optional_magic = get_u16(file_data, (pe_offset + 24) as u32);
|
||||
if optional_magic != 0x20B {
|
||||
return Err(UnpackError::UnsupportedDllPeMagic {
|
||||
found: optional_magic,
|
||||
});
|
||||
}
|
||||
let size_of_image = get_i32(file_data, pe_offset + 80);
|
||||
if size_of_image <= 0 || size_of_image as u64 > super::MAX_IMAGE_SIZE {
|
||||
return Err(UnpackError::DllUnpack("implausible SizeOfImage".into()));
|
||||
return Err(UnpackError::InvalidImageSize {
|
||||
size: i64::from(size_of_image),
|
||||
max: super::MAX_IMAGE_SIZE,
|
||||
});
|
||||
}
|
||||
let mut out = vec![0u8; size_of_image as usize];
|
||||
let base_offset = keys[6] - keys[3] + 0x2000;
|
||||
@@ -512,6 +543,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
table_val ^ checksum2 ^ (xor_accumulator as i32),
|
||||
&decomp_params,
|
||||
None,
|
||||
DecompressionStage::DllCodeBlock1,
|
||||
)?;
|
||||
|
||||
let addr3b = get_i32(&out, decrypted_addr1 + 3728);
|
||||
@@ -537,6 +569,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
crc_xored ^ (xor_accumulator as i32) ^ trailing_val,
|
||||
&decomp_params,
|
||||
None,
|
||||
DecompressionStage::DllCodeBlock2,
|
||||
)?;
|
||||
|
||||
let checksum3 = calculate_checksum(&out, (decrypted_addr1 + 3480) as u32) as i32;
|
||||
@@ -549,6 +582,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
(not_val ^ (xor_key as u32)) as i32,
|
||||
&decomp_params,
|
||||
None,
|
||||
DecompressionStage::DllCodeBlock3,
|
||||
)?;
|
||||
|
||||
let addr4 = get_i32(&out, addr4_offset);
|
||||
@@ -574,8 +608,9 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
lfsr_seed_val = lfsr_seed_val.wrapping_add(k);
|
||||
}
|
||||
|
||||
let decrypt_func = generate(&out, lfsr as u32)
|
||||
.ok_or_else(|| UnpackError::DllUnpack("Failed to build decryption expression".into()))?;
|
||||
let decrypt_func = generate(&out, lfsr as u32).ok_or(UnpackError::BytecodeGenerationFailed(
|
||||
BytecodeStage::DllPrimaryDecryptor,
|
||||
))?;
|
||||
|
||||
let addr5_offset = decrypted_addr1 + 3840;
|
||||
let addr5 = get_i32(&out, addr5_offset);
|
||||
@@ -585,6 +620,7 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
lfsr_seed_val ^ xor_key ^ checksum4,
|
||||
&decomp_params,
|
||||
Some(&decrypt_func),
|
||||
DecompressionStage::DllCodeBlock4,
|
||||
)?;
|
||||
if verbose {
|
||||
println!("[7/9] Decrypting code block 4 (addr5)...");
|
||||
@@ -603,9 +639,9 @@ fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError>
|
||||
let lfsr2 = metadata_offset + 88;
|
||||
decrypt_data6(&mut out, lfsr2 as u32);
|
||||
|
||||
let decrypt_func2 = generate(&out, lfsr2 as u32).ok_or_else(|| {
|
||||
UnpackError::DllUnpack("Failed to build second decryption expression".into())
|
||||
})?;
|
||||
let decrypt_func2 = generate(&out, lfsr2 as u32).ok_or(
|
||||
UnpackError::BytecodeGenerationFailed(BytecodeStage::DllSectionDecryptor),
|
||||
)?;
|
||||
|
||||
let section_image_base = 4095 - get_i32(original_file_data, 4224);
|
||||
let section_data_offset = get_i32(&out, addr5 + 11976);
|
||||
|
||||
+293
-53
@@ -2,13 +2,121 @@ use super::bytecode::{Op, OpsLut, generate};
|
||||
use super::primitives;
|
||||
use super::primitives::*;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum UnpackError {
|
||||
#[error("input too short for header (need at least 4096 bytes, got {0})")]
|
||||
InputTooShort(usize),
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DecompressionStage {
|
||||
ExeStage3,
|
||||
ExeStage3Secondary,
|
||||
ExeStage4,
|
||||
ExeStage5,
|
||||
Pe32FourthStage,
|
||||
Pe32FifthStage,
|
||||
Pe32SeventhStage,
|
||||
DllCodeBlock1,
|
||||
DllCodeBlock2,
|
||||
DllCodeBlock3,
|
||||
DllCodeBlock4,
|
||||
}
|
||||
|
||||
#[error("info[1] mismatch — corrupt data or wrong offset")]
|
||||
HeaderMismatch,
|
||||
impl std::fmt::Display for DecompressionStage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::ExeStage3 => "EXE stage3",
|
||||
Self::ExeStage3Secondary => "EXE secondary stage3",
|
||||
Self::ExeStage4 => "EXE stage4",
|
||||
Self::ExeStage5 => "EXE stage5",
|
||||
Self::Pe32FourthStage => "PE32 fourth stage",
|
||||
Self::Pe32FifthStage => "PE32 fifth stage",
|
||||
Self::Pe32SeventhStage => "PE32 seventh stage",
|
||||
Self::DllCodeBlock1 => "DLL code block 1",
|
||||
Self::DllCodeBlock2 => "DLL code block 2",
|
||||
Self::DllCodeBlock3 => "DLL code block 3",
|
||||
Self::DllCodeBlock4 => "DLL code block 4",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BytecodeStage {
|
||||
ExeStage4,
|
||||
ExeStage5,
|
||||
Pe32CustomDecryptor,
|
||||
Pe32FileDecryptor,
|
||||
DllPrimaryDecryptor,
|
||||
DllSectionDecryptor,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BytecodeStage {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::ExeStage4 => "EXE stage4",
|
||||
Self::ExeStage5 => "EXE stage5",
|
||||
Self::Pe32CustomDecryptor => "PE32 custom decryptor",
|
||||
Self::Pe32FileDecryptor => "PE32 file decryptor",
|
||||
Self::DllPrimaryDecryptor => "DLL primary decryptor",
|
||||
Self::DllSectionDecryptor => "DLL section decryptor",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SectionPipeline {
|
||||
ExePe32Plus,
|
||||
ExePe32,
|
||||
Dll,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SectionPipeline {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::ExePe32Plus => "PE32+ EXE",
|
||||
Self::ExePe32 => "PE32 EXE",
|
||||
Self::Dll => "DLL",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DescriptorTable {
|
||||
DllSectionBlocks,
|
||||
DllZeroFill,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DescriptorTable {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::DllSectionBlocks => "DLL section-block",
|
||||
Self::DllZeroFill => "DLL zero-fill",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BufferOperation {
|
||||
Read,
|
||||
CopySource,
|
||||
CopyDestination,
|
||||
ZeroFill,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BufferOperation {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Read => "read",
|
||||
Self::CopySource => "copy source",
|
||||
Self::CopyDestination => "copy destination",
|
||||
Self::ZeroFill => "zero-fill",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum UnpackError {
|
||||
#[error("input too short (need at least {required} bytes, got {actual})")]
|
||||
InputTooShort { actual: usize, required: usize },
|
||||
|
||||
#[error("decrypted header magic mismatch (got 0x{found:08X})")]
|
||||
HeaderMagicMismatch { found: u32 },
|
||||
|
||||
#[error("anchor field not found — corrupt data or wrong offset")]
|
||||
AnchorNotFound,
|
||||
@@ -22,23 +130,42 @@ pub enum UnpackError {
|
||||
#[error("table_start not found — corrupt data or wrong offset")]
|
||||
TableStartNotFound,
|
||||
|
||||
#[error("stage4 bytecode generation failed — corrupt data or wrong offset")]
|
||||
BytecodeGenFailed,
|
||||
#[error("{0} bytecode generation failed — corrupt data or wrong offset")]
|
||||
BytecodeGenerationFailed(BytecodeStage),
|
||||
|
||||
#[error("stage5 marker not found — this build's layout is not supported by this unpacker")]
|
||||
Stage5MarkerNotFound,
|
||||
|
||||
#[error("stage5 bytecode generation failed — corrupt data or wrong offset")]
|
||||
Stage5BytecodeGenFailed,
|
||||
|
||||
#[error("DLL unpack failed: {0}")]
|
||||
DllUnpack(String),
|
||||
|
||||
#[error("not a Crackproof-protected file")]
|
||||
NotCrackproof,
|
||||
|
||||
#[error("out-of-bounds access at offset {0}")]
|
||||
OutOfBounds(usize),
|
||||
#[error("invalid PE header offset {offset} for {input_len}-byte input")]
|
||||
InvalidPeOffset { offset: i64, input_len: usize },
|
||||
|
||||
#[error("DLL pipeline requires PE32+ optional-header magic, got 0x{found:04X}")]
|
||||
UnsupportedDllPeMagic { found: u16 },
|
||||
|
||||
#[error("invalid SizeOfImage {size}; expected 1..={max}")]
|
||||
InvalidImageSize { size: i64, max: u64 },
|
||||
|
||||
#[error(
|
||||
"{operation} range out of bounds (offset {offset}, size {size}, buffer length {buffer_len})"
|
||||
)]
|
||||
BufferRangeOutOfBounds {
|
||||
operation: BufferOperation,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
buffer_len: usize,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"{table} descriptor out of bounds (offset {offset}, size 16, image length {image_len})"
|
||||
)]
|
||||
DescriptorOutOfBounds {
|
||||
table: DescriptorTable,
|
||||
offset: usize,
|
||||
image_len: usize,
|
||||
},
|
||||
|
||||
#[error("PE32 tbl not found — corrupt data or wrong offset")]
|
||||
Pe32TblNotFound,
|
||||
@@ -49,20 +176,52 @@ pub enum UnpackError {
|
||||
#[error("PE32 customDecryptor not found in sevenStage")]
|
||||
Pe32CustomDecryptorNotFound,
|
||||
|
||||
#[error("PE32 stage bytecode generation failed")]
|
||||
Pe32BytecodeGenFailed,
|
||||
|
||||
#[error("PE32 eighthStageKey not found")]
|
||||
Pe32EighthKeyNotFound,
|
||||
|
||||
#[error("PE32 file LFSR not found in eighthStage")]
|
||||
Pe32FileLfsrNotFound,
|
||||
|
||||
#[error("decompression failed — corrupt data or wrong offset")]
|
||||
DecompressFailed,
|
||||
#[error("{0} decompression failed — corrupt data or wrong offset")]
|
||||
StageDecompressionFailed(DecompressionStage),
|
||||
|
||||
#[error("input is corrupt or not a supported Crackproof layout")]
|
||||
Corrupt,
|
||||
#[error("{pipeline} section block {block} decompression failed")]
|
||||
SectionDecompressionFailed {
|
||||
pipeline: SectionPipeline,
|
||||
block: usize,
|
||||
},
|
||||
|
||||
#[error("AES key schedule is outside the image at offset {offset}")]
|
||||
InvalidAesKeySchedule { offset: u32 },
|
||||
|
||||
#[error("Huffman table is outside the image at offset {offset}")]
|
||||
InvalidHuffmanTable { offset: u32 },
|
||||
|
||||
#[error(
|
||||
"PE32 second-stage range is invalid (offset {offset}, size {size}, image length {image_len})"
|
||||
)]
|
||||
Pe32SecondStageRangeInvalid {
|
||||
offset: u32,
|
||||
size: u32,
|
||||
image_len: usize,
|
||||
},
|
||||
|
||||
#[error("PE32 relocation-data descriptor not found")]
|
||||
Pe32RelocationDataNotFound,
|
||||
|
||||
#[error("file decryptor candidate failed structural validation")]
|
||||
FileDecryptorValidationFailed,
|
||||
|
||||
#[error("PE32 memory image could not be rebuilt as a file-layout PE")]
|
||||
Pe32OutputLayoutInvalid,
|
||||
|
||||
#[error("internal panic at {file}:{line}:{column}: {message}")]
|
||||
InternalPanic {
|
||||
message: String,
|
||||
file: String,
|
||||
line: u32,
|
||||
column: u32,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
|
||||
@@ -93,8 +252,12 @@ fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> {
|
||||
}
|
||||
|
||||
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
|
||||
if input.len() < 4096 {
|
||||
return Err(UnpackError::InputTooShort(input.len()));
|
||||
const HEADER_LEN: usize = 4128;
|
||||
if input.len() < HEADER_LEN {
|
||||
return Err(UnpackError::InputTooShort {
|
||||
actual: input.len(),
|
||||
required: HEADER_LEN,
|
||||
});
|
||||
}
|
||||
// The pipeline chases offsets read out of the decrypted image; on a
|
||||
// truncated/garbled-but-detected file those run out of bounds. Trap any
|
||||
@@ -376,13 +539,25 @@ impl<'a> Unpacker<'a> {
|
||||
println!(" info[7] = 0x{:08X}", u.info[7]);
|
||||
}
|
||||
if !super::is_supported_magic(u.info[1]) {
|
||||
return Err(UnpackError::HeaderMismatch);
|
||||
return Err(UnpackError::HeaderMagicMismatch { found: u.info[1] });
|
||||
}
|
||||
|
||||
let pe_off = get_u32(u.file_data, 60);
|
||||
if (pe_off as usize)
|
||||
.checked_add(84)
|
||||
.is_none_or(|end| end > u.file_data.len())
|
||||
{
|
||||
return Err(UnpackError::InvalidPeOffset {
|
||||
offset: i64::from(pe_off),
|
||||
input_len: u.file_data.len(),
|
||||
});
|
||||
}
|
||||
let size_of_image = get_u32(u.file_data, pe_off.wrapping_add(80));
|
||||
if size_of_image == 0 || size_of_image as u64 > super::MAX_IMAGE_SIZE {
|
||||
return Err(UnpackError::Corrupt);
|
||||
return Err(UnpackError::InvalidImageSize {
|
||||
size: i64::from(size_of_image),
|
||||
max: super::MAX_IMAGE_SIZE,
|
||||
});
|
||||
}
|
||||
u.decompressed = vec![0u8; size_of_image as usize];
|
||||
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192);
|
||||
@@ -671,7 +846,9 @@ impl<'a> Unpacker<'a> {
|
||||
println!(" stage3 = 0x{:08X}", stage3_field);
|
||||
}
|
||||
if !u.decrypt_and_decompress_data(at1, xor_acc ^ chk2 ^ accum, None) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::ExeStage3,
|
||||
));
|
||||
}
|
||||
|
||||
let at2 = stage1.wrapping_add(stage2_off.wrapping_add(104));
|
||||
@@ -689,7 +866,9 @@ impl<'a> Unpacker<'a> {
|
||||
.unwrap_or_else(|| stage3_field.wrapping_add(4692));
|
||||
let v4_val = get_u32(&u.decompressed, v4);
|
||||
if !u.decrypt_and_decompress_data(at2, xor_acc ^ chk3 ^ v4_val, None) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::ExeStage3Secondary,
|
||||
));
|
||||
}
|
||||
|
||||
let chk4 = u.calculate_checksum(stage1.wrapping_add(chk_src_start.wrapping_add(16)));
|
||||
@@ -708,7 +887,9 @@ impl<'a> Unpacker<'a> {
|
||||
println!(" stage4 = 0x{:08X}", stage4_field);
|
||||
}
|
||||
if !u.decrypt_and_decompress_data(at3, xor_acc ^ chk4 ^ v5_val, None) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::ExeStage4,
|
||||
));
|
||||
}
|
||||
|
||||
// Inside stage4, two locations vary by build:
|
||||
@@ -758,7 +939,9 @@ impl<'a> Unpacker<'a> {
|
||||
let ops1 = match generate(&u.decompressed, data_offset) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(UnpackError::BytecodeGenFailed);
|
||||
return Err(UnpackError::BytecodeGenerationFailed(
|
||||
BytecodeStage::ExeStage4,
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -771,7 +954,9 @@ impl<'a> Unpacker<'a> {
|
||||
println!(" stage5 = 0x{:08X}", stage5_field);
|
||||
}
|
||||
if !u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1)) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::ExeStage5,
|
||||
));
|
||||
}
|
||||
|
||||
// Inside stage5, the loader stores a table of (ptr, size) pairs at a
|
||||
@@ -921,7 +1106,9 @@ impl<'a> Unpacker<'a> {
|
||||
let ops2 = match generate(&u.decompressed, data_offset2) {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
return Err(UnpackError::Stage5BytecodeGenFailed);
|
||||
return Err(UnpackError::BytecodeGenerationFailed(
|
||||
BytecodeStage::ExeStage5,
|
||||
));
|
||||
}
|
||||
};
|
||||
// The new layout picked its file decryptor by distance (no marker, no
|
||||
@@ -930,7 +1117,7 @@ impl<'a> Unpacker<'a> {
|
||||
// garbling into the output without any error (see the validator).
|
||||
let rebase = (!get_u32(u.file_data, 4224)).wrapping_add(4096);
|
||||
if new_layout && !u.new_layout_file_ops_validate(walk4_slot, &ops2, rebase) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::FileDecryptorValidationFailed);
|
||||
}
|
||||
|
||||
let at6 = walk4_slot;
|
||||
@@ -980,9 +1167,9 @@ impl<'a> Unpacker<'a> {
|
||||
// Snapshot the shared tables before the fan-out: workers get
|
||||
// disjoint span slices, not the whole buffer.
|
||||
let ks_snap = primitives::aes_schedule_snapshot(&u.decompressed, ko[2])
|
||||
.ok_or(UnpackError::Corrupt)?;
|
||||
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko[2] })?;
|
||||
let tab_snap = primitives::huffman_table_snapshot(&u.decompressed, ko[0])
|
||||
.ok_or(UnpackError::DecompressFailed)?;
|
||||
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko[0] })?;
|
||||
let spans: Vec<(usize, usize)> = blocks
|
||||
.iter()
|
||||
.map(|b| {
|
||||
@@ -1009,7 +1196,10 @@ impl<'a> Unpacker<'a> {
|
||||
b.len,
|
||||
b.plain_len,
|
||||
) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::SectionDecompressionFailed {
|
||||
pipeline: SectionPipeline::ExePe32Plus,
|
||||
block: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -1475,7 +1665,7 @@ impl<'a> Unpacker<'a> {
|
||||
/// whose fileCS pointer sits just past `info[3]`), not by content. A
|
||||
/// coincidental LFSR-shaped block at a shorter distance would decode to a
|
||||
/// wrong `ops2` translate and silently garble every section block — raw
|
||||
/// blocks never hit `DecompressFailed`, so the failure would ship as a
|
||||
/// raw blocks never enter the decompressor, so the failure would ship as a
|
||||
/// plausible but wrong image. Replay the first *compressed* block's full
|
||||
/// transform (raw copy, AES, translate, decompress) on a snapshot and
|
||||
/// require decompression to succeed; restore the region afterwards.
|
||||
@@ -1837,7 +2027,11 @@ impl<'a> Unpacker<'a> {
|
||||
let ss_lo = ss as usize;
|
||||
let ss_hi = ss_lo.wrapping_add(ss_size as usize);
|
||||
if ss_hi < ss_lo || ss_hi > self.decompressed.len() {
|
||||
return Err(UnpackError::Corrupt);
|
||||
return Err(UnpackError::Pe32SecondStageRangeInvalid {
|
||||
offset: ss,
|
||||
size: ss_size,
|
||||
image_len: self.decompressed.len(),
|
||||
});
|
||||
}
|
||||
let ss_ct: Vec<u8> = self.decompressed[ss_lo..ss_hi].to_vec();
|
||||
// PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
|
||||
@@ -1872,7 +2066,7 @@ impl<'a> Unpacker<'a> {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return Err(UnpackError::Corrupt);
|
||||
return Err(UnpackError::Pe32RelocationDataNotFound);
|
||||
}
|
||||
if verbose {
|
||||
println!(
|
||||
@@ -1996,7 +2190,9 @@ impl<'a> Unpacker<'a> {
|
||||
let forth_addr = dp_base.wrapping_add(0x40);
|
||||
let fk = header_checksum ^ second_stage_cs ^ forth_stage_key;
|
||||
if !self.decrypt_and_decompress_data(forth_addr, fk, None) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::Pe32FourthStage,
|
||||
));
|
||||
}
|
||||
|
||||
// ---- FifthStage ----
|
||||
@@ -2012,7 +2208,9 @@ impl<'a> Unpacker<'a> {
|
||||
);
|
||||
let fk5 = header_checksum ^ forth_cs ^ fifth_key;
|
||||
if !self.decrypt_and_decompress_data(fifth_addr, fk5, None) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::Pe32FifthStage,
|
||||
));
|
||||
}
|
||||
|
||||
// ---- SevenStage ----
|
||||
@@ -2035,7 +2233,9 @@ impl<'a> Unpacker<'a> {
|
||||
);
|
||||
let fk7 = header_checksum ^ fifth_cs ^ seven_key;
|
||||
if !self.decrypt_and_decompress_data(seven_addr, fk7, None) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::StageDecompressionFailed(
|
||||
DecompressionStage::Pe32SeventhStage,
|
||||
));
|
||||
}
|
||||
|
||||
// ---- EighthStage ----
|
||||
@@ -2063,8 +2263,9 @@ impl<'a> Unpacker<'a> {
|
||||
.ok_or(UnpackError::Pe32CustomDecryptorNotFound)?;
|
||||
let custom_dec_addr = seven_start_actual.wrapping_add(custom_dec_off);
|
||||
self.decrypt_data6(custom_dec_addr);
|
||||
let custom_ops = generate(&self.decompressed, custom_dec_addr)
|
||||
.ok_or(UnpackError::Pe32BytecodeGenFailed)?;
|
||||
let custom_ops = generate(&self.decompressed, custom_dec_addr).ok_or(
|
||||
UnpackError::BytecodeGenerationFailed(BytecodeStage::Pe32CustomDecryptor),
|
||||
)?;
|
||||
|
||||
let seven_cs = self.calculate_checksum(seven_stage_cs_addr);
|
||||
let eighth_addr = dp_base.wrapping_add(0xC0);
|
||||
@@ -2323,7 +2524,7 @@ impl<'a> Unpacker<'a> {
|
||||
// "legacy loose scan" picked the nearest LFSR-shaped block
|
||||
// by offset distance without any validation — that is
|
||||
// exactly how a wrong file_ops got applied to every data
|
||||
// block (uncompressed blocks never hit DecompressFailed),
|
||||
// block (uncompressed blocks never enter the decompressor),
|
||||
// producing a plausible but fully wrong image (the PE32
|
||||
// .text scramble root cause). Trial-and-validate or error.
|
||||
return Err(UnpackError::Pe32FileLfsrNotFound);
|
||||
@@ -2362,8 +2563,9 @@ impl<'a> Unpacker<'a> {
|
||||
};
|
||||
let file_dec_addr = eighth_start.wrapping_add(lfsr_off);
|
||||
self.decrypt_data6(file_dec_addr);
|
||||
let file_ops = generate(&self.decompressed, file_dec_addr)
|
||||
.ok_or(UnpackError::Pe32BytecodeGenFailed)?;
|
||||
let file_ops = generate(&self.decompressed, file_dec_addr).ok_or(
|
||||
UnpackError::BytecodeGenerationFailed(BytecodeStage::Pe32FileDecryptor),
|
||||
)?;
|
||||
|
||||
// ---- PE32 metadata: EP and data dirs from info[3] ----
|
||||
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10));
|
||||
@@ -2448,9 +2650,9 @@ impl<'a> Unpacker<'a> {
|
||||
let clean = &self.file_data;
|
||||
let ko = self.key_offsets;
|
||||
let ks_snap = primitives::aes_schedule_snapshot(&self.decompressed, ko[2])
|
||||
.ok_or(UnpackError::Corrupt)?;
|
||||
.ok_or(UnpackError::InvalidAesKeySchedule { offset: ko[2] })?;
|
||||
let tab_snap = primitives::huffman_table_snapshot(&self.decompressed, ko[0])
|
||||
.ok_or(UnpackError::DecompressFailed)?;
|
||||
.ok_or(UnpackError::InvalidHuffmanTable { offset: ko[0] })?;
|
||||
let spans: Vec<(usize, usize)> = blocks
|
||||
.iter()
|
||||
.map(|b| {
|
||||
@@ -2472,7 +2674,10 @@ impl<'a> Unpacker<'a> {
|
||||
if !primitives::decompress_tbl(
|
||||
&tab_snap, span, rel as u32, rel as u32, b.ssz, b.dsz,
|
||||
) {
|
||||
return Err(UnpackError::DecompressFailed);
|
||||
return Err(UnpackError::SectionDecompressionFailed {
|
||||
pipeline: SectionPipeline::ExePe32,
|
||||
block: i,
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -2805,8 +3010,43 @@ impl<'a> Unpacker<'a> {
|
||||
if !is_dll && !primitives::pe32_imports_already_match_idata_layout(&mut out, pe_off) {
|
||||
primitives::move_pe32_imports_to_kmiat(&mut out, pe_off);
|
||||
}
|
||||
let compact =
|
||||
primitives::compact_memory_image_to_pe(&out, pe_off).ok_or(UnpackError::Corrupt)?;
|
||||
let compact = primitives::compact_memory_image_to_pe(&out, pe_off)
|
||||
.ok_or(UnpackError::Pe32OutputLayoutInvalid)?;
|
||||
Ok(compact)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod error_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn short_input_reports_actual_and_required_lengths() {
|
||||
let error = unpack(&[0; 4096]).expect_err("header must be rejected");
|
||||
assert_eq!(
|
||||
error,
|
||||
UnpackError::InputTooShort {
|
||||
actual: 4096,
|
||||
required: 4128,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn structured_errors_include_stage_and_block_context() {
|
||||
let stage = UnpackError::StageDecompressionFailed(DecompressionStage::ExeStage4);
|
||||
assert_eq!(
|
||||
stage.to_string(),
|
||||
"EXE stage4 decompression failed — corrupt data or wrong offset"
|
||||
);
|
||||
|
||||
let block = UnpackError::SectionDecompressionFailed {
|
||||
pipeline: SectionPipeline::ExePe32,
|
||||
block: 7,
|
||||
};
|
||||
assert_eq!(
|
||||
block.to_string(),
|
||||
"PE32 EXE section block 7 decompression failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+207
-16
@@ -9,8 +9,14 @@ pub(crate) mod parallel;
|
||||
pub(crate) mod primitives;
|
||||
mod tables;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub use dll::{unpack_dll, unpack_dll_v};
|
||||
pub use exe::{UnpackError, unpack as unpack_exe, unpack_v as unpack_exe_v};
|
||||
pub use exe::{
|
||||
BufferOperation, BytecodeStage, DecompressionStage, DescriptorTable, SectionPipeline,
|
||||
UnpackError, unpack as unpack_exe, unpack_v as unpack_exe_v,
|
||||
};
|
||||
pub use integrity::{IntegrityReport, check as check_integrity};
|
||||
|
||||
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer
|
||||
@@ -20,11 +26,139 @@ pub use integrity::{IntegrityReport, check as check_integrity};
|
||||
/// Real protected binaries are far below this.
|
||||
pub(crate) const MAX_IMAGE_SIZE: u64 = 1 << 30; // 1 GiB
|
||||
|
||||
#[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)))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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);
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn install_panic_capture_hook() {}
|
||||
|
||||
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::Corrupt`] so the public API stays panic-free on any input
|
||||
/// (truncated/garbled files chase offsets out of bounds). The default panic
|
||||
/// hook is suppressed transiently so a trapped panic does not spill a
|
||||
/// backtrace to stderr.
|
||||
/// [`UnpackError::InternalPanic`] so the public API stays panic-free on any input
|
||||
/// (truncated/garbled files chase offsets out of bounds). The panic location and
|
||||
/// payload are captured for diagnostics without printing a backtrace to stderr.
|
||||
///
|
||||
/// Note: allocation *failures* abort the process and are NOT caught here; size
|
||||
/// requests are bounds-checked against [`MAX_IMAGE_SIZE`] before allocating.
|
||||
@@ -32,17 +166,17 @@ pub(crate) fn catch_unpack<F>(f: F) -> Result<Vec<u8>, UnpackError>
|
||||
where
|
||||
F: FnOnce() -> Result<Vec<u8>, UnpackError>,
|
||||
{
|
||||
// Hook suppression is skipped on wasm: the prebuilt std cannot unwind
|
||||
// there, so a panic traps immediately — and the suppressed hook would
|
||||
// hide the panic message, leaving a bare `unreachable` with no clue.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let prev = std::panic::take_hook();
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
std::panic::set_hook(Box::new(|_| {}));
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
std::panic::set_hook(prev);
|
||||
r.unwrap_or(Err(UnpackError::Corrupt))
|
||||
// Hook capture is skipped on wasm: the prebuilt std cannot unwind there,
|
||||
// so a panic traps immediately. The Web Worker boundary reports that trap.
|
||||
install_panic_capture_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())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Crackproof header magic stored in `keys[1]`/`info[1]`.
|
||||
@@ -208,3 +342,60 @@ pub fn unpack_auto_v(input: &[u8], verbose: bool) -> Result<(Kind, Vec<u8>), Unp
|
||||
};
|
||||
Ok((detected.kind, out))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn caught_panic_reports_location_and_message() {
|
||||
let error = catch_unpack(|| -> Result<Vec<u8>, UnpackError> {
|
||||
panic!("test panic");
|
||||
})
|
||||
.expect_err("panic must become an error");
|
||||
let UnpackError::InternalPanic {
|
||||
message,
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
} = error
|
||||
else {
|
||||
panic!("unexpected error: {error}");
|
||||
};
|
||||
assert_eq!(message, "test panic");
|
||||
assert!(file.ends_with("src/unpacker/mod.rs") || file.ends_with("src\\unpacker\\mod.rs"));
|
||||
assert!(line > 0);
|
||||
assert!(column > 0);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn worker_panic_keeps_the_worker_source_location() {
|
||||
let error = catch_unpack(|| -> Result<Vec<u8>, UnpackError> {
|
||||
let capture = current_panic_capture();
|
||||
let result = std::thread::spawn(move || {
|
||||
with_panic_capture(capture, || panic!("worker panic"));
|
||||
})
|
||||
.join();
|
||||
if let Err(payload) = result {
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
Ok(Vec::new())
|
||||
})
|
||||
.expect_err("worker panic must become an error");
|
||||
let UnpackError::InternalPanic {
|
||||
message,
|
||||
file,
|
||||
line,
|
||||
column,
|
||||
} = error
|
||||
else {
|
||||
panic!("unexpected error: {error}");
|
||||
};
|
||||
assert_eq!(message, "worker panic");
|
||||
assert!(file.ends_with("src/unpacker/mod.rs") || file.ends_with("src\\unpacker\\mod.rs"));
|
||||
assert!(line > 0);
|
||||
assert!(column > 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ pub(crate) fn thread_cap() -> usize {
|
||||
///
|
||||
/// Returns the first `Err` any block produces; re-raises the first block panic
|
||||
/// on the calling thread (so the pipeline's existing `catch_unpack` still
|
||||
/// converts it to `UnpackError::Corrupt`).
|
||||
/// converts it to `UnpackError::InternalPanic`).
|
||||
pub(crate) fn parallel_for<E, F>(
|
||||
buf: &mut [u8],
|
||||
spans: &[(usize, usize)],
|
||||
@@ -125,6 +125,7 @@ where
|
||||
let stop = AtomicBool::new(false);
|
||||
let first_err: Mutex<Option<E>> = Mutex::new(None);
|
||||
let first_panic: Mutex<Option<Box<dyn std::any::Any + Send>>> = Mutex::new(None);
|
||||
let panic_capture = super::current_panic_capture();
|
||||
|
||||
std::thread::scope(|scope| {
|
||||
for _ in 0..workers {
|
||||
@@ -133,6 +134,7 @@ where
|
||||
let first_err = &first_err;
|
||||
let first_panic = &first_panic;
|
||||
let f = &f;
|
||||
let panic_capture = panic_capture.clone();
|
||||
scope.spawn(move || {
|
||||
loop {
|
||||
if stop.load(Ordering::Relaxed) {
|
||||
@@ -141,9 +143,15 @@ where
|
||||
let next = iter.lock().unwrap().next();
|
||||
let Some((i, piece)) = next else { break };
|
||||
let span = piece.unwrap();
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
f(i, spans[i].0, span)
|
||||
}));
|
||||
// Keep details local until this panic wins `first_panic`;
|
||||
// otherwise simultaneous workers could pair one worker's
|
||||
// location with another worker's propagated payload.
|
||||
let block_capture = panic_capture.as_ref().map(|_| super::PanicCapture::new());
|
||||
let r = super::with_panic_capture(block_capture.clone(), || {
|
||||
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
f(i, spans[i].0, span)
|
||||
}))
|
||||
});
|
||||
match r {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(e)) => {
|
||||
@@ -157,6 +165,11 @@ where
|
||||
Err(panic) => {
|
||||
let mut slot = first_panic.lock().unwrap();
|
||||
if slot.is_none() {
|
||||
if let (Some(parent), Some(block)) =
|
||||
(&panic_capture, &block_capture)
|
||||
{
|
||||
parent.merge_from(block);
|
||||
}
|
||||
*slot = Some(panic);
|
||||
}
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
|
||||
+77
-11
@@ -69,9 +69,22 @@ pub(crate) fn write_u32(data: &mut [u8], offset: u32, value: u32) {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn try_u32(d: &[u8], off: usize) -> Result<u32, super::UnpackError> {
|
||||
d.get(off..off + 4)
|
||||
let end = off
|
||||
.checked_add(4)
|
||||
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::BufferOperation::Read,
|
||||
offset: off,
|
||||
size: 4,
|
||||
buffer_len: d.len(),
|
||||
})?;
|
||||
d.get(off..end)
|
||||
.map(|s| u32::from_le_bytes(s.try_into().unwrap()))
|
||||
.ok_or(super::UnpackError::OutOfBounds(off))
|
||||
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::BufferOperation::Read,
|
||||
offset: off,
|
||||
size: 4,
|
||||
buffer_len: d.len(),
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -79,7 +92,7 @@ pub(crate) fn try_i32(d: &[u8], off: usize) -> Result<i32, super::UnpackError> {
|
||||
try_u32(d, off).map(|v| v as i32)
|
||||
}
|
||||
|
||||
/// Checked copy: returns OutOfBounds if src or dst ranges exceed their respective slices.
|
||||
/// Checked copy with distinct source and destination range errors.
|
||||
pub(crate) fn try_copy_from_slice(
|
||||
dst: &mut [u8],
|
||||
dst_off: usize,
|
||||
@@ -87,17 +100,39 @@ pub(crate) fn try_copy_from_slice(
|
||||
src: &[u8],
|
||||
src_off: usize,
|
||||
) -> Result<(), super::UnpackError> {
|
||||
let dst_end = dst_off
|
||||
.checked_add(dst_len)
|
||||
.ok_or(super::UnpackError::OutOfBounds(dst_off))?;
|
||||
let src_end = src_off
|
||||
.checked_add(dst_len)
|
||||
.ok_or(super::UnpackError::OutOfBounds(src_off))?;
|
||||
let dst_end =
|
||||
dst_off
|
||||
.checked_add(dst_len)
|
||||
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::BufferOperation::CopyDestination,
|
||||
offset: dst_off,
|
||||
size: dst_len,
|
||||
buffer_len: dst.len(),
|
||||
})?;
|
||||
let src_end =
|
||||
src_off
|
||||
.checked_add(dst_len)
|
||||
.ok_or(super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::BufferOperation::CopySource,
|
||||
offset: src_off,
|
||||
size: dst_len,
|
||||
buffer_len: src.len(),
|
||||
})?;
|
||||
if dst_end > dst.len() {
|
||||
return Err(super::UnpackError::OutOfBounds(dst_off));
|
||||
return Err(super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::BufferOperation::CopyDestination,
|
||||
offset: dst_off,
|
||||
size: dst_len,
|
||||
buffer_len: dst.len(),
|
||||
});
|
||||
}
|
||||
if src_end > src.len() {
|
||||
return Err(super::UnpackError::OutOfBounds(src_off));
|
||||
return Err(super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::BufferOperation::CopySource,
|
||||
offset: src_off,
|
||||
size: dst_len,
|
||||
buffer_len: src.len(),
|
||||
});
|
||||
}
|
||||
dst[dst_off..dst_end].copy_from_slice(&src[src_off..src_end]);
|
||||
Ok(())
|
||||
@@ -2005,6 +2040,37 @@ fn score_dd8_shift(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn checked_copy_distinguishes_source_and_destination_ranges() {
|
||||
let mut short_destination = [0u8; 2];
|
||||
let source = [1u8; 4];
|
||||
let error = try_copy_from_slice(&mut short_destination, 0, 3, &source, 0)
|
||||
.expect_err("destination must be rejected");
|
||||
assert!(matches!(
|
||||
error,
|
||||
super::super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::super::BufferOperation::CopyDestination,
|
||||
offset: 0,
|
||||
size: 3,
|
||||
buffer_len: 2,
|
||||
}
|
||||
));
|
||||
|
||||
let mut destination = [0u8; 4];
|
||||
let short_source = [1u8; 2];
|
||||
let error = try_copy_from_slice(&mut destination, 0, 3, &short_source, 0)
|
||||
.expect_err("source must be rejected");
|
||||
assert!(matches!(
|
||||
error,
|
||||
super::super::UnpackError::BufferRangeOutOfBounds {
|
||||
operation: super::super::BufferOperation::CopySource,
|
||||
offset: 0,
|
||||
size: 3,
|
||||
buffer_len: 2,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aes_ks_variant_matches_single_buffer() {
|
||||
// Random-ish key schedule at ko and data block; both variants must
|
||||
|
||||
Reference in New Issue
Block a user