feat: Optimize error reporting

This commit is contained in:
bfloat16
2026-08-11 10:51:37 +08:00
parent 21cd151e15
commit 67178d34af
7 changed files with 664 additions and 113 deletions
+7 -2
View File
@@ -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 - **`src/unpacker/`** — the core. Pure functions over byte slices: no file
I/O, no environment access (beyond a few debugging overrides, see I/O, no environment access (beyond a few debugging overrides, see
[development.md](development.md)), panic-free at the public boundary (all [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. is what the WebAssembly build embeds.
- **`src/` (top level)** — the CLI shell: argument parsing, recursive folder - **`src/` (top level)** — the CLI shell: argument parsing, recursive folder
scanning, per-run log file, progress bar, Explorer-friendly exit pause, and 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` The public API never panics: every pipeline runs under a `catch_unwind`
wrapper (`catch_unpack`) that converts a trapped panic to 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 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 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 allocation. In folder mode each file is isolated: one file's failure is logged
+2 -2
View File
@@ -61,8 +61,8 @@ impl OpsLut {
pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> { pub fn generate(data: &[u8], offset: u32) -> Option<Vec<Op>> {
// Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the // Bounds-checked cursor: a corrupt `data_offset` (bad decrypt_data6 / the
// alignment fallback) must yield `None`, not an out-of-bounds panic — the // alignment fallback) must yield `None`, not an out-of-bounds panic — the
// panic path would surface as a misleading `UnpackError::Corrupt` instead // panic path would surface as a misleading `UnpackError::InternalPanic` instead
// of the precise `BytecodeGenFailed`, and any future caller without a // of the precise `BytecodeGenerationFailed`, and any future caller without a
// `catch_unwind` wrapper would abort outright. // `catch_unwind` wrapper would abort outright.
let mut pos = offset as usize; let mut pos = offset as usize;
let mut next = move || { let mut next = move || {
+61 -25
View File
@@ -13,9 +13,12 @@
//! CalculateChecksumWithSizeXor -> primitives::calculate_checksum //! CalculateChecksumWithSizeXor -> primitives::calculate_checksum
//! CalculateCrc32 -> crc32::compute (via above) //! CalculateCrc32 -> crc32::compute (via above)
use super::UnpackError;
use super::bytecode::{Op, OpsLut, generate}; use super::bytecode::{Op, OpsLut, generate};
use super::primitives::{self, *}; use super::primitives::{self, *};
use super::{
BufferOperation, BytecodeStage, DecompressionStage, DescriptorTable, SectionPipeline,
UnpackError,
};
/// Read a signed 32-bit little-endian value. /// Read a signed 32-bit little-endian value.
fn get_i32(d: &[u8], offset: i32) -> i32 { fn get_i32(d: &[u8], offset: i32) -> i32 {
@@ -68,6 +71,7 @@ fn decrypt_data4(
key: i32, key: i32,
decomp_params: &[i32; 4], decomp_params: &[i32; 4],
transform: Option<&[Op]>, transform: Option<&[Op]>,
stage: DecompressionStage,
) -> Result<(), UnpackError> { ) -> Result<(), UnpackError> {
let addr = get_i32(d, offset); let addr = get_i32(d, offset);
let size = get_i32(d, offset + 4); let size = get_i32(d, offset + 4);
@@ -95,7 +99,7 @@ fn decrypt_data4(
size as u32, size as u32,
decompressed_size as u32, decompressed_size as u32,
) { ) {
return Err(UnpackError::DecompressFailed); return Err(UnpackError::StageDecompressionFailed(stage));
} }
} }
Ok(()) Ok(())
@@ -218,7 +222,11 @@ fn decrypt_and_decompress_data(
// Guard: need 16 bytes at section_data_offset in `d` // Guard: need 16 bytes at section_data_offset in `d`
let off = section_data_offset as usize; let off = section_data_offset as usize;
if off.saturating_add(16) > d.len() { 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); decrypt_data6_shift6(d, section_data_offset, 16);
let dest_offset = get_i32(d, section_data_offset); 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 lut = OpsLut::new(decrypt_func);
let ko0 = decomp_params[0]; let ko0 = decomp_params[0];
let ko2 = decomp_params[2]; let ko2 = decomp_params[2];
let ks_snap = let ks_snap = primitives::aes_schedule_snapshot(d, ko2 as u32)
primitives::aes_schedule_snapshot(d, ko2 as u32).ok_or(UnpackError::Corrupt)?; .ok_or(UnpackError::InvalidAesKeySchedule { offset: ko2 as u32 })?;
let tab_snap = primitives::huffman_table_snapshot(d, ko0 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 let spans: Vec<(usize, usize)> = blocks
.iter() .iter()
.map(|b| { .map(|b| {
@@ -277,7 +285,10 @@ fn decrypt_and_decompress_data(
b.size as u32, b.size as u32,
b.expected_crc as u32, b.expected_crc as u32,
) { ) {
return Err(UnpackError::DecompressFailed); return Err(UnpackError::SectionDecompressionFailed {
pipeline: SectionPipeline::Dll,
block: i,
});
} }
} }
Ok(()) Ok(())
@@ -292,7 +303,11 @@ fn decrypt_and_decompress_data(
// decrypts 16 too, so guard 16 (an 8-byte guard would let // decrypts 16 too, so guard 16 (an 8-byte guard would let
// decrypt_data6_shift6 index past the end of a truncated descriptor). // decrypt_data6_shift6 index past the end of a truncated descriptor).
if off.saturating_add(16) > d.len() { 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); decrypt_data6_shift6(d, section_data_offset, 16);
let zero_offset = get_i32(d, section_data_offset); let zero_offset = get_i32(d, section_data_offset);
@@ -305,7 +320,12 @@ fn decrypt_and_decompress_data(
for i in 0..zero_size { for i in 0..zero_size {
let idx = (zero_offset + i) as usize; let idx = (zero_offset + i) as usize;
if idx >= d.len() { 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; 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> { fn unpack_dll_inner(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
if input.len() < 4096 { const HEADER_LEN: usize = 4128;
return Err(UnpackError::InputTooShort(input.len())); 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. // `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) { if !super::is_supported_magic(keys[1] as u32) {
return Err(UnpackError::DllUnpack( return Err(UnpackError::HeaderMagicMismatch {
"Not a Crackproof protected file (KONN magic mismatch)".into(), found: keys[1] as u32,
)); });
} }
let pe_offset = get_i32(file_data, 60); let pe_offset = get_i32(file_data, 60);
if pe_offset < 0 || (pe_offset as usize).saturating_add(84) > file_data.len() { 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 // 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 // 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 // structurally plausible but unloadable file. Reject early with a clear
// error so `unpack_auto`'s EXE-pipeline fallback handles PE32 DLLs (that // 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. // path is PE32-aware — see run_pe32), instead of us mangling them here.
if get_i32(file_data, pe_offset + 24) & 0xFFFF != 0x20B { let optional_magic = get_u16(file_data, (pe_offset + 24) as u32);
return Err(UnpackError::DllUnpack( if optional_magic != 0x20B {
"not a PE32+ image (the DLL pipeline handles 64-bit only)".into(), return Err(UnpackError::UnsupportedDllPeMagic {
)); found: optional_magic,
});
} }
let size_of_image = get_i32(file_data, pe_offset + 80); 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 { 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 mut out = vec![0u8; size_of_image as usize];
let base_offset = keys[6] - keys[3] + 0x2000; 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), table_val ^ checksum2 ^ (xor_accumulator as i32),
&decomp_params, &decomp_params,
None, None,
DecompressionStage::DllCodeBlock1,
)?; )?;
let addr3b = get_i32(&out, decrypted_addr1 + 3728); 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, crc_xored ^ (xor_accumulator as i32) ^ trailing_val,
&decomp_params, &decomp_params,
None, None,
DecompressionStage::DllCodeBlock2,
)?; )?;
let checksum3 = calculate_checksum(&out, (decrypted_addr1 + 3480) as u32) as i32; 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, (not_val ^ (xor_key as u32)) as i32,
&decomp_params, &decomp_params,
None, None,
DecompressionStage::DllCodeBlock3,
)?; )?;
let addr4 = get_i32(&out, addr4_offset); 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); lfsr_seed_val = lfsr_seed_val.wrapping_add(k);
} }
let decrypt_func = generate(&out, lfsr as u32) let decrypt_func = generate(&out, lfsr as u32).ok_or(UnpackError::BytecodeGenerationFailed(
.ok_or_else(|| UnpackError::DllUnpack("Failed to build decryption expression".into()))?; BytecodeStage::DllPrimaryDecryptor,
))?;
let addr5_offset = decrypted_addr1 + 3840; let addr5_offset = decrypted_addr1 + 3840;
let addr5 = get_i32(&out, addr5_offset); 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, lfsr_seed_val ^ xor_key ^ checksum4,
&decomp_params, &decomp_params,
Some(&decrypt_func), Some(&decrypt_func),
DecompressionStage::DllCodeBlock4,
)?; )?;
if verbose { if verbose {
println!("[7/9] Decrypting code block 4 (addr5)..."); 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; let lfsr2 = metadata_offset + 88;
decrypt_data6(&mut out, lfsr2 as u32); decrypt_data6(&mut out, lfsr2 as u32);
let decrypt_func2 = generate(&out, lfsr2 as u32).ok_or_else(|| { let decrypt_func2 = generate(&out, lfsr2 as u32).ok_or(
UnpackError::DllUnpack("Failed to build second decryption expression".into()) UnpackError::BytecodeGenerationFailed(BytecodeStage::DllSectionDecryptor),
})?; )?;
let section_image_base = 4095 - get_i32(original_file_data, 4224); let section_image_base = 4095 - get_i32(original_file_data, 4224);
let section_data_offset = get_i32(&out, addr5 + 11976); let section_data_offset = get_i32(&out, addr5 + 11976);
+293 -53
View File
@@ -2,13 +2,121 @@ use super::bytecode::{Op, OpsLut, generate};
use super::primitives; use super::primitives;
use super::primitives::*; use super::primitives::*;
#[derive(Debug, thiserror::Error)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnpackError { pub enum DecompressionStage {
#[error("input too short for header (need at least 4096 bytes, got {0})")] ExeStage3,
InputTooShort(usize), ExeStage3Secondary,
ExeStage4,
ExeStage5,
Pe32FourthStage,
Pe32FifthStage,
Pe32SeventhStage,
DllCodeBlock1,
DllCodeBlock2,
DllCodeBlock3,
DllCodeBlock4,
}
#[error("info[1] mismatch — corrupt data or wrong offset")] impl std::fmt::Display for DecompressionStage {
HeaderMismatch, 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")] #[error("anchor field not found — corrupt data or wrong offset")]
AnchorNotFound, AnchorNotFound,
@@ -22,23 +130,42 @@ pub enum UnpackError {
#[error("table_start not found — corrupt data or wrong offset")] #[error("table_start not found — corrupt data or wrong offset")]
TableStartNotFound, TableStartNotFound,
#[error("stage4 bytecode generation failed — corrupt data or wrong offset")] #[error("{0} bytecode generation failed — corrupt data or wrong offset")]
BytecodeGenFailed, BytecodeGenerationFailed(BytecodeStage),
#[error("stage5 marker not found — this build's layout is not supported by this unpacker")] #[error("stage5 marker not found — this build's layout is not supported by this unpacker")]
Stage5MarkerNotFound, 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")] #[error("not a Crackproof-protected file")]
NotCrackproof, NotCrackproof,
#[error("out-of-bounds access at offset {0}")] #[error("invalid PE header offset {offset} for {input_len}-byte input")]
OutOfBounds(usize), 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")] #[error("PE32 tbl not found — corrupt data or wrong offset")]
Pe32TblNotFound, Pe32TblNotFound,
@@ -49,20 +176,52 @@ pub enum UnpackError {
#[error("PE32 customDecryptor not found in sevenStage")] #[error("PE32 customDecryptor not found in sevenStage")]
Pe32CustomDecryptorNotFound, Pe32CustomDecryptorNotFound,
#[error("PE32 stage bytecode generation failed")]
Pe32BytecodeGenFailed,
#[error("PE32 eighthStageKey not found")] #[error("PE32 eighthStageKey not found")]
Pe32EighthKeyNotFound, Pe32EighthKeyNotFound,
#[error("PE32 file LFSR not found in eighthStage")] #[error("PE32 file LFSR not found in eighthStage")]
Pe32FileLfsrNotFound, Pe32FileLfsrNotFound,
#[error("decompression failed — corrupt data or wrong offset")] #[error("{0} decompression failed — corrupt data or wrong offset")]
DecompressFailed, StageDecompressionFailed(DecompressionStage),
#[error("input is corrupt or not a supported Crackproof layout")] #[error("{pipeline} section block {block} decompression failed")]
Corrupt, 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> { 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> { pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
if input.len() < 4096 { const HEADER_LEN: usize = 4128;
return Err(UnpackError::InputTooShort(input.len())); 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 // The pipeline chases offsets read out of the decrypted image; on a
// truncated/garbled-but-detected file those run out of bounds. Trap any // 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]); println!(" info[7] = 0x{:08X}", u.info[7]);
} }
if !super::is_supported_magic(u.info[1]) { 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); 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)); 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 { 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.decompressed = vec![0u8; size_of_image as usize];
u.decrypt_size = u.info[6].wrapping_sub(u.info[3]).wrapping_add(8192); 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); println!(" stage3 = 0x{:08X}", stage3_field);
} }
if !u.decrypt_and_decompress_data(at1, xor_acc ^ chk2 ^ accum, None) { 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)); 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)); .unwrap_or_else(|| stage3_field.wrapping_add(4692));
let v4_val = get_u32(&u.decompressed, v4); let v4_val = get_u32(&u.decompressed, v4);
if !u.decrypt_and_decompress_data(at2, xor_acc ^ chk3 ^ v4_val, None) { 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))); 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); println!(" stage4 = 0x{:08X}", stage4_field);
} }
if !u.decrypt_and_decompress_data(at3, xor_acc ^ chk4 ^ v5_val, None) { 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: // Inside stage4, two locations vary by build:
@@ -758,7 +939,9 @@ impl<'a> Unpacker<'a> {
let ops1 = match generate(&u.decompressed, data_offset) { let ops1 = match generate(&u.decompressed, data_offset) {
Some(v) => v, Some(v) => v,
None => { 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); println!(" stage5 = 0x{:08X}", stage5_field);
} }
if !u.decrypt_and_decompress_data(at4, xor_acc ^ chk4 ^ chk5 ^ accum2, Some(&ops1)) { 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 // 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) { let ops2 = match generate(&u.decompressed, data_offset2) {
Some(v) => v, Some(v) => v,
None => { None => {
return Err(UnpackError::Stage5BytecodeGenFailed); return Err(UnpackError::BytecodeGenerationFailed(
BytecodeStage::ExeStage5,
));
} }
}; };
// The new layout picked its file decryptor by distance (no marker, no // 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). // garbling into the output without any error (see the validator).
let rebase = (!get_u32(u.file_data, 4224)).wrapping_add(4096); let rebase = (!get_u32(u.file_data, 4224)).wrapping_add(4096);
if new_layout && !u.new_layout_file_ops_validate(walk4_slot, &ops2, rebase) { 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; let at6 = walk4_slot;
@@ -980,9 +1167,9 @@ impl<'a> Unpacker<'a> {
// Snapshot the shared tables before the fan-out: workers get // Snapshot the shared tables before the fan-out: workers get
// disjoint span slices, not the whole buffer. // disjoint span slices, not the whole buffer.
let ks_snap = primitives::aes_schedule_snapshot(&u.decompressed, ko[2]) 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]) 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 let spans: Vec<(usize, usize)> = blocks
.iter() .iter()
.map(|b| { .map(|b| {
@@ -1009,7 +1196,10 @@ impl<'a> Unpacker<'a> {
b.len, b.len,
b.plain_len, b.plain_len,
) { ) {
return Err(UnpackError::DecompressFailed); return Err(UnpackError::SectionDecompressionFailed {
pipeline: SectionPipeline::ExePe32Plus,
block: i,
});
} }
} }
Ok(()) Ok(())
@@ -1475,7 +1665,7 @@ impl<'a> Unpacker<'a> {
/// whose fileCS pointer sits just past `info[3]`), not by content. 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 /// coincidental LFSR-shaped block at a shorter distance would decode to a
/// wrong `ops2` translate and silently garble every section block — raw /// 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 /// plausible but wrong image. Replay the first *compressed* block's full
/// transform (raw copy, AES, translate, decompress) on a snapshot and /// transform (raw copy, AES, translate, decompress) on a snapshot and
/// require decompression to succeed; restore the region afterwards. /// require decompression to succeed; restore the region afterwards.
@@ -1837,7 +2027,11 @@ impl<'a> Unpacker<'a> {
let ss_lo = ss as usize; let ss_lo = ss as usize;
let ss_hi = ss_lo.wrapping_add(ss_size as usize); let ss_hi = ss_lo.wrapping_add(ss_size as usize);
if ss_hi < ss_lo || ss_hi > self.decompressed.len() { 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(); 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. // PE32 data dir 5 (BaseReloc) = optional_header(pe+24) + 0x60 + 5*8 = pe+0xA0.
@@ -1872,7 +2066,7 @@ impl<'a> Unpacker<'a> {
} }
} }
if !found { if !found {
return Err(UnpackError::Corrupt); return Err(UnpackError::Pe32RelocationDataNotFound);
} }
if verbose { if verbose {
println!( println!(
@@ -1996,7 +2190,9 @@ impl<'a> Unpacker<'a> {
let forth_addr = dp_base.wrapping_add(0x40); let forth_addr = dp_base.wrapping_add(0x40);
let fk = header_checksum ^ second_stage_cs ^ forth_stage_key; let fk = header_checksum ^ second_stage_cs ^ forth_stage_key;
if !self.decrypt_and_decompress_data(forth_addr, fk, None) { if !self.decrypt_and_decompress_data(forth_addr, fk, None) {
return Err(UnpackError::DecompressFailed); return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::Pe32FourthStage,
));
} }
// ---- FifthStage ---- // ---- FifthStage ----
@@ -2012,7 +2208,9 @@ impl<'a> Unpacker<'a> {
); );
let fk5 = header_checksum ^ forth_cs ^ fifth_key; let fk5 = header_checksum ^ forth_cs ^ fifth_key;
if !self.decrypt_and_decompress_data(fifth_addr, fk5, None) { if !self.decrypt_and_decompress_data(fifth_addr, fk5, None) {
return Err(UnpackError::DecompressFailed); return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::Pe32FifthStage,
));
} }
// ---- SevenStage ---- // ---- SevenStage ----
@@ -2035,7 +2233,9 @@ impl<'a> Unpacker<'a> {
); );
let fk7 = header_checksum ^ fifth_cs ^ seven_key; let fk7 = header_checksum ^ fifth_cs ^ seven_key;
if !self.decrypt_and_decompress_data(seven_addr, fk7, None) { if !self.decrypt_and_decompress_data(seven_addr, fk7, None) {
return Err(UnpackError::DecompressFailed); return Err(UnpackError::StageDecompressionFailed(
DecompressionStage::Pe32SeventhStage,
));
} }
// ---- EighthStage ---- // ---- EighthStage ----
@@ -2063,8 +2263,9 @@ impl<'a> Unpacker<'a> {
.ok_or(UnpackError::Pe32CustomDecryptorNotFound)?; .ok_or(UnpackError::Pe32CustomDecryptorNotFound)?;
let custom_dec_addr = seven_start_actual.wrapping_add(custom_dec_off); let custom_dec_addr = seven_start_actual.wrapping_add(custom_dec_off);
self.decrypt_data6(custom_dec_addr); self.decrypt_data6(custom_dec_addr);
let custom_ops = generate(&self.decompressed, custom_dec_addr) let custom_ops = generate(&self.decompressed, custom_dec_addr).ok_or(
.ok_or(UnpackError::Pe32BytecodeGenFailed)?; UnpackError::BytecodeGenerationFailed(BytecodeStage::Pe32CustomDecryptor),
)?;
let seven_cs = self.calculate_checksum(seven_stage_cs_addr); let seven_cs = self.calculate_checksum(seven_stage_cs_addr);
let eighth_addr = dp_base.wrapping_add(0xC0); 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 // "legacy loose scan" picked the nearest LFSR-shaped block
// by offset distance without any validation — that is // by offset distance without any validation — that is
// exactly how a wrong file_ops got applied to every data // 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 // producing a plausible but fully wrong image (the PE32
// .text scramble root cause). Trial-and-validate or error. // .text scramble root cause). Trial-and-validate or error.
return Err(UnpackError::Pe32FileLfsrNotFound); return Err(UnpackError::Pe32FileLfsrNotFound);
@@ -2362,8 +2563,9 @@ impl<'a> Unpacker<'a> {
}; };
let file_dec_addr = eighth_start.wrapping_add(lfsr_off); let file_dec_addr = eighth_start.wrapping_add(lfsr_off);
self.decrypt_data6(file_dec_addr); self.decrypt_data6(file_dec_addr);
let file_ops = generate(&self.decompressed, file_dec_addr) let file_ops = generate(&self.decompressed, file_dec_addr).ok_or(
.ok_or(UnpackError::Pe32BytecodeGenFailed)?; UnpackError::BytecodeGenerationFailed(BytecodeStage::Pe32FileDecryptor),
)?;
// ---- PE32 metadata: EP and data dirs from info[3] ---- // ---- PE32 metadata: EP and data dirs from info[3] ----
let test_val = get_u32(&self.decompressed, info3.wrapping_add(0x10)); 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 clean = &self.file_data;
let ko = self.key_offsets; let ko = self.key_offsets;
let ks_snap = primitives::aes_schedule_snapshot(&self.decompressed, ko[2]) 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]) 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 let spans: Vec<(usize, usize)> = blocks
.iter() .iter()
.map(|b| { .map(|b| {
@@ -2472,7 +2674,10 @@ impl<'a> Unpacker<'a> {
if !primitives::decompress_tbl( if !primitives::decompress_tbl(
&tab_snap, span, rel as u32, rel as u32, b.ssz, b.dsz, &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(()) Ok(())
@@ -2805,8 +3010,43 @@ impl<'a> Unpacker<'a> {
if !is_dll && !primitives::pe32_imports_already_match_idata_layout(&mut out, pe_off) { if !is_dll && !primitives::pe32_imports_already_match_idata_layout(&mut out, pe_off) {
primitives::move_pe32_imports_to_kmiat(&mut out, pe_off); primitives::move_pe32_imports_to_kmiat(&mut out, pe_off);
} }
let compact = let compact = primitives::compact_memory_image_to_pe(&out, pe_off)
primitives::compact_memory_image_to_pe(&out, pe_off).ok_or(UnpackError::Corrupt)?; .ok_or(UnpackError::Pe32OutputLayoutInvalid)?;
Ok(compact) 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
View File
@@ -9,8 +9,14 @@ pub(crate) mod parallel;
pub(crate) mod primitives; pub(crate) mod primitives;
mod tables; mod tables;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
pub use dll::{unpack_dll, unpack_dll_v}; 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}; pub use integrity::{IntegrityReport, check as check_integrity};
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer /// 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. /// Real protected binaries are far below this.
pub(crate) const MAX_IMAGE_SIZE: u64 = 1 << 30; // 1 GiB 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 /// Run an unpack pipeline, converting any internal panic into a clean
/// [`UnpackError::Corrupt`] so the public API stays panic-free on any input /// [`UnpackError::InternalPanic`] so the public API stays panic-free on any input
/// (truncated/garbled files chase offsets out of bounds). The default panic /// (truncated/garbled files chase offsets out of bounds). The panic location and
/// hook is suppressed transiently so a trapped panic does not spill a /// payload are captured for diagnostics without printing a backtrace to stderr.
/// backtrace to stderr.
/// ///
/// Note: allocation *failures* abort the process and are NOT caught here; size /// Note: allocation *failures* abort the process and are NOT caught here; size
/// requests are bounds-checked against [`MAX_IMAGE_SIZE`] before allocating. /// 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 where
F: FnOnce() -> Result<Vec<u8>, UnpackError>, F: FnOnce() -> Result<Vec<u8>, UnpackError>,
{ {
// Hook suppression is skipped on wasm: the prebuilt std cannot unwind // Hook capture is skipped on wasm: the prebuilt std cannot unwind there,
// there, so a panic traps immediately — and the suppressed hook would // so a panic traps immediately. The Web Worker boundary reports that trap.
// hide the panic message, leaving a bare `unreachable` with no clue. install_panic_capture_hook();
#[cfg(not(target_arch = "wasm32"))] let capture = PanicCapture::new();
let prev = std::panic::take_hook(); let r = with_panic_capture(Some(capture.clone()), || {
#[cfg(not(target_arch = "wasm32"))] std::panic::catch_unwind(std::panic::AssertUnwindSafe(f))
std::panic::set_hook(Box::new(|_| {})); });
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)); match r {
#[cfg(not(target_arch = "wasm32"))] Ok(result) => result,
std::panic::set_hook(prev); Err(payload) => Err(capture.into_error(payload.as_ref())),
r.unwrap_or(Err(UnpackError::Corrupt)) }
} }
/// Crackproof header magic stored in `keys[1]`/`info[1]`. /// 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)) 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);
}
}
+16 -3
View File
@@ -46,7 +46,7 @@ pub(crate) fn thread_cap() -> usize {
/// ///
/// Returns the first `Err` any block produces; re-raises the first block panic /// 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 /// 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>( pub(crate) fn parallel_for<E, F>(
buf: &mut [u8], buf: &mut [u8],
spans: &[(usize, usize)], spans: &[(usize, usize)],
@@ -125,6 +125,7 @@ where
let stop = AtomicBool::new(false); let stop = AtomicBool::new(false);
let first_err: Mutex<Option<E>> = Mutex::new(None); let first_err: Mutex<Option<E>> = Mutex::new(None);
let first_panic: Mutex<Option<Box<dyn std::any::Any + Send>>> = 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| { std::thread::scope(|scope| {
for _ in 0..workers { for _ in 0..workers {
@@ -133,6 +134,7 @@ where
let first_err = &first_err; let first_err = &first_err;
let first_panic = &first_panic; let first_panic = &first_panic;
let f = &f; let f = &f;
let panic_capture = panic_capture.clone();
scope.spawn(move || { scope.spawn(move || {
loop { loop {
if stop.load(Ordering::Relaxed) { if stop.load(Ordering::Relaxed) {
@@ -141,9 +143,15 @@ where
let next = iter.lock().unwrap().next(); let next = iter.lock().unwrap().next();
let Some((i, piece)) = next else { break }; let Some((i, piece)) = next else { break };
let span = piece.unwrap(); let span = piece.unwrap();
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { // 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) f(i, spans[i].0, span)
})); }))
});
match r { match r {
Ok(Ok(())) => {} Ok(Ok(())) => {}
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -157,6 +165,11 @@ where
Err(panic) => { Err(panic) => {
let mut slot = first_panic.lock().unwrap(); let mut slot = first_panic.lock().unwrap();
if slot.is_none() { if slot.is_none() {
if let (Some(parent), Some(block)) =
(&panic_capture, &block_capture)
{
parent.merge_from(block);
}
*slot = Some(panic); *slot = Some(panic);
} }
stop.store(true, Ordering::Relaxed); stop.store(true, Ordering::Relaxed);
+75 -9
View File
@@ -69,9 +69,22 @@ pub(crate) fn write_u32(data: &mut [u8], offset: u32, value: u32) {
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn try_u32(d: &[u8], off: usize) -> Result<u32, super::UnpackError> { 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())) .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)] #[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) 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( pub(crate) fn try_copy_from_slice(
dst: &mut [u8], dst: &mut [u8],
dst_off: usize, dst_off: usize,
@@ -87,17 +100,39 @@ pub(crate) fn try_copy_from_slice(
src: &[u8], src: &[u8],
src_off: usize, src_off: usize,
) -> Result<(), super::UnpackError> { ) -> Result<(), super::UnpackError> {
let dst_end = dst_off let dst_end =
dst_off
.checked_add(dst_len) .checked_add(dst_len)
.ok_or(super::UnpackError::OutOfBounds(dst_off))?; .ok_or(super::UnpackError::BufferRangeOutOfBounds {
let src_end = src_off operation: super::BufferOperation::CopyDestination,
offset: dst_off,
size: dst_len,
buffer_len: dst.len(),
})?;
let src_end =
src_off
.checked_add(dst_len) .checked_add(dst_len)
.ok_or(super::UnpackError::OutOfBounds(src_off))?; .ok_or(super::UnpackError::BufferRangeOutOfBounds {
operation: super::BufferOperation::CopySource,
offset: src_off,
size: dst_len,
buffer_len: src.len(),
})?;
if dst_end > dst.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() { 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]); dst[dst_off..dst_end].copy_from_slice(&src[src_off..src_end]);
Ok(()) Ok(())
@@ -2005,6 +2040,37 @@ fn score_dd8_shift(
mod tests { mod tests {
use super::*; 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] #[test]
fn aes_ks_variant_matches_single_buffer() { fn aes_ks_variant_matches_single_buffer() {
// Random-ish key schedule at ko and data block; both variants must // Random-ish key schedule at ko and data block; both variants must