refactor: align platform crate boundaries

This commit is contained in:
bfloat16
2026-09-07 19:29:54 +08:00
parent aa1bcaa2eb
commit 6250ca4e98
43 changed files with 1004 additions and 1004 deletions
+2 -1
View File
@@ -7,7 +7,6 @@ license.workspace = true
description = "Platform unpacking engines for Senbei"
[dependencies]
goblin.workspace = true
memmap2.workspace = true
serde.workspace = true
serde_json.workspace = true
@@ -15,6 +14,8 @@ sha2.workspace = true
tempfile.workspace = true
thiserror.workspace = true
senbei-crypto.workspace = true
senbei-elf.workspace = true
senbei-pe.workspace = true
[lints]
workspace = true
+32
View File
@@ -0,0 +1,32 @@
//! Shared Android engine filesystem and digest helpers.
use std::io::Write;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
pub(crate) fn absolute(path: &Path) -> std::io::Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
std::env::current_dir().map(|current| current.join(path))
}
}
pub(crate) fn write_atomic(path: &Path, data: &[u8]) -> std::io::Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)?;
let mut temporary = NamedTempFile::new_in(parent)?;
temporary.write_all(data)?;
temporary.as_file().sync_all()?;
temporary.persist(path).map_err(|error| error.error)?;
Ok(())
}
#[must_use]
pub(crate) fn sha256(data: &[u8]) -> String {
let mut digest = Sha256::new();
digest.update(data);
senbei_crypto::hex_digest(&digest.finalize())
}
+1 -1
View File
@@ -14,7 +14,7 @@ pub enum Error {
Elf {
path: PathBuf,
#[source]
source: goblin::error::Error,
source: senbei_elf::Error,
},
#[error("serialize extraction index: {0}")]
Json(#[from] serde_json::Error),
+7 -37
View File
@@ -1,14 +1,12 @@
use std::collections::{BTreeMap, BTreeSet, HashSet};
use std::fs::{File, create_dir_all};
use std::io::Write;
use std::fs::File;
use std::path::{Path, PathBuf};
use memmap2::MmapOptions;
use senbei_crypto::android::{Module9bConfig, decode_container};
use serde_json::to_vec_pretty;
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
use super::super::common;
use super::error::{Error, Result, invalid};
use super::report::{
ArtifactReport, DecoderReport, ExtractionReport, ModuleRegistryEntry, RecordReport,
@@ -86,7 +84,7 @@ pub fn extract_stage2(options: &ExtractOptions) -> Result<ExtractionReport> {
return invalid("refusing to overwrite the protected ELF with Stage 2 output");
}
}
create_dir_all(&output_dir)
std::fs::create_dir_all(&output_dir)
.map_err(|source| Error::io("create Stage 2 output directory", &output_dir, source))?;
let file = File::open(&input_path)
@@ -497,42 +495,14 @@ fn write_json_atomic(path: &Path, value: &impl serde::Serialize) -> Result<()> {
}
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
create_dir_all(parent)
.map_err(|source| Error::io("create output directory", parent, source))?;
let mut temporary = NamedTempFile::new_in(parent)
.map_err(|source| Error::io("create temporary output", parent, source))?;
temporary
.write_all(data)
.and_then(|()| temporary.as_file().sync_all())
.map_err(|source| Error::io("write temporary output", temporary.path(), source))?;
temporary
.persist(path)
.map_err(|error| Error::io("replace output", path, error.error))?;
Ok(())
common::write_atomic(path, data)
.map_err(|source| Error::io("write temporary output", path, source))
}
fn absolute(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
std::env::current_dir()
.map(|current| current.join(path))
.map_err(|source| Error::io("query current directory", path, source))
}
common::absolute(path).map_err(|source| Error::io("query current directory", path, source))
}
fn sha256(data: &[u8]) -> String {
let mut digest = Sha256::new();
digest.update(data);
hex_digest(&digest.finalize())
}
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
/// hex directly).
fn hex_digest(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len() * 2);
for byte in data {
out.push_str(&format!("{byte:02x}"));
}
out
common::sha256(data)
}
@@ -7,9 +7,6 @@ use super::stage1::{self, DEFAULT_CIPHER_CONSTANT, DEFAULT_OUTER_SIZE};
/// Return whether `data` has a supported protected AArch64 IL2CPP layout.
#[must_use]
pub fn is_protected_libil2cpp(data: &[u8]) -> bool {
if !stage1::looks_protected(data) {
return false;
}
let Ok(stage1) = stage1::inspect(
data,
Path::new("<probe>"),
+14 -36
View File
@@ -1,44 +1,13 @@
use std::path::Path;
use goblin::elf::{Elf, header::EM_AARCH64};
use senbei_elf::{AARCH64_MACHINE, Error as ElfError, parse};
use super::error::{Error, Result, invalid};
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
pub(crate) use senbei_elf::SHT_LOUSER;
pub const DEFAULT_CIPHER_CONSTANT: u32 = 0xbf20_165d;
pub const DEFAULT_OUTER_SIZE: usize = 0x23c;
pub(crate) fn looks_protected(data: &[u8]) -> bool {
let Ok(elf) = Elf::parse(data) else {
return false;
};
if elf.header.e_machine != EM_AARCH64
|| elf
.section_headers
.iter()
.filter(|section| section.sh_type == SHT_LOUSER)
.count()
!= 1
{
return false;
}
[
".dynsym",
".dynstr",
".gnu.hash",
".gnu.version",
".gnu.version_r",
]
.into_iter()
.all(|wanted| {
elf.section_headers.iter().any(|section| {
elf.shdr_strtab
.get_at(section.sh_name)
.is_some_and(|name| name == wanted)
})
})
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Stage1Header {
pub key: u32,
@@ -70,13 +39,13 @@ pub(crate) fn inspect(
outer_size: usize,
cipher_constant: u32,
) -> Result<Stage1Result> {
let elf = Elf::parse(data).map_err(|source| Error::Elf {
let elf = parse(data).map_err(|source: ElfError| Error::Elf {
path: path.to_path_buf(),
source,
})?;
if elf.header.e_machine != EM_AARCH64 {
if elf.header.e_machine != AARCH64_MACHINE {
return invalid(format!(
"expected AArch64 ELF (machine 0x{EM_AARCH64:X}), got 0x{:X}",
"expected AArch64 ELF (machine 0x{AARCH64_MACHINE:X}), got 0x{:X}",
elf.header.e_machine
));
}
@@ -92,6 +61,15 @@ pub(crate) fn inspect(
matches.len()
));
}
for wanted in senbei_elf::PROBE_SECTION_NAMES {
if !elf.section_headers.iter().any(|section| {
elf.shdr_strtab
.get_at(section.sh_name)
.is_some_and(|name| name == wanted)
}) {
return invalid(format!("protected ELF lacks required section {wanted}"));
}
}
let (section_index, section) = matches[0];
let section_offset = usize::try_from(section.sh_offset)
.map_err(|_| Error::Invalid("SHT_LOUSER offset exceeds usize".to_owned()))?;
+1
View File
@@ -1,5 +1,6 @@
//! Android AArch64 extraction and ELF restoration.
mod common;
mod extract;
mod restore;
@@ -14,6 +14,8 @@ pub enum Error {
Json(#[from] serde_json::Error),
#[error(transparent)]
Crypto(#[from] senbei_crypto::android::Error),
#[error(transparent)]
Elf(#[from] senbei_elf::Error),
#[error("{0}")]
Invalid(String),
}
-107
View File
@@ -1,107 +0,0 @@
use super::error::{Error, Result, invalid};
#[must_use]
pub(crate) fn elf_hash(name: &[u8]) -> u32 {
let mut value = 0_u32;
for &byte in name {
value = value.wrapping_shl(4).wrapping_add(u32::from(byte));
let high = value & 0xf000_0000;
if high != 0 {
value ^= high >> 24;
value &= !high;
}
}
value
}
#[must_use]
pub(crate) fn gnu_hash(name: &[u8]) -> u32 {
name.iter().fold(5381_u32, |value, &byte| {
value.wrapping_mul(33).wrapping_add(u32::from(byte))
})
}
pub(crate) fn build_sysv_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
if names.len() < 2 {
return invalid("dynamic symbol table is unexpectedly empty");
}
let bucket_count = names.len();
let symbol_count = names.len();
let mut buckets = vec![0_u32; bucket_count];
let mut chains = vec![0_u32; symbol_count];
for (symbol_index, name) in names.iter().enumerate().skip(1) {
let bucket_index = elf_hash(name) as usize % bucket_count;
let symbol_index32 = u32::try_from(symbol_index)
.map_err(|_| Error::Invalid("dynamic symbol index exceeds u32".to_owned()))?;
if buckets[bucket_index] == 0 {
buckets[bucket_index] = symbol_index32;
continue;
}
let mut chain_index = buckets[bucket_index] as usize;
while chains[chain_index] != 0 {
chain_index = chains[chain_index] as usize;
}
chains[chain_index] = symbol_index32;
}
let mut output = Vec::with_capacity((2 + bucket_count + symbol_count) * 4);
output.extend_from_slice(
&u32::try_from(bucket_count)
.map_err(|_| Error::Invalid("SysV bucket count exceeds u32".to_owned()))?
.to_le_bytes(),
);
output.extend_from_slice(
&u32::try_from(symbol_count)
.map_err(|_| Error::Invalid("SysV symbol count exceeds u32".to_owned()))?
.to_le_bytes(),
);
for value in buckets.into_iter().chain(chains) {
output.extend_from_slice(&value.to_le_bytes());
}
Ok(output)
}
pub(crate) fn build_gnu_hash(names: &[Vec<u8>]) -> Result<Vec<u8>> {
let hashes = names
.iter()
.skip(1)
.map(|name| gnu_hash(name))
.collect::<Vec<_>>();
if hashes.is_empty() {
return invalid("GNU hash requires at least one dynamic symbol");
}
let bloom_shift = 5_u32;
let mut bloom_word = 0_u64;
for &value in &hashes {
bloom_word |= 1_u64 << (value & 63);
bloom_word |= 1_u64 << ((value >> bloom_shift) & 63);
}
let mut chains = hashes
.into_iter()
.map(|value| value & !1)
.collect::<Vec<_>>();
let last = chains
.last_mut()
.ok_or_else(|| Error::Invalid("GNU hash chain is empty".to_owned()))?;
*last |= 1;
let mut output = Vec::with_capacity(28 + chains.len() * 4);
for value in [1_u32, 1, 1, bloom_shift] {
output.extend_from_slice(&value.to_le_bytes());
}
output.extend_from_slice(&bloom_word.to_le_bytes());
output.extend_from_slice(&1_u32.to_le_bytes());
for value in chains {
output.extend_from_slice(&value.to_le_bytes());
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_elf_hash_is_stable() {
assert_eq!(elf_hash(b"printf"), 0x0779_05a6);
assert_eq!(gnu_hash(b"printf"), 0x156b_2bb8);
}
}
-636
View File
@@ -1,636 +0,0 @@
use super::error::{Error, Result, invalid};
pub(crate) const SHT_NOBITS: u32 = 8;
pub(crate) const SHT_STRTAB: u32 = 3;
pub(crate) const SHT_LOUSER: u32 = 0x8000_0000;
pub(crate) const SHF_ALLOC: u64 = 2;
const PT_LOAD: u32 = 1;
pub(crate) const PF_R: u32 = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct LoadSegment {
pub offset: u64,
pub virtual_address: u64,
pub file_size: u64,
pub memory_size: u64,
pub flags: u32,
pub alignment: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SectionHeader {
pub name: u32,
pub section_type: u32,
pub flags: u64,
pub address: u64,
pub offset: u64,
pub size: u64,
pub link: u32,
pub info: u32,
pub alignment: u64,
pub entry_size: u64,
}
impl SectionHeader {
pub const SIZE: usize = 0x40;
fn parse(data: &[u8], offset: usize) -> Result<Self> {
Ok(Self {
name: read_u32(data, offset)?,
section_type: read_u32(data, offset + 4)?,
flags: read_u64(data, offset + 8)?,
address: read_u64(data, offset + 0x10)?,
offset: read_u64(data, offset + 0x18)?,
size: read_u64(data, offset + 0x20)?,
link: read_u32(data, offset + 0x28)?,
info: read_u32(data, offset + 0x2c)?,
alignment: read_u64(data, offset + 0x30)?,
entry_size: read_u64(data, offset + 0x38)?,
})
}
pub fn encode(self) -> [u8; Self::SIZE] {
let mut output = [0_u8; Self::SIZE];
output[0..4].copy_from_slice(&self.name.to_le_bytes());
output[4..8].copy_from_slice(&self.section_type.to_le_bytes());
output[8..0x10].copy_from_slice(&self.flags.to_le_bytes());
output[0x10..0x18].copy_from_slice(&self.address.to_le_bytes());
output[0x18..0x20].copy_from_slice(&self.offset.to_le_bytes());
output[0x20..0x28].copy_from_slice(&self.size.to_le_bytes());
output[0x28..0x2c].copy_from_slice(&self.link.to_le_bytes());
output[0x2c..0x30].copy_from_slice(&self.info.to_le_bytes());
output[0x30..0x38].copy_from_slice(&self.alignment.to_le_bytes());
output[0x38..0x40].copy_from_slice(&self.entry_size.to_le_bytes());
output
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ElfLayout {
pub entrypoint: u64,
pub program_header_offset: usize,
pub program_header_size: usize,
pub program_header_count: usize,
pub program_headers: Vec<LoadSegment>,
pub section_headers: Vec<SectionHeader>,
pub section_name_index: usize,
pub private_section_index: usize,
}
impl ElfLayout {
pub fn parse(data: &[u8], require_private: bool) -> Result<Self> {
let ident = slice(data, 0, 6)?;
if ident[..4] != *b"\x7fELF" || ident[4] != 2 || ident[5] != 1 {
return invalid("input is not a little-endian ELF64 file");
}
if read_u16(data, 0x12)? != 0xb7 {
return invalid("input is not an AArch64 ELF");
}
let entrypoint = read_u64(data, 0x18)?;
let program_header_offset = usize_from_u64(read_u64(data, 0x20)?, "program header offset")?;
let section_header_offset = usize_from_u64(read_u64(data, 0x28)?, "section header offset")?;
let program_header_size = usize::from(read_u16(data, 0x36)?);
let program_header_count = usize::from(read_u16(data, 0x38)?);
let section_header_size = usize::from(read_u16(data, 0x3a)?);
let section_header_count = usize::from(read_u16(data, 0x3c)?);
let section_name_index = usize::from(read_u16(data, 0x3e)?);
if program_header_size != 0x38 || section_header_size != SectionHeader::SIZE {
return invalid("unexpected ELF program/section header size");
}
let mut program_headers = Vec::new();
for index in 0..program_header_count {
let offset = checked_index(program_header_offset, index, program_header_size)?;
if read_u32(data, offset)? != PT_LOAD {
continue;
}
let segment = LoadSegment {
flags: read_u32(data, offset + 4)?,
offset: read_u64(data, offset + 8)?,
virtual_address: read_u64(data, offset + 0x10)?,
file_size: read_u64(data, offset + 0x20)?,
memory_size: read_u64(data, offset + 0x28)?,
alignment: read_u64(data, offset + 0x30)?,
};
let file_end = segment
.offset
.checked_add(segment.file_size)
.ok_or_else(|| Error::Invalid(format!("PT_LOAD {index} file range overflow")))?;
if file_end > data.len() as u64 {
return invalid(format!("PT_LOAD {index} exceeds input file"));
}
program_headers.push(segment);
}
if program_headers.is_empty() {
return invalid("input ELF contains no PT_LOAD segments");
}
let mut section_headers = Vec::with_capacity(section_header_count);
for index in 0..section_header_count {
let offset = checked_index(section_header_offset, index, section_header_size)?;
section_headers.push(SectionHeader::parse(data, offset)?);
}
if section_name_index >= section_headers.len() {
return invalid("ELF section-name index is out of range");
}
let private = section_headers
.iter()
.enumerate()
.filter_map(|(index, section)| (section.section_type == SHT_LOUSER).then_some(index))
.collect::<Vec<_>>();
let private_section_index = match private.as_slice() {
[index] => *index,
[] if !require_private => usize::MAX,
_ => {
return invalid(format!(
"expected {} SHT_LOUSER section, found {}",
if require_private {
"one"
} else {
"at most one"
},
private.len()
));
}
};
let layout = Self {
entrypoint,
program_header_offset,
program_header_size,
program_header_count,
program_headers,
section_headers,
section_name_index,
private_section_index,
};
// Section roles are resolved from the ELF's own string table. Validate
// it at the format boundary so callers cannot silently continue with
// fabricated or lossy section names.
layout.section_names(data)?;
Ok(layout)
}
pub fn private_section(&self) -> Result<SectionHeader> {
self.section_headers
.get(self.private_section_index)
.copied()
.ok_or_else(|| Error::Invalid("ELF has no private section".to_owned()))
}
pub fn load_end(&self) -> Result<u64> {
self.program_headers
.iter()
.map(|segment| {
segment
.virtual_address
.checked_add(segment.memory_size)
.ok_or_else(|| Error::Invalid("PT_LOAD memory end overflow".to_owned()))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.max()
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD memory range".to_owned()))
}
pub fn file_load_end(&self) -> Result<u64> {
self.program_headers
.iter()
.map(|segment| {
segment
.offset
.checked_add(segment.file_size)
.ok_or_else(|| Error::Invalid("PT_LOAD file end overflow".to_owned()))
})
.collect::<Result<Vec<_>>>()?
.into_iter()
.max()
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD file range".to_owned()))
}
pub fn load_alignment(&self) -> Result<u64> {
let alignment = self
.program_headers
.iter()
.map(|segment| segment.alignment)
.max()
.ok_or_else(|| Error::Invalid("ELF has no PT_LOAD alignment".to_owned()))?;
if alignment == 0 || !alignment.is_power_of_two() {
return invalid(format!("invalid PT_LOAD alignment 0x{alignment:x}"));
}
Ok(alignment)
}
pub fn append_load_segment(&self, output: &mut [u8], segment: LoadSegment) -> Result<Self> {
if self.program_header_size != 0x38 {
return invalid("unexpected ELF program header size");
}
if segment.file_size == 0 {
return invalid("new PT_LOAD has no file contents");
}
if segment.memory_size < segment.file_size {
return invalid("new PT_LOAD memory size is smaller than file size");
}
if segment.alignment == 0 || !segment.alignment.is_power_of_two() {
return invalid(format!(
"invalid new PT_LOAD alignment 0x{:x}",
segment.alignment
));
}
if segment.offset % segment.alignment != segment.virtual_address % segment.alignment {
return invalid("new PT_LOAD offset and address are misaligned");
}
let segment_file_end = segment
.offset
.checked_add(segment.file_size)
.ok_or_else(|| Error::Invalid("new PT_LOAD file range overflow".to_owned()))?;
let segment_memory_end = segment
.virtual_address
.checked_add(segment.memory_size)
.ok_or_else(|| Error::Invalid("new PT_LOAD memory range overflow".to_owned()))?;
if segment_file_end > output.len() as u64 {
return invalid("new PT_LOAD exceeds output mapping");
}
for existing in &self.program_headers {
let existing_file_end = existing
.offset
.checked_add(existing.file_size)
.ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?;
if segment.offset < existing_file_end && existing.offset < segment_file_end {
return invalid("new PT_LOAD overlaps an existing file range");
}
let existing_memory_end = existing
.virtual_address
.checked_add(existing.memory_size)
.ok_or_else(|| Error::Invalid("PT_LOAD memory range overflow".to_owned()))?;
if segment.virtual_address < existing_memory_end
&& existing.virtual_address < segment_memory_end
{
return invalid("new PT_LOAD overlaps an existing memory range");
}
}
let new_count = self
.program_header_count
.checked_add(1)
.ok_or_else(|| Error::Invalid("program header count overflow".to_owned()))?;
let new_count_u16 = u16::try_from(new_count)
.map_err(|_| Error::Invalid("program header count exceeds u16".to_owned()))?;
let header_offset = checked_index(
self.program_header_offset,
self.program_header_count,
self.program_header_size,
)?;
let header_end = header_offset
.checked_add(self.program_header_size)
.ok_or_else(|| Error::Invalid("new program header range overflow".to_owned()))?;
slice(output, header_offset, self.program_header_size)?;
let first_file_section = self
.section_headers
.iter()
.filter(|section| section.section_type != SHT_NOBITS && section.size != 0)
.map(|section| section.offset)
.min();
if first_file_section.is_some_and(|offset| header_end as u64 > offset) {
return invalid("no space for an additional program header");
}
let mut header = [0_u8; 0x38];
header[0..4].copy_from_slice(&PT_LOAD.to_le_bytes());
header[4..8].copy_from_slice(&segment.flags.to_le_bytes());
header[8..0x10].copy_from_slice(&segment.offset.to_le_bytes());
header[0x10..0x18].copy_from_slice(&segment.virtual_address.to_le_bytes());
header[0x18..0x20].copy_from_slice(&segment.virtual_address.to_le_bytes());
header[0x20..0x28].copy_from_slice(&segment.file_size.to_le_bytes());
header[0x28..0x30].copy_from_slice(&segment.memory_size.to_le_bytes());
header[0x30..0x38].copy_from_slice(&segment.alignment.to_le_bytes());
output
.get_mut(header_offset..header_end)
.ok_or_else(|| Error::Invalid("new program header exceeds output".to_owned()))?
.copy_from_slice(&header);
output
.get_mut(0x38..0x3a)
.ok_or_else(|| Error::Invalid("ELF header is truncated".to_owned()))?
.copy_from_slice(&new_count_u16.to_le_bytes());
let mut updated = self.clone();
updated.program_header_count = new_count;
updated.program_headers.push(segment);
Ok(updated)
}
/// Resolve every section's name from the ELF `shstrtab` section.
///
/// The returned names are source data, not role labels supplied by the
/// caller. Any malformed string-table reference is an input error.
pub fn section_names(&self, data: &[u8]) -> Result<Vec<String>> {
let table = self
.section_headers
.get(self.section_name_index)
.copied()
.ok_or_else(|| Error::Invalid("ELF section-name index is out of range".to_owned()))?;
if table.section_type != SHT_STRTAB {
return invalid(format!(
"ELF section-name table has unexpected type 0x{:x}",
table.section_type
));
}
let strings = slice_u64(data, table.offset, table.size)?;
if strings.is_empty() || strings[0] != 0 {
return invalid("ELF section-name table does not start with NUL");
}
if strings.last().copied() != Some(0) {
return invalid("ELF section-name table is not NUL terminated");
}
self.section_headers
.iter()
.enumerate()
.map(|(index, section)| {
let offset = section.name as usize;
if offset >= strings.len() {
return invalid(format!(
"ELF section {index} name offset 0x{offset:x} exceeds section-name table"
));
}
let end = strings[offset..]
.iter()
.position(|&byte| byte == 0)
.map(|length| offset + length)
.ok_or_else(|| {
Error::Invalid(format!(
"ELF section {index} name at 0x{offset:x} is unterminated"
))
})?;
let name = std::str::from_utf8(&strings[offset..end]).map_err(|error| {
Error::Invalid(format!(
"ELF section {index} name at 0x{offset:x} is not UTF-8: {error}"
))
})?;
if index == 0 && section.name != 0 {
return invalid("ELF null section has a nonzero name offset");
}
Ok(name.to_owned())
})
.collect()
}
pub fn file_offset_to_virtual_address(&self, offset: u64, size: u64) -> Result<u64> {
let end = offset
.checked_add(size)
.ok_or_else(|| Error::Invalid("file range overflow".to_owned()))?;
for segment in &self.program_headers {
let segment_end = segment
.offset
.checked_add(segment.file_size)
.ok_or_else(|| Error::Invalid("PT_LOAD file range overflow".to_owned()))?;
if segment.offset <= offset && end <= segment_end {
return segment
.virtual_address
.checked_add(offset - segment.offset)
.ok_or_else(|| Error::Invalid("virtual address overflow".to_owned()));
}
}
invalid(format!(
"file range 0x{offset:x}..0x{end:x} is not in PT_LOAD"
))
}
}
pub(crate) fn slice(data: &[u8], offset: usize, size: usize) -> Result<&[u8]> {
let end = offset
.checked_add(size)
.ok_or_else(|| Error::Invalid("byte range overflow".to_owned()))?;
data.get(offset..end).ok_or_else(|| {
Error::Invalid(format!(
"byte range 0x{offset:x}..0x{end:x} is out of bounds"
))
})
}
pub(crate) fn slice_u64(data: &[u8], offset: u64, size: u64) -> Result<&[u8]> {
slice(
data,
usize_from_u64(offset, "file offset")?,
usize_from_u64(size, "file size")?,
)
}
pub(crate) fn read_u16(data: &[u8], offset: usize) -> Result<u16> {
let bytes: [u8; 2] = slice(data, offset, 2)?
.try_into()
.map_err(|_| Error::Invalid("invalid u16 range".to_owned()))?;
Ok(u16::from_le_bytes(bytes))
}
pub(crate) fn read_u32(data: &[u8], offset: usize) -> Result<u32> {
let bytes: [u8; 4] = slice(data, offset, 4)?
.try_into()
.map_err(|_| Error::Invalid("invalid u32 range".to_owned()))?;
Ok(u32::from_le_bytes(bytes))
}
pub(crate) fn read_u64(data: &[u8], offset: usize) -> Result<u64> {
let bytes: [u8; 8] = slice(data, offset, 8)?
.try_into()
.map_err(|_| Error::Invalid("invalid u64 range".to_owned()))?;
Ok(u64::from_le_bytes(bytes))
}
pub(crate) fn read_i64(data: &[u8], offset: usize) -> Result<i64> {
let bytes: [u8; 8] = slice(data, offset, 8)?
.try_into()
.map_err(|_| Error::Invalid("invalid i64 range".to_owned()))?;
Ok(i64::from_le_bytes(bytes))
}
pub(crate) fn usize_from_u64(value: u64, field: &str) -> Result<usize> {
usize::try_from(value).map_err(|_| Error::Invalid(format!("{field} 0x{value:x} exceeds usize")))
}
pub(crate) fn checked_index(base: usize, index: usize, stride: usize) -> Result<usize> {
index
.checked_mul(stride)
.and_then(|value| base.checked_add(value))
.ok_or_else(|| Error::Invalid("table index overflow".to_owned()))
}
pub(crate) fn align_up(value: u64, alignment: u64) -> Result<u64> {
if alignment == 0 || !alignment.is_power_of_two() {
return invalid(format!("invalid alignment {alignment}"));
}
value
.checked_add(alignment - 1)
.map(|aligned| aligned & !(alignment - 1))
.ok_or_else(|| Error::Invalid("alignment overflow".to_owned()))
}
#[cfg(test)]
mod tests {
use super::*;
fn layout(name_index: u32) -> ElfLayout {
ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 0,
program_headers: Vec::new(),
section_headers: vec![
SectionHeader {
name: 0,
section_type: 0,
flags: 0,
address: 0,
offset: 0,
size: 0,
link: 0,
info: 0,
alignment: 0,
entry_size: 0,
},
SectionHeader {
name: name_index,
section_type: 1,
flags: 0,
address: 0,
offset: 0,
size: 0,
link: 0,
info: 0,
alignment: 0,
entry_size: 0,
},
SectionHeader {
name: 1,
section_type: SHT_STRTAB,
flags: 0,
address: 0,
offset: 0,
size: 8,
link: 0,
info: 0,
alignment: 1,
entry_size: 0,
},
],
section_name_index: 2,
private_section_index: usize::MAX,
}
}
#[test]
fn section_names_resolve_from_elf_string_table() {
let names = layout(1)
.section_names(b"\0text\0\0\0")
.expect("valid names");
assert_eq!(names, ["", "text", "text"]);
}
#[test]
fn section_names_reject_out_of_range_name_offsets() {
let error = layout(8)
.section_names(b"\0text\0\0\0")
.expect_err("invalid offset");
assert!(error.to_string().contains("exceeds section-name table"));
}
#[test]
fn section_names_reject_invalid_utf8() {
let mut elf_layout = layout(1);
elf_layout.section_headers[1].name = 1;
let error = elf_layout
.section_names(b"\0\xff\0\0\0\0\0\0")
.expect_err("invalid UTF-8");
assert!(error.to_string().contains("is not UTF-8"));
}
#[test]
fn section_names_reject_non_string_table() {
let mut elf_layout = layout(1);
elf_layout.section_headers[2].section_type = 1;
let error = elf_layout
.section_names(b"\0text\0\0\0")
.expect_err("wrong section type");
assert!(error.to_string().contains("unexpected type"));
}
#[test]
fn section_names_reject_unterminated_table() {
let elf_layout = layout(1);
let error = elf_layout
.section_names(b"\0text\0\x01\x01")
.expect_err("unterminated table");
assert!(error.to_string().contains("not NUL terminated"));
}
#[test]
fn append_load_segment_updates_program_headers() {
let elf_layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 0,
program_headers: Vec::new(),
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: usize::MAX,
};
let mut output = vec![0_u8; 0x2000];
let updated = elf_layout
.append_load_segment(
&mut output,
LoadSegment {
offset: 0x1000,
virtual_address: 0x2000,
file_size: 0x20,
memory_size: 0x20,
flags: PF_R,
alignment: 0x1000,
},
)
.expect("append segment");
assert_eq!(updated.program_header_count, 1);
assert_eq!(updated.program_headers[0].virtual_address, 0x2000);
assert_eq!(&output[0..4], &PT_LOAD.to_le_bytes());
assert_eq!(&output[0x38..0x3a], &1_u16.to_le_bytes());
}
#[test]
fn append_load_segment_rejects_program_header_overlap() {
let mut elf_layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 0,
program_headers: Vec::new(),
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: usize::MAX,
};
elf_layout.section_headers.push(SectionHeader {
name: 0,
section_type: 1,
flags: 0,
address: 0,
offset: 0x20,
size: 1,
link: 0,
info: 0,
alignment: 1,
entry_size: 0,
});
let mut output = vec![0_u8; 0x100];
let error = elf_layout
.append_load_segment(
&mut output,
LoadSegment {
offset: 0x80,
virtual_address: 0x1080,
file_size: 0x20,
memory_size: 0x20,
flags: PF_R,
alignment: 0x1000,
},
)
.expect_err("overlapping program header");
assert!(error.to_string().contains("additional program header"));
}
}
-2
View File
@@ -1,7 +1,5 @@
mod artifact;
mod error;
mod hash;
mod layout;
mod pipeline;
pub use error::Error;
+16 -72
View File
@@ -12,35 +12,19 @@ use serde::Serialize;
use sha2::{Digest, Sha256};
use tempfile::NamedTempFile;
use super::super::common;
use super::artifact::load_artifacts;
use super::error::{Error, Result, invalid};
use super::hash::{build_gnu_hash, build_sysv_hash};
use super::layout::{
ElfLayout, LoadSegment, PF_R, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, align_up,
read_i64, read_u32, read_u64, slice, slice_u64, usize_from_u64,
use senbei_elf::{
DT_GNU_HASH, DT_HASH, DT_JMPREL, DT_PLTRELSZ, DT_RELA, DT_RELACOUNT, DT_RELASZ, DT_STRSZ,
DT_STRTAB, DT_SYMTAB, DT_VERNEED, DT_VERSYM, ELF64_RELA_SIZE, ELF64_SYMBOL_SIZE, ElfLayout,
LoadSegment, PF_R, R_AARCH64_ABS64, R_AARCH64_GLOB_DAT, R_AARCH64_JUMP_SLOT,
R_AARCH64_RELATIVE, SHF_ALLOC, SHT_LOUSER, SHT_NOBITS, SectionHeader, VER_NDX_GLOBAL, align_up,
build_gnu_hash, build_sysv_hash, read_i64, read_u32, read_u64, slice, slice_u64,
usize_from_u64,
};
const CHUNK_SIZE: usize = 16 * 1024 * 1024;
const ELF64_SYMBOL_SIZE: usize = 0x18;
const ELF64_RELA_SIZE: usize = 0x18;
const R_AARCH64_ABS64: u32 = 0x101;
const R_AARCH64_GLOB_DAT: u32 = 0x401;
const R_AARCH64_JUMP_SLOT: u32 = 0x402;
const R_AARCH64_RELATIVE: u32 = 0x403;
const VER_NDX_GLOBAL: u16 = 1;
const DT_PLTRELSZ: u64 = 2;
const DT_HASH: u64 = 4;
const DT_STRTAB: u64 = 5;
const DT_SYMTAB: u64 = 6;
const DT_RELA: u64 = 7;
const DT_RELASZ: u64 = 8;
const DT_STRSZ: u64 = 10;
const DT_JMPREL: u64 = 23;
const DT_GNU_HASH: u64 = 0x6fff_fef5;
const DT_VERSYM: u64 = 0x6fff_fff0;
const DT_RELACOUNT: u64 = 0x6fff_fff9;
const DT_VERNEED: u64 = 0x6fff_fffe;
/// Inputs and optional diagnostics for one `libil2cpp.so` restoration.
#[derive(Debug, Clone)]
@@ -187,9 +171,7 @@ fn read_file(path: &Path) -> Result<Vec<u8>> {
}
fn sha256_bytes(data: &[u8]) -> String {
let mut digest = Sha256::new();
digest.update(data);
hex_digest(&digest.finalize())
common::sha256(data)
}
fn sha256_file(path: &Path) -> Result<String> {
@@ -205,7 +187,7 @@ fn sha256_file(path: &Path) -> Result<String> {
}
digest.update(&buffer[..read]);
}
Ok(hex_digest(&digest.finalize()))
Ok(senbei_crypto::hex_digest(&digest.finalize()))
}
fn copy_range(source: &[u8], output: &mut File, size: usize, path: &Path) -> Result<()> {
@@ -286,7 +268,7 @@ impl FileLayoutWriter<'_> {
"decoded write 0x{virtual_address:x}..0x{end:x} is not covered by PT_LOAD memory"
));
}
usize_from_u64(written, "written byte count")
Ok(usize_from_u64(written, "written byte count")?)
}
}
@@ -772,18 +754,8 @@ fn dynamic_contains_tag(output: &[u8], dynamic: SectionHeader, wanted: u64) -> R
}
fn required_section_indices(names: &[String]) -> Result<HashMap<&'static str, usize>> {
const REQUIRED: [&str; 8] = [
".dynsym",
".gnu.version",
".gnu.version_r",
".gnu.hash",
".dynstr",
".rela.dyn",
".rela.plt",
".dynamic",
];
let mut result = HashMap::with_capacity(REQUIRED.len());
for required in REQUIRED {
let mut result = HashMap::with_capacity(senbei_elf::DYNAMIC_SECTION_NAMES.len());
for required in senbei_elf::DYNAMIC_SECTION_NAMES {
let indices = names
.iter()
.enumerate()
@@ -952,7 +924,7 @@ fn metadata_mapping_length(
let end = extension_start
.checked_add(cursor)
.ok_or_else(|| Error::Invalid("dynamic-table mapping end overflow".to_owned()))?;
usize_from_u64(end, "dynamic-table mapping length")
Ok(usize_from_u64(end, "dynamic-table mapping length")?)
}
fn table_placements(
@@ -1467,29 +1439,11 @@ fn validate_restored_binary(
}
fn absolute(path: &Path) -> Result<PathBuf> {
if path.is_absolute() {
Ok(path.to_path_buf())
} else {
std::env::current_dir()
.map(|current| current.join(path))
.map_err(|error| Error::io("query current directory", path, error))
}
common::absolute(path).map_err(|error| Error::io("query current directory", path, error))
}
fn write_atomic(path: &Path, data: &[u8]) -> Result<()> {
let parent = path.parent().unwrap_or_else(|| Path::new("."));
std::fs::create_dir_all(parent)
.map_err(|error| Error::io("create output directory", parent, error))?;
let mut temporary = NamedTempFile::new_in(parent)
.map_err(|error| Error::io("create temporary file", parent, error))?;
temporary
.write_all(data)
.and_then(|_| temporary.as_file().sync_all())
.map_err(|error| Error::io("write temporary file", temporary.path(), error))?;
temporary
.persist(path)
.map_err(|error| Error::io("replace output", path, error.error))?;
Ok(())
common::write_atomic(path, data).map_err(|error| Error::io("write temporary file", path, error))
}
/// Restore the current protected `libil2cpp.so` without executing protector code.
@@ -1723,16 +1677,6 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
elapsed_seconds: started.elapsed().as_secs_f64(),
})
}
/// Lowercase hex of a digest output (sha2 0.11's `Array` no longer formats as
/// hex directly).
fn hex_digest(data: &[u8]) -> String {
let mut out = String::with_capacity(data.len() * 2);
for byte in data {
out.push_str(&format!("{byte:02x}"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
+9 -1
View File
@@ -10,5 +10,13 @@ pub use windows::{
/// Deterministic worker-thread cap shared by filesystem scanning and engines.
pub fn thread_cap() -> usize {
windows::thread_cap()
if let Ok(value) = std::env::var("SENBEI_THREADS")
&& let Ok(count) = value.trim().parse::<usize>()
&& count >= 1
{
return count;
}
std::thread::available_parallelism()
.map(|count| count.get())
.unwrap_or(1)
}
+5 -16
View File
@@ -15,23 +15,12 @@ pub fn unpack(input: &[u8]) -> Result<Vec<u8>, UnpackError> {
/// Used by the new-layout managed (CLR) metadata restore to locate the COR20
/// header and BSJB MetaData stream in the original protected file.
fn prot_rva_to_off(file_data: &[u8], pe_header: u32, rva: u32) -> Option<u32> {
let nsec = get_u16(file_data, pe_header + 6) as u32;
let opt = get_u16(file_data, pe_header + 20) as u32;
let tab = pe_header + 24 + opt;
for i in 0..nsec {
let s = tab + i * 40;
if (s as usize + 24) > file_data.len() {
return None;
}
let va = get_u32(file_data, s + 12);
let vs = get_u32(file_data, s + 8);
let rsz = get_u32(file_data, s + 16);
let rp = get_u32(file_data, s + 20);
if va <= rva && rva < va + vs.max(rsz) {
return Some(rp + (rva - va));
}
let headers = senbei_pe::parse(file_data).ok()?;
if headers.pe_offset != pe_header as usize {
return None;
}
None
let offset = senbei_pe::rva_to_offset(file_data, headers, rva).ok()?;
u32::try_from(offset).ok()
}
pub fn unpack_v(input: &[u8], verbose: bool) -> Result<Vec<u8>, UnpackError> {
+54 -78
View File
@@ -42,47 +42,26 @@ fn rd_u32(d: &[u8], off: u32) -> Option<u32> {
.map(|s| u32::from_le_bytes([s[0], s[1], s[2], s[3]]))
}
/// A parsed section-table entry (only the fields we translate against).
struct Section {
va: u32,
vsize: u32,
raw_ptr: u32,
raw_size: u32,
chars: u32,
}
/// PE format section data used by the integrity policy.
type Section = senbei_pe::Section;
/// Walk the output's own section table and translate an RVA to a file offset.
/// Works for both memory-image output (raw_ptr == va) and compacted disk
/// output (real raw pointers), because it consults whatever the output declares.
/// Returns the offset only if the translated range `[off, off+need)` lies inside
/// the file.
fn rva_to_off(secs: &[Section], file_len: usize, rva: u32, need: u32) -> Option<u32> {
for s in secs {
// The mapped span is the larger of virtual and raw size, so an RVA that
// falls in the virtual tail of a section still resolves.
let span = s.vsize.max(s.raw_size);
if span == 0 {
continue;
}
if rva >= s.va && rva < s.va.wrapping_add(span) {
let delta = rva - s.va;
let off = s.raw_ptr.checked_add(delta)?;
let end = off.checked_add(need)?;
if (end as usize) <= file_len {
return Some(off);
}
return None;
}
}
None
fn rva_to_off(data: &[u8], headers: senbei_pe::Headers, rva: u32, need: u32) -> Option<u32> {
let offset = u32::try_from(senbei_pe::rva_to_offset(data, headers, rva).ok()?).ok()?;
let end = offset.checked_add(need)?;
(usize::try_from(end).ok()? <= data.len()).then_some(offset)
}
fn is_executable_rva(secs: &[Section], rva: u32) -> bool {
secs.iter().any(|section| {
let span = section.vsize.max(section.raw_size);
rva >= section.va
&& rva < section.va.wrapping_add(span)
&& (section.chars & 0x2000_0000) != 0
let span = section.virtual_size.max(section.raw_size);
rva >= section.virtual_address
&& rva < section.virtual_address.wrapping_add(span)
&& (section.characteristics & 0x2000_0000) != 0
})
}
@@ -151,7 +130,6 @@ pub fn check(out: &[u8]) -> IntegrityReport {
return r;
}
};
let opt_hdr_size = rd_u16(out, pe_off.wrapping_add(20)).unwrap_or(0) as u32;
let opt = pe_off.wrapping_add(24);
let magic = match rd_u16(out, opt) {
Some(v) => v,
@@ -181,42 +159,38 @@ pub fn check(out: &[u8]) -> IntegrityReport {
}
// --- Section table ------------------------------------------------------
let sec_table = opt.wrapping_add(opt_hdr_size);
let mut secs: Vec<Section> = Vec::new();
for i in 0..num_sections {
let base = sec_table.wrapping_add(i * 40);
// If the table runs past EOF the image is structurally broken.
let (vsize, va, raw_size, raw_ptr, chars) = match (
rd_u32(out, base.wrapping_add(8)),
rd_u32(out, base.wrapping_add(12)),
rd_u32(out, base.wrapping_add(16)),
rd_u32(out, base.wrapping_add(20)),
rd_u32(out, base.wrapping_add(36)),
) {
(Some(a), Some(b), Some(c), Some(d), Some(e)) => (a, b, c, d, e),
_ => {
r.issues
.push("section table extends past end of file".into());
return r;
}
};
// Raw data must lie within the file for compacted (disk-layout) output.
if raw_size != 0 {
let end = raw_ptr.wrapping_add(raw_size) as usize;
if end > file_len {
r.issues.push(format!(
"section #{i} raw data [0x{raw_ptr:X}..0x{end:X}] exceeds file size 0x{file_len:X}"
));
}
let headers = match senbei_pe::parse(out) {
Ok(headers) => headers,
Err(_) => {
r.issues
.push("section table extends past end of file".into());
return r;
}
secs.push(Section {
va,
vsize,
raw_ptr,
raw_size,
chars,
});
}
};
let parsed_sections = match senbei_pe::sections(out, headers) {
Ok(sections) => sections,
Err(_) => {
r.issues
.push("section table extends past end of file".into());
return r;
}
};
let secs: Vec<Section> = parsed_sections
.into_iter()
.enumerate()
.map(|(i, section)| {
if section.raw_size != 0 {
let end = section.raw_offset.wrapping_add(section.raw_size) as usize;
if end > file_len {
r.issues.push(format!(
"section #{i} raw data [0x{:X}..0x{end:X}] exceeds file size 0x{file_len:X}",
section.raw_offset
));
}
}
section
})
.collect();
// --- Managed (CLR) detection ------------------------------------------
// The COR20 (CLR) data directory, when present and non-zero, marks a managed
@@ -266,7 +240,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
r.issues.push("entry point RVA is zero".into());
}
} else if !is_managed {
match rva_to_off(&secs, file_len, ep, 16) {
match rva_to_off(out, headers, ep, 16) {
None => {
r.issues.push(format!(
"entry point RVA 0x{ep:X} does not map into any section"
@@ -288,8 +262,10 @@ pub fn check(out: &[u8]) -> IntegrityReport {
}
// The entry must live in an executable section.
let exec = secs.iter().any(|s| {
let span = s.vsize.max(s.raw_size);
ep >= s.va && ep < s.va.wrapping_add(span) && (s.chars & 0x2000_0000) != 0
let span = s.virtual_size.max(s.raw_size);
ep >= s.virtual_address
&& ep < s.virtual_address.wrapping_add(span)
&& (s.characteristics & 0x2000_0000) != 0
});
if !exec {
r.issues.push(format!(
@@ -316,7 +292,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
if !is_managed {
let imp_rva = rd_u32(out, dd_base.wrapping_add(8)).unwrap_or(0);
if imp_rva != 0 {
match rva_to_off(&secs, file_len, imp_rva, 20) {
match rva_to_off(out, headers, imp_rva, 20) {
None => r.issues.push(format!(
"import directory RVA 0x{imp_rva:X} does not map into any section"
)),
@@ -331,7 +307,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
if name_rva == 0 {
break;
}
match rva_to_off(&secs, file_len, name_rva, 1) {
match rva_to_off(out, headers, name_rva, 1) {
None => r.issues.push(format!(
"import descriptor {i} DLL name RVA 0x{name_rva:X} does not map into any section"
)),
@@ -359,7 +335,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
// still refuses to load. Validate: COR20 cb == 0x48, and the MetaData stream
// begins with the "BSJB" signature.
if is_managed {
match rva_to_off(&secs, file_len, clr_rva, 0x48) {
match rva_to_off(out, headers, clr_rva, 0x48) {
None => r.issues.push(format!(
"CLR (COR20) directory RVA 0x{clr_rva:X} does not map into any section"
)),
@@ -373,7 +349,7 @@ pub fn check(out: &[u8]) -> IntegrityReport {
// MetaData RVA/size live at COR20 + 0x08 / + 0x0C.
let md_rva = rd_u32(out, coff.wrapping_add(8)).unwrap_or(0);
if md_rva != 0 {
match rva_to_off(&secs, file_len, md_rva, 4) {
match rva_to_off(out, headers, md_rva, 4) {
None => r.issues.push(format!(
"CLR MetaData RVA 0x{md_rva:X} does not map into any section"
)),
@@ -416,11 +392,11 @@ mod tests {
fn executable_text() -> Vec<Section> {
vec![Section {
va: 0x1000,
vsize: 0x4000,
raw_ptr: 0x1000,
virtual_address: 0x1000,
virtual_size: 0x4000,
raw_offset: 0x1000,
raw_size: 0x4000,
chars: 0x6000_0020,
characteristics: 0x6000_0020,
}]
}
+1 -1
View File
@@ -11,11 +11,11 @@ use senbei_crypto::primitives;
use std::cell::RefCell;
use std::sync::{Arc, Mutex};
pub use crate::thread_cap;
pub use dll::{unpack_dll, unpack_dll_v};
pub use error::*;
pub use exe::{unpack as unpack_exe, unpack_v as unpack_exe_v};
pub use integrity::{IntegrityReport, check as check_integrity};
pub use parallel::thread_cap;
/// Maximum plausible PE `SizeOfImage` we are willing to allocate a zero buffer
/// for. Guards against a corrupt/crafted header requesting a multi-gigabyte
+1 -15
View File
@@ -19,20 +19,6 @@
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
/// Worker-thread cap. `SENBEI_THREADS` overrides it (`1` forces the sequential
/// path); otherwise the host's available parallelism; otherwise 1.
pub fn thread_cap() -> usize {
if let Ok(v) = std::env::var("SENBEI_THREADS")
&& let Ok(n) = v.trim().parse::<usize>()
&& n >= 1
{
return n;
}
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
}
/// Run `f(i, span_base, span)` for every block `i`, fanning out across worker
/// threads when the spans are disjoint and worthwhile, else sequentially.
///
@@ -102,7 +88,7 @@ where
}
}
let cap = thread_cap();
let cap = crate::thread_cap();
let per = min_per_thread.max(1);
let workers = if cap > 1 && n >= per.saturating_mul(2) {
cap.min(n / per)