mirror of
https://github.com/Momoko-Ayase/Senbei.git
synced 2026-11-04 10:31:29 -05:00
fix(android): bound archive extraction and decoder resources
This commit is contained in:
1 parent
22dd92982e
commit
a380031567
21 files changed
+2714
-181
No files matched your search
+113
-4
@@ -1,3 +1,4 @@
|
||||
use senbei_engine::android::ResourceLimits;
|
||||
use senbei_io::{job, pause, scan};
|
||||
use std::path::Path;
|
||||
|
||||
@@ -11,6 +12,7 @@ fn main() -> std::process::ExitCode {
|
||||
let mut verbose = false;
|
||||
let mut scan_all = false;
|
||||
let mut strict_integrity = false;
|
||||
let mut limits = ResourceLimits::default();
|
||||
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
@@ -28,6 +30,13 @@ fn main() -> std::process::ExitCode {
|
||||
"--no-log" => no_log = true,
|
||||
"--scan-all" => scan_all = true,
|
||||
"--strict-integrity" => strict_integrity = true,
|
||||
flag @ ("--max-entry-mib" | "--max-total-mib" | "--max-entries" | "--max-depth") => {
|
||||
if let Err(message) = set_resource_limit(&mut limits, flag, args.next().as_deref())
|
||||
{
|
||||
eprintln!("error: {message}");
|
||||
return std::process::ExitCode::from(2);
|
||||
}
|
||||
}
|
||||
"--out" => match args.next() {
|
||||
Some(v) if !v.starts_with('-') => out = Some(v),
|
||||
_ => {
|
||||
@@ -62,16 +71,17 @@ fn main() -> std::process::ExitCode {
|
||||
let p = Path::new(&p);
|
||||
let out_path = out.as_deref().map(Path::new);
|
||||
let result = if p.is_dir() {
|
||||
job::run_folder_opts(
|
||||
job::run_folder_with_limits(
|
||||
p,
|
||||
out_path,
|
||||
quiet,
|
||||
verbose,
|
||||
no_log,
|
||||
scan_all || scan::scan_all_env(),
|
||||
&limits,
|
||||
)
|
||||
} else {
|
||||
job::run_file_v(p, out_path, quiet, verbose, no_log)
|
||||
job::run_file_with_limits(p, out_path, quiet, verbose, no_log, &limits)
|
||||
};
|
||||
match result {
|
||||
Ok(summary) => {
|
||||
@@ -92,7 +102,7 @@ fn main() -> std::process::ExitCode {
|
||||
};
|
||||
|
||||
pause::maybe_pause(no_pause);
|
||||
std::process::ExitCode::from(code as u8)
|
||||
std::process::ExitCode::from(code)
|
||||
}
|
||||
|
||||
/// Process status for a finished run.
|
||||
@@ -108,9 +118,40 @@ fn exit_code(summary: &job::Summary, strict_integrity: bool) -> u8 {
|
||||
}
|
||||
}
|
||||
|
||||
fn set_resource_limit(
|
||||
limits: &mut ResourceLimits,
|
||||
flag: &str,
|
||||
value: Option<&str>,
|
||||
) -> Result<(), String> {
|
||||
let invalid = || format!("{flag} requires an unsigned integer argument");
|
||||
let text = value
|
||||
.filter(|text| !text.is_empty() && text.bytes().all(|byte| byte.is_ascii_digit()))
|
||||
.ok_or_else(invalid)?;
|
||||
let value: u64 = text.parse().map_err(|_| invalid())?;
|
||||
if value == 0 && flag != "--max-depth" {
|
||||
return Err(format!("{flag} must be greater than zero"));
|
||||
}
|
||||
match flag {
|
||||
"--max-entry-mib" | "--max-total-mib" => {
|
||||
let bytes = value
|
||||
.checked_mul(1024 * 1024)
|
||||
.ok_or_else(|| format!("{flag} exceeds the supported byte range"))?;
|
||||
if flag == "--max-entry-mib" {
|
||||
limits.max_entry_bytes = bytes;
|
||||
} else {
|
||||
limits.max_total_bytes = bytes;
|
||||
}
|
||||
}
|
||||
"--max-entries" => limits.max_entries = usize::try_from(value).map_err(|_| invalid())?,
|
||||
"--max-depth" => limits.max_depth = usize::try_from(value).map_err(|_| invalid())?,
|
||||
_ => return Err(format!("unknown resource option '{flag}'")),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"senbei <file|folder> [--out DIR] [-v|--verbose] [-q|--quiet]... [--scan-all] [--strict-integrity] [--no-log] [--no-pause] [-V|--version] [-h|--help]"
|
||||
"senbei <file|folder> [--out DIR] [-v|--verbose] [-q|--quiet]... [--scan-all] [--strict-integrity] [--max-entry-mib N] [--max-total-mib N] [--max-entries N] [--max-depth N] [--no-log] [--no-pause] [-V|--version] [-h|--help]"
|
||||
);
|
||||
println!(
|
||||
" input a Crackproof PE (.exe/.dll), an il2cpp global-metadata.dat,\n\
|
||||
@@ -129,6 +170,19 @@ fn print_help() {
|
||||
\x20 suspect is still written and the process exits 0 unless\n\
|
||||
\x20 a file errors. Quiet levels are unchanged."
|
||||
);
|
||||
let defaults = ResourceLimits::default();
|
||||
println!(
|
||||
" Android resource limits (MiB = 1048576 bytes):\n\
|
||||
\x20 --max-entry-mib N single archive entry/decoded buffer/output (default {})\n\
|
||||
\x20 --max-total-mib N cumulative archive bytes and per-engine task budget (default {})\n\
|
||||
\x20 --max-entries N archive entries/stage-2 records (default {})\n\
|
||||
\x20 --max-depth N nesting depth, root = 0 (default {})\n\
|
||||
\x20 Limits are checked before extraction/allocation; they are not a peak-RSS cap.",
|
||||
defaults.max_entry_bytes / (1024 * 1024),
|
||||
defaults.max_total_bytes / (1024 * 1024),
|
||||
defaults.max_entries,
|
||||
defaults.max_depth,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -172,3 +226,58 @@ mod strict_integrity {
|
||||
assert_eq!(exit_code(&summary, true), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod resource_limits {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn flags_override_only_the_selected_limit() {
|
||||
let mut limits = ResourceLimits::default();
|
||||
set_resource_limit(&mut limits, "--max-entry-mib", Some("4096")).unwrap();
|
||||
assert_eq!(limits.max_entry_bytes, 4096 * 1024 * 1024);
|
||||
assert_eq!(
|
||||
limits.max_total_bytes,
|
||||
ResourceLimits::default().max_total_bytes
|
||||
);
|
||||
set_resource_limit(&mut limits, "--max-total-mib", Some("16384")).unwrap();
|
||||
set_resource_limit(&mut limits, "--max-entries", Some("123")).unwrap();
|
||||
set_resource_limit(&mut limits, "--max-depth", Some("0")).unwrap();
|
||||
assert_eq!(limits.max_total_bytes, 16384 * 1024 * 1024);
|
||||
assert_eq!(limits.max_entries, 123);
|
||||
assert_eq!(limits.max_depth, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_and_overflowing_limits_are_usage_errors() {
|
||||
let mut limits = ResourceLimits::default();
|
||||
for flag in [
|
||||
"--max-entry-mib",
|
||||
"--max-total-mib",
|
||||
"--max-entries",
|
||||
"--max-depth",
|
||||
] {
|
||||
for value in [
|
||||
None,
|
||||
Some(""),
|
||||
Some("-1"),
|
||||
Some("1.5"),
|
||||
Some("--no-log"),
|
||||
Some("18446744073709551616"),
|
||||
] {
|
||||
assert!(
|
||||
set_resource_limit(&mut limits, flag, value).is_err(),
|
||||
"{flag} {value:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
for flag in ["--max-entry-mib", "--max-total-mib", "--max-entries"] {
|
||||
assert!(set_resource_limit(&mut limits, flag, Some("0")).is_err());
|
||||
}
|
||||
assert!(
|
||||
set_resource_limit(&mut limits, "--max-entry-mib", Some("18446744073709551615"))
|
||||
.is_err()
|
||||
);
|
||||
assert_eq!(limits, ResourceLimits::default());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn command() -> Command {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_senbei"));
|
||||
command.args(["--no-pause", "--no-log"]);
|
||||
command
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_lists_resource_limits_and_defaults() {
|
||||
let output = command().arg("--help").output().unwrap();
|
||||
assert!(output.status.success());
|
||||
let text = String::from_utf8(output.stdout).unwrap();
|
||||
for option in [
|
||||
"--max-entry-mib",
|
||||
"--max-total-mib",
|
||||
"--max-entries",
|
||||
"--max-depth",
|
||||
] {
|
||||
assert!(text.contains(option), "missing {option}");
|
||||
}
|
||||
for value in ["2048", "8192", "100000", "32", "not a peak-RSS cap"] {
|
||||
assert!(text.contains(value), "missing {value}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_limits_exit_with_usage_error() {
|
||||
for option in [
|
||||
"--max-entry-mib",
|
||||
"--max-total-mib",
|
||||
"--max-entries",
|
||||
"--max-depth",
|
||||
] {
|
||||
for value in [
|
||||
None,
|
||||
Some("-1"),
|
||||
Some("invalid"),
|
||||
Some("18446744073709551616"),
|
||||
] {
|
||||
let mut command = command();
|
||||
command.arg(option);
|
||||
if let Some(value) = value {
|
||||
command.arg(value);
|
||||
}
|
||||
let output = command.output().unwrap();
|
||||
assert_eq!(output.status.code(), Some(2), "{option} {value:?}");
|
||||
assert!(String::from_utf8_lossy(&output.stderr).contains(option));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn limits_compose_with_strict_and_quiet_modes() {
|
||||
let input = tempfile::tempdir().unwrap();
|
||||
let out = tempfile::tempdir().unwrap();
|
||||
let output = command()
|
||||
.arg(input.path())
|
||||
.arg("--out")
|
||||
.arg(out.path())
|
||||
.args([
|
||||
"--max-entry-mib",
|
||||
"1",
|
||||
"--max-total-mib",
|
||||
"2",
|
||||
"--max-entries",
|
||||
"1",
|
||||
"--max-depth",
|
||||
"0",
|
||||
"--strict-integrity",
|
||||
"-q",
|
||||
"-q",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{output:?}");
|
||||
assert!(output.stdout.is_empty());
|
||||
assert!(output.stderr.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zip_entry_limit_rejects_before_central_directory_indexing() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let package = temp.path().join("large.apk");
|
||||
// A ZIP end record advertising two entries is enough to exercise the
|
||||
// preflight. Its directory is intentionally absent: the count limit must
|
||||
// reject it before the ZIP parser tries to read that directory.
|
||||
let mut bytes = b"PK\x03\x04".to_vec();
|
||||
let mut end = [0_u8; 22];
|
||||
end[..4].copy_from_slice(b"PK\x05\x06");
|
||||
end[8..10].copy_from_slice(&2_u16.to_le_bytes());
|
||||
end[10..12].copy_from_slice(&2_u16.to_le_bytes());
|
||||
bytes.extend_from_slice(&end);
|
||||
std::fs::write(&package, bytes).unwrap();
|
||||
let output = command()
|
||||
.arg(&package)
|
||||
.arg("--out")
|
||||
.arg(temp.path().join("out"))
|
||||
.args(["--max-entries", "1"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(output.status.code(), Some(1));
|
||||
assert!(
|
||||
String::from_utf8_lossy(&output.stderr).contains("entry limit"),
|
||||
"{output:?}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user