2 Commits
Author SHA1 Message Date
bfloat16 366a19c2a1 fix(android): adjust the parsing of construction tables 2026-10-26 19:05:37 +08:00
bfloat16 ba1dacc0b2 fix(android): fix seed references 2026-10-25 20:46:41 +08:00
5 changed files with 1055 additions and 167 deletions

No files matched your search

@@ -16,6 +16,7 @@ fn wanted_id(command_id: u32) -> bool {
pub(crate) struct Artifact {
pub path: PathBuf,
pub size: u64,
pub entry_offset: Option<u32>,
}
pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact>> {
@@ -47,6 +48,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
Artifact {
path: root.join(path),
size,
entry_offset: item
.get("entry_offset")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
},
);
}
@@ -79,6 +84,10 @@ pub(crate) fn load_artifacts(index_path: &Path) -> Result<BTreeMap<u32, Artifact
Artifact {
path: root.join(path),
size,
entry_offset: record
.get("entry_offset")
.and_then(Value::as_u64)
.and_then(|value| u32::try_from(value).ok()),
},
);
}
@@ -0,0 +1,418 @@
//! Evaluate the callback selector reached from the handoff module entry.
//!
//! This is a bounded interpreter for the selector's integer/control-flow
//! instructions, not a native module loader. Only stack writes are permitted;
//! image calls are recorded, never executed. Unknown instructions or memory
//! accesses reject a candidate. Table offsets, ordering, flags and the group
//! fallback decision consequently come from the module's own code.
use std::collections::{BTreeSet, HashSet};
use senbei_elf::{ElfLayout, read_u32, slice_u64};
use super::error::{Result, invalid};
use super::pipeline::{aarch64_adrp_target, virtual_address_to_file_offset};
const RUNTIME: u64 = 1 << 32;
const IMAGE: u64 = 2 << 32;
const STACK: u64 = 3 << 32;
const STACK_SIZE: usize = 0x10000;
const RETURN: u64 = u64::MAX - 3;
#[cfg(test)]
#[path = "constructors_tests.rs"]
mod tests;
fn signed(value: u32, bits: u32) -> i64 {
i64::from((value << (32 - bits)) as i32 >> (32 - bits))
}
fn branch_target(pc: u64, instruction: u32, bits: u32, shift: u32) -> Option<u64> {
pc.checked_add_signed(signed((instruction >> shift) & ((1 << bits) - 1), bits) * 4)
}
/// Follow direct control flow from the descriptor entry, without inspecting
/// unrelated code or searching the data image for plausible tables.
fn selector_functions(module: &[u8], entry: u32) -> Result<BTreeSet<u64>> {
let mut functions = vec![u64::from(entry)];
let mut visited_functions = HashSet::new();
let mut selectors = BTreeSet::new();
let mut budget = 100_000_usize;
while let Some(function) = functions.pop() {
if !visited_functions.insert(function) {
continue;
}
let mut pending = vec![function];
let mut visited = HashSet::new();
while let Some(pc) = pending.pop() {
if !visited.insert(pc) {
continue;
}
if budget == 0 {
return invalid("constructor module control flow exceeds analysis budget");
}
budget -= 1;
let Some(instruction) = usize::try_from(pc)
.ok()
.filter(|offset| offset.is_multiple_of(4))
.and_then(|offset| read_u32(module, offset).ok())
else {
return invalid("constructor module branches outside its image");
};
if instruction & 0x7c00_0000 == 0x1400_0000 {
let Some(target) = branch_target(pc, instruction, 26, 0) else {
return invalid("constructor module branch address overflows");
};
if instruction & 0x8000_0000 != 0 {
functions.push(target);
pending.push(pc + 4);
} else {
pending.push(target);
}
continue;
}
let conditional_bits = if instruction & 0xff00_0010 == 0x5400_0000
|| instruction & 0x7e00_0000 == 0x3400_0000
{
Some(19)
} else if instruction & 0x7e00_0000 == 0x3600_0000 {
Some(14)
} else {
None
};
if let Some(bits) = conditional_bits {
let Some(target) = branch_target(pc, instruction, bits, 5) else {
return invalid("constructor module conditional branch overflows");
};
pending.push(target);
}
if instruction & 0xffff_fc1f == 0xd63f_0000 {
selectors.insert(function);
} else if instruction & 0xffff_fc1f == 0xd61f_0000
|| instruction & 0xffff_fc1f == 0xd65f_0000
|| instruction == 0
{
continue;
}
pending.push(pc + 4);
}
}
Ok(selectors)
}
fn mask(bits: u32) -> u64 {
u64::MAX >> (64 - bits)
}
fn logical_immediate(instruction: u32, width: u32) -> Option<u64> {
let n = (instruction >> 22) & 1;
let imms = (instruction >> 10) & 63;
let immr = (instruction >> 16) & 63;
let encoded = (n << 6) | (!imms & 63);
let len = 31_u32.checked_sub(encoded.leading_zeros())?;
if len < 1 || (width == 32 && n != 0) {
return None;
}
let size = 1 << len;
let ones = (imms & (size - 1)) + 1;
if ones == size || size > width {
return None;
}
let rotate = immr & (size - 1);
let value = mask(ones);
let element = if rotate == 0 {
value
} else {
((value >> rotate) | (value << (size - rotate))) & mask(size)
};
let mut result = 0;
for position in (0..width).step_by(size as usize) {
result |= element << position;
}
Some(result)
}
struct Selector<'a> {
module: &'a [u8],
image: &'a [u8],
layout: &'a ElfLayout,
registers: [Option<u64>; 31],
sp: u64,
stack: Vec<Option<u8>>,
flags: Option<(bool, bool, bool, bool)>,
read_image_base: bool,
calls: Vec<u64>,
}
impl Selector<'_> {
fn reg(&self, index: u32, sp: bool) -> Option<u64> {
if index == 31 {
Some(if sp { self.sp } else { 0 })
} else {
self.registers[index as usize]
}
}
fn set(&mut self, index: u32, value: u64, width: u32, sp: bool) {
let value = value & mask(width);
if index != 31 {
self.registers[index as usize] = Some(value);
} else if sp {
self.sp = value;
}
}
fn read(&mut self, address: u64, size: usize) -> Option<u64> {
if address == RUNTIME + 8 && size == 8 {
self.read_image_base = true;
return Some(IMAGE);
}
let bytes = if address >= STACK {
let start = usize::try_from(address - STACK).ok()?;
self.stack
.get(start..start.checked_add(size)?)?
.iter()
.copied()
.collect::<Option<Vec<_>>>()?
} else if address >= IMAGE {
let offset = virtual_address_to_file_offset(self.layout, address - IMAGE, size as u64)?;
self.image.get(offset..offset.checked_add(size)?)?.to_vec()
} else {
slice_u64(self.module, address, size as u64).ok()?.to_vec()
};
Some(bytes.iter().enumerate().fold(0, |value, (index, byte)| {
value | (u64::from(*byte) << (index * 8))
}))
}
fn write(&mut self, address: u64, size: usize, value: u64) -> Option<()> {
let offset = usize::try_from(address.checked_sub(STACK)?).ok()?;
for (index, byte) in self
.stack
.get_mut(offset..offset.checked_add(size)?)?
.iter_mut()
.enumerate()
{
*byte = Some((value >> (index * 8)) as u8);
}
Some(())
}
fn condition(&self, condition: u32) -> Option<bool> {
let (negative, zero, carry, overflow) = self.flags?;
let matched = match condition >> 1 {
0 => zero,
1 => carry,
2 => negative,
3 => overflow,
4 => carry && !zero,
5 => negative == overflow,
6 => negative == overflow && !zero,
7 => return Some(true),
_ => return None,
};
Some(matched ^ (condition & 1 != 0))
}
fn run(mut self, start: u64, max_calls: usize) -> Option<Vec<u64>> {
let mut pc = start;
let budget = max_calls
.checked_add(1)?
.checked_mul(256)?
.checked_add(1024)?
.min(1_000_000);
for _ in 0..budget {
if !pc.is_multiple_of(4) {
return None;
}
let instruction = read_u32(self.module, usize::try_from(pc).ok()?).ok()?;
let mut next = pc.checked_add(4)?;
let rd = instruction & 31;
let rn = (instruction >> 5) & 31;
let width = if instruction >> 31 == 0 { 32 } else { 64 };
if instruction & 0xffff_fc1f == 0xd65f_0000 {
return (self.reg(rn, false)? == RETURN
&& self.sp == STACK + STACK_SIZE as u64
&& self.read_image_base
&& !self.calls.is_empty()
&& self.reg(0, false)? == 1)
.then_some(self.calls);
} else if instruction & 0xffff_fc1f == 0xd63f_0000 {
let address = self.reg(rn, false)?.checked_sub(IMAGE)?;
if self.calls.len() >= max_calls || !executable_address(self.layout, address) {
return None;
}
self.calls.push(address);
self.registers[..19].fill(None);
self.registers[30] = Some(next);
self.flags = None;
} else if instruction & 0xfc00_0000 == 0x1400_0000 {
next = branch_target(pc, instruction, 26, 0)?;
} else if instruction & 0xff00_0010 == 0x5400_0000 {
if self.condition(instruction & 15)? {
next = branch_target(pc, instruction, 19, 5)?;
}
} else if instruction & 0x7e00_0000 == 0x3400_0000 {
let nonzero = self.reg(rd, false)? & mask(width) != 0;
if nonzero == (instruction & (1 << 24) != 0) {
next = branch_target(pc, instruction, 19, 5)?;
}
} else if instruction & 0x9f00_0000 == 0x9000_0000 {
self.set(rd, aarch64_adrp_target(instruction, pc)?, 64, false);
} else if instruction & 0x1f80_0000 == 0x1280_0000 {
let shift = ((instruction >> 21) & 3) * 16;
if shift >= width {
return None;
}
let immediate = u64::from((instruction >> 5) & 0xffff) << shift;
let value = match (instruction >> 29) & 3 {
0 => !immediate,
2 => immediate,
3 => (self.reg(rd, false)? & !(0xffff_u64 << shift)) | immediate,
_ => return None,
};
self.set(rd, value, width, false);
} else if instruction & 0x1f00_0000 == 0x1100_0000
|| instruction & 0x1f20_0000 == 0x0b00_0000
{
let immediate = instruction & 0x1f00_0000 == 0x1100_0000;
let left = self.reg(rn, immediate)? & mask(width);
let right = if immediate {
u64::from((instruction >> 10) & 0xfff)
<< (if instruction & (1 << 22) != 0 { 12 } else { 0 })
} else {
if (instruction >> 22) & 3 != 0 {
return None;
}
let shift = (instruction >> 10) & 63;
if shift >= width {
return None;
}
(self.reg((instruction >> 16) & 31, false)? << shift) & mask(width)
};
let subtract = instruction & (1 << 30) != 0;
let set_flags = instruction & (1 << 29) != 0;
let value = if subtract {
left.wrapping_sub(right)
} else {
left.wrapping_add(right)
} & mask(width);
if set_flags {
let sign = 1_u64 << (width - 1);
let carry = if subtract {
left >= right
} else {
u128::from(left) + u128::from(right) > u128::from(mask(width))
};
let overflow = if subtract {
(left ^ right) & (left ^ value) & sign != 0
} else {
!(left ^ right) & (left ^ value) & sign != 0
};
self.flags = Some((value & sign != 0, value == 0, carry, overflow));
}
self.set(rd, value, width, immediate && !set_flags);
} else if instruction & 0x1f80_0000 == 0x1200_0000
|| instruction & 0x1f20_0000 == 0x0a00_0000
{
let left = self.reg(rn, false)?;
let right = if instruction & 0x1f80_0000 == 0x1200_0000 {
logical_immediate(instruction, width)?
} else {
if (instruction >> 22) & 3 != 0 {
return None;
}
let shift = (instruction >> 10) & 63;
if shift >= width {
return None;
}
self.reg((instruction >> 16) & 31, false)? << shift
};
let value = match (instruction >> 29) & 3 {
0 => left & right,
1 => left | right,
2 => left ^ right,
_ => return None,
};
self.set(rd, value, width, false);
} else if instruction & 0x7f80_0000 == 0x5300_0000 {
let rotate = (instruction >> 16) & 63;
let end = (instruction >> 10) & 63;
if rotate >= width || end >= width || (instruction >> 22) & 1 != width / 64 {
return None;
}
let source = self.reg(rn, false)? & mask(width);
let value = if rotate <= end {
(source >> rotate) & mask(end - rotate + 1)
} else {
(source & mask(end + 1)) << (width - rotate)
};
self.set(rd, value, width, false);
} else if instruction & 0x3b00_0000 == 0x3900_0000 && instruction & (1 << 26) == 0 {
let size = 1_usize << (instruction >> 30);
let address = self
.reg(rn, true)?
.checked_add(u64::from((instruction >> 10) & 0xfff) * size as u64)?;
match (instruction >> 22) & 3 {
0 => self.write(address, size, self.reg(rd, false)?)?,
1 => {
let value = self.read(address, size)?;
self.set(rd, value, if size == 8 { 64 } else { 32 }, false);
}
_ => return None,
}
} else {
return None;
}
pc = next;
}
None
}
}
pub(super) fn executable_address(layout: &ElfLayout, address: u64) -> bool {
address.is_multiple_of(4)
&& layout.program_headers.iter().any(|segment| {
segment.flags & 1 != 0
&& address >= segment.virtual_address
&& address
.checked_add(4)
.zip(segment.virtual_address.checked_add(segment.file_size))
.is_some_and(|(end, limit)| end <= limit)
})
}
pub(super) fn callbacks(
module: &[u8],
entry: u32,
image: &[u8],
layout: &ElfLayout,
max_calls: usize,
) -> Result<Vec<u64>> {
let mut matches = Vec::new();
for function in selector_functions(module, entry)? {
let mut registers = [None; 31];
registers[0] = Some(RUNTIME);
registers[30] = Some(RETURN);
let selector = Selector {
module,
image,
layout,
registers,
sp: STACK + STACK_SIZE as u64,
stack: vec![None; STACK_SIZE],
flags: None,
read_image_base: false,
calls: Vec::new(),
};
if let Some(calls) = selector.run(function, max_calls) {
matches.push(calls);
}
}
match matches.len() {
1 => Ok(matches.pop().unwrap()),
count => invalid(format!(
"constructor module entry reaches {count} validated callback selectors"
)),
}
}
@@ -0,0 +1,180 @@
use super::*;
use senbei_elf::LoadSegment;
fn fixture() -> (Vec<u8>, Vec<u8>, ElfLayout) {
let mut module = vec![0; 0x2000];
// Synthetic selector: check every primary, then call either all primary
// or all fallback offsets. Bit 1 disables a record's instruction check;
// null primaries are skipped. The entry reaches it through a direct call.
let code: [u32; 41] = [
0xd10043ff, // sub sp, sp, #16
0xf90003fe, // str x30, [sp]
0xf9400413, // ldr x19, [x0, #8]
0xb0000014, // adrp x20, table page
0x91200294, // add x20, x20, #0x800
0xb9400295, // ldr w21, [x20] (count)
0x91002294, // add x20, x20, #8 (records)
0x52800016, // mov w22, #0 (fallback)
0x52800017, // mov w23, #0 (index)
0x6b1502ff, // cmp w23, w21
0x540001e2, // b.hs checked
0x8b171298, // add x24, x20, x23, lsl #4
0xb9400300, // ldr w0, [x24]
0x34000140, // cbz w0, check_next
0xb9400f01, // ldr w1, [x24, #12]
0x121f0021, // and w1, w1, #2
0x350000e1, // cbnz w1, check_next
0x8b000260, // add x0, x19, x0
0xb9400000, // ldr w0, [x0]
0xb9400b01, // ldr w1, [x24, #8]
0x6b01001f, // cmp w0, w1
0x54000040, // b.eq check_next
0x52800036, // mov w22, #1
0x110006f7, // add w23, w23, #1
0x17fffff1, // b check
0x52800017, // mov w23, #0
0x6b1502ff, // cmp w23, w21
0x54000142, // b.hs done
0x8b171298, // add x24, x20, x23, lsl #4
0xb9400300, // ldr w0, [x24]
0x340000a0, // cbz w0, invoke_next
0x34000056, // cbz w22, primary
0xb9400700, // ldr w0, [x24, #4]
0x8b000260, // add x0, x19, x0
0xd63f0000, // blr x0
0x110006f7, // add w23, w23, #1
0x17fffff6, // b invoke
0x52800020, // mov w0, #1
0xf94003fe, // ldr x30, [sp]
0x910043ff, // add sp, sp, #16
0xd65f03c0, // ret
];
put(&mut module, 0x40, 0x9400_0030);
put(&mut module, 0x44, 0xd65f_03c0);
for (index, instruction) in code.into_iter().enumerate() {
put(&mut module, 0x100 + index * 4, instruction);
}
put(&mut module, 0x1800, 2);
for (index, record) in [[0x4400, 0x4500, 0x1234, 0], [0x4200, 0x4520, 0x5678, 0]]
.into_iter()
.enumerate()
{
for (field, word) in record.into_iter().enumerate() {
put(&mut module, 0x1808 + index * 16 + field * 4, word);
}
}
let mut image = vec![0; 0x1000];
put(&mut image, 0x400, 0x1234);
put(&mut image, 0x200, 0x5678);
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0x4000,
file_size: 0x1000,
memory_size: 0x1000,
flags: 5,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
(module, image, layout)
}
fn put(data: &mut [u8], offset: usize, word: u32) {
data[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
#[test]
fn callback_order_comes_from_execution_even_after_module_relocation() {
let (module, image, layout) = fixture();
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4400, 0x4200]
);
let mut relocated = vec![0; 0x5000];
relocated.extend_from_slice(&module);
// Unreachable data and code cannot become another table candidate.
relocated[..module.len()].copy_from_slice(&module);
assert_eq!(
callbacks(&relocated, 0x5040, &image, &layout, 2).unwrap(),
[0x4400, 0x4200]
);
}
#[test]
fn one_instruction_mismatch_selects_the_entire_fallback_group() {
let (module, mut image, layout) = fixture();
put(&mut image, 0x200, 0);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4500, 0x4520]
);
}
#[test]
fn callback_flags_null_records_and_single_entries_follow_the_code() {
let (mut module, mut image, layout) = fixture();
put(&mut image, 0x200, 0);
put(&mut module, 0x1824, 2);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4400, 0x4200]
);
put(&mut module, 0x1818, 0);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 2).unwrap(),
[0x4400]
);
put(&mut module, 0x1800, 1);
assert_eq!(
callbacks(&module, 0x40, &image, &layout, 1).unwrap(),
[0x4400]
);
}
#[test]
fn unsupported_code_invalid_memory_and_excess_calls_are_rejected() {
let (module, image, layout) = fixture();
assert!(callbacks(&module, 0x40, &image, &layout, 1).is_err());
for (offset, instruction) in [
(0x100, 0xd400_0001), // system call
(0x108, 0xf900_0413), // attempted write outside the stack
(0x150, 0x1400_0000), // nonterminating comparison loop
(0x1808, 0x9000), // primary is outside the image
(0x181c, 0x9000), // fallback is outside the image
(0x1800, u32::MAX), // count exceeds the module/table bounds
] {
let mut broken = module.clone();
put(&mut broken, offset, instruction);
let mut mismatched = image.clone();
if offset == 0x181c {
put(&mut mismatched, 0x200, 0);
}
assert!(
callbacks(&broken, 0x40, &mismatched, &layout, 2).is_err(),
"offset {offset:x}"
);
}
assert!(callbacks(&module[..0x1820], 0x40, &image, &layout, 2).is_err());
assert!(callbacks(&module, 0xffff_ffff, &image, &layout, 2).is_err());
}
#[test]
fn multiple_reachable_selectors_are_not_silently_chosen() {
let (mut module, image, layout) = fixture();
module.copy_within(0x100..0x1a4, 0x300);
put(&mut module, 0x44, 0x9400_00af); // BL 0x300
put(&mut module, 0x48, 0xd65f_03c0);
assert!(
callbacks(&module, 0x40, &image, &layout, 2)
.unwrap_err()
.to_string()
.contains("2 validated")
);
}
+1
View File
@@ -1,4 +1,5 @@
mod artifact;
mod constructors;
mod error;
mod pipeline;
+447 -167
View File
@@ -602,19 +602,59 @@ fn dynamic_symbol_names(symbols: &[u8], strings: &[u8]) -> Result<Vec<Vec<u8>>>
.collect()
}
fn auxiliary_seed_references(module: &[u8]) -> HashSet<u64> {
let mut references = HashSet::new();
// The relocation decoder loads the seed through the module GOT, then
// squares it in a W register. Follow ADRP/LDR X/LDR W/MUL W instead of
// treating a neighbouring module's function offset as a format constant.
for (index, code) in module.windows(16).step_by(4).enumerate() {
let word = |offset| u32::from_le_bytes(code[offset..offset + 4].try_into().unwrap());
let adrp = word(0);
let pointer = word(4);
let seed = word(8);
let square = word(12);
let page_register = adrp & 31;
let pointer_register = pointer & 31;
let seed_register = seed & 31;
if adrp & 0x9f00_0000 != 0x9000_0000
|| page_register == 31
|| pointer & 0xffc0_0000 != 0xf940_0000
|| (pointer >> 5) & 31 != page_register
|| pointer_register == 31
|| seed != 0xb940_0000 | (pointer_register << 5) | seed_register
|| seed_register == 31
|| square != 0x1b00_7c00 | (seed_register << 16) | (seed_register << 5) | seed_register
{
continue;
}
let Some(address) = aarch64_adrp_target(adrp, (index * 4) as u64)
.and_then(|page| page.checked_add(u64::from((pointer >> 10) & 0xfff) * 8))
else {
continue;
};
if let Ok(offset) = usize::try_from(address)
&& let Ok(target) = read_u64(module, offset)
{
references.insert(target);
}
}
references
}
fn discover_auxiliary_symbol_seed(module: &[u8]) -> Result<u32> {
const PREFIX0: u32 = 0x8f;
const PREFIX1: u32 = 0x15ec;
const TYPE_ID: u32 = 0x81;
const MODE0: u32 = 1;
const MODE1: u32 = 5;
const SUFFIX0: u32 = 0x0a54;
const SUFFIX1: u32 = 0x12008;
let references = auxiliary_seed_references(module);
let mut matches = Vec::new();
for seed_offset in (0x20..module.len().saturating_sub(0x10)).step_by(4) {
for seed_offset in (0x20..=module.len().saturating_sub(0x10)).step_by(4) {
if read_u32(module, seed_offset - 0x20)? == PREFIX0
&& read_u32(module, seed_offset - 0x1c)? == PREFIX1
&& read_u32(module, seed_offset - 0x1c)? != 0
&& read_u32(module, seed_offset - 0x1c)?.is_multiple_of(4)
&& read_u32(module, seed_offset - 0x18)? == TYPE_ID
&& read_u32(module, seed_offset - 0x10)? == TYPE_ID
&& read_u32(module, seed_offset - 8)? == MODE0
@@ -622,6 +662,7 @@ fn discover_auxiliary_symbol_seed(module: &[u8]) -> Result<u32> {
&& read_u32(module, seed_offset + 4)? == SUFFIX0
&& read_u32(module, seed_offset + 8)? == SUFFIX1
&& read_u32(module, seed_offset + 0x0c)? == 0
&& references.contains(&(seed_offset as u64))
{
matches.push(read_u32(module, seed_offset)?);
}
@@ -687,7 +728,53 @@ fn aarch64_writes_w1_immediate(instruction: u32) -> bool {
)
}
pub(super) fn aarch64_adrp_target(instruction: u32, pc: u64) -> Option<u64> {
if instruction & 0x9f00_0000 != 0x9000_0000 {
return None;
}
let immediate = ((instruction >> 29) & 3) | ((instruction >> 5) & 0x7ffff) << 2;
let pages = i64::from(immediate);
let pages = if pages & (1 << 20) != 0 {
pages - (1 << 21)
} else {
pages
};
(pc & !0xfff).checked_add_signed(pages * 0x1000)
}
fn aarch64_plt_veneer_target(code: &[u8], pc: u64) -> Option<u64> {
let adrp = read_u32(code, 0).ok()?;
let add = read_u32(code, 4).ok()?;
let branch = read_u32(code, 8).ok()?;
let register = adrp & 31;
// Linker range-extension veneers preserve the call's arguments and LR:
// ADRP X16/X17, page; ADD X16/X17, X16/X17, lo12; BR X16/X17.
if !matches!(register, 16 | 17)
|| add & 0xffc0_03ff != 0x9100_0000 | (register << 5) | register
|| branch != 0xd61f_0000 | (register << 5)
{
return None;
}
aarch64_adrp_target(adrp, pc)?.checked_add(u64::from((add >> 10) & 0xfff))
}
fn plt_call_features(image: &[u8], layout: &ElfLayout, target: u64) -> Result<PltCallFeatures> {
let mut targets = HashSet::from([target]);
for segment in &layout.program_headers {
if segment.flags & 1 == 0 {
continue;
}
let code = slice_u64(image, segment.offset, segment.file_size)?;
for (index, instructions) in code.windows(12).step_by(4).enumerate() {
let pc = segment
.virtual_address
.checked_add((index * 4) as u64)
.ok_or_else(|| Error::Invalid("veneer PC overflow".to_owned()))?;
if aarch64_plt_veneer_target(instructions, pc) == Some(target) {
targets.insert(pc);
}
}
}
let mut features = PltCallFeatures::default();
for segment in &layout.program_headers {
if segment.flags & 1 == 0 || segment.file_size < 4 {
@@ -708,7 +795,8 @@ fn plt_call_features(image: &[u8], layout: &ElfLayout, target: u64) -> Result<Pl
.checked_add(relative as u64)
.ok_or_else(|| Error::Invalid("executable PC overflow".to_owned()))?;
let instruction = read_u32(image, offset)?;
if aarch64_bl_target(instruction, pc) != Some(target) {
if !aarch64_bl_target(instruction, pc).is_some_and(|address| targets.contains(&address))
{
continue;
}
features.calls += 1;
@@ -838,7 +926,11 @@ struct ConstructorProfile {
fini_addend: u64,
}
fn virtual_address_to_file_offset(layout: &ElfLayout, address: u64, size: u64) -> Option<usize> {
pub(super) fn virtual_address_to_file_offset(
layout: &ElfLayout,
address: u64,
size: u64,
) -> Option<usize> {
let end = address.checked_add(size)?;
for segment in &layout.program_headers {
let segment_end = segment.virtual_address.checked_add(segment.file_size)?;
@@ -852,131 +944,91 @@ fn virtual_address_to_file_offset(layout: &ElfLayout, address: u64, size: u64) -
None
}
fn discover_suppressed_constructors(
module: &[u8],
source: &[u8],
layout: &ElfLayout,
max_count: usize,
) -> Result<Option<Vec<u64>>> {
const RECORD_SIZE: usize = 16;
const MIN_COUNT: usize = 2;
const MAX_COUNT: usize = 256;
let mut matches = Vec::new();
if module.len() < 8 {
return Ok(None);
}
for base in (0..=module.len() - 8).step_by(4) {
let count = read_u32(module, base)? as usize;
if !(MIN_COUNT..=MAX_COUNT.min(max_count)).contains(&count)
|| read_u32(module, base + 4)? != 0
// The bootstrap saves the incoming linker register frame, calls Stage 1,
// restores that frame and returns. Validate this at the ELF init relocation;
// e_entry may be zero for a shared library, so it cannot identify the slot.
fn is_stage1_constructor(source: &[u8], layout: &ElfLayout, address: u64) -> bool {
let Some(offset) = virtual_address_to_file_offset(layout, address, 0x84) else {
return false;
};
for pair in 0..15_u32 {
let first = pair * 2 + 1;
let save = 0xa9bf_03e0 | ((first + 1) << 10) | first;
let restore = 0xa8c1_03e0 | ((first + 1) << 10) | first;
if read_u32(source, offset + pair as usize * 4).ok() != Some(save)
|| read_u32(source, offset + 0x44 + (14 - pair) as usize * 4).ok() != Some(restore)
{
continue;
}
let table_size = 8_usize
.checked_add(
count
.checked_mul(RECORD_SIZE)
.ok_or_else(|| Error::Invalid("constructor table size overflow".to_owned()))?,
)
.ok_or_else(|| Error::Invalid("constructor table size overflow".to_owned()))?;
if base
.checked_add(table_size)
.is_none_or(|end| end > module.len())
{
continue;
}
let mut constructors = Vec::with_capacity(count);
let mut valid = true;
for index in 0..count {
let record = base + 8 + index * RECORD_SIZE;
let constructor = u64::from(read_u32(module, record)?);
let fallback = u64::from(read_u32(module, record + 4)?);
let original = read_u32(module, record + 8)?;
let reserved = read_u32(module, record + 12)?;
let constructor_offset = virtual_address_to_file_offset(layout, constructor, 4);
let fallback_offset = virtual_address_to_file_offset(layout, fallback, 4);
if constructor == 0
|| fallback == 0
|| reserved != 0
|| constructor_offset.and_then(|offset| read_u32(source, offset).ok())
!= Some(original)
|| fallback_offset.and_then(|offset| read_u32(source, offset).ok())
!= Some(AARCH64_RET)
{
valid = false;
break;
}
constructors.push(constructor);
}
if valid
&& constructors.windows(2).all(|pair| pair[0] < pair[1])
&& constructors.iter().copied().collect::<HashSet<_>>().len() == count
{
matches.push(constructors);
return false;
}
}
match matches.len() {
1 => Ok(matches.pop()),
0 => Ok(None),
count => invalid(format!(
"constructor control module has {count} validated suppression tables"
)),
}
read_u32(source, offset + 0x3c).ok() == Some(0x9100_03e0)
&& read_u32(source, offset + 0x40)
.ok()
.and_then(|instruction| aarch64_bl_target(instruction, address + 0x40))
.is_some_and(|target| super::constructors::executable_address(layout, target))
&& read_u32(source, offset + 0x80).ok() == Some(AARCH64_RET)
}
fn constructor_profile(
module: &[u8],
module: (&[u8], u32),
source: &[u8],
protected: &[u8],
layout: &ElfLayout,
init_array: SectionHeader,
fini_array: SectionHeader,
old_dyn: RelocationTable<'_>,
) -> Result<Option<ConstructorProfile>> {
) -> Result<ConstructorProfile> {
if !init_array.size.is_multiple_of(8) {
return invalid("init-array size is not entry-aligned");
}
let init_count = usize_from_u64(init_array.size / 8, "init-array count")?;
let Some(mut init_addends) =
discover_suppressed_constructors(module, source, layout, init_count)?
else {
return Ok(None);
};
let max_suppressed = *init_addends
.last()
.ok_or_else(|| Error::Invalid("constructor table is empty".to_owned()))?;
let callbacks = super::constructors::callbacks(module.0, module.1, source, layout, init_count)?;
let init_end = init_array
.address
.checked_add(init_array.size)
.ok_or_else(|| Error::Invalid("init-array range overflow".to_owned()))?;
let existing = old_dyn.collect_where(|relocation| {
let mut existing = old_dyn.collect_where(|relocation| {
relocation.offset >= init_array.address && relocation.offset < init_end
})?;
let extra = existing
.iter()
.filter(|relocation| {
relocation.kind() == R_AARCH64_RELATIVE
&& relocation.symbol() == 0
&& relocation.addend >= 0
})
.map(|relocation| relocation.addend as u64)
.filter(|&addend| {
addend != layout.entrypoint
&& addend > max_suppressed
&& addend <= max_suppressed.saturating_add(0x1000)
})
.collect::<Vec<_>>();
if extra.len() != 1 {
existing.sort_by_key(|relocation| relocation.offset);
if existing.len() != init_count {
return invalid("init-array does not have one relocation per slot");
}
let mut init_addends = Vec::new();
let mut bootstraps = 0;
for (slot, relocation) in existing.iter().enumerate() {
if relocation.offset != init_array.address + slot as u64 * 8
|| relocation.kind() != R_AARCH64_RELATIVE
|| relocation.symbol() != 0
|| relocation.addend < 0
{
return invalid("init-array contains an invalid relocation");
}
let address = relocation.addend as u64;
if is_stage1_constructor(protected, layout, address) {
bootstraps += 1;
init_addends.extend_from_slice(&callbacks);
} else {
if !super::constructors::executable_address(layout, address) {
return invalid("init-array callback is outside executable segments");
}
let offset = virtual_address_to_file_offset(layout, address, 4).ok_or_else(|| {
Error::Invalid("init-array callback is not file-backed".to_owned())
})?;
// The linker still calls the unsuppressed entries after Stage 1
// returns. RET-only placeholders have no initialization effect.
if read_u32(source, offset)? != AARCH64_RET {
init_addends.push(address);
}
}
}
if bootstraps != 1 {
return invalid(format!(
"expected one unsuppressed constructor near the suppression table, found {}",
extra.len()
"init-array has {bootstraps} validated Stage 1 callbacks"
));
}
init_addends.push(extra[0]);
if init_addends.len() > init_count {
return invalid(format!(
"constructor profile produced {} entries for {init_count} init slots",
init_addends.len()
));
return invalid("restored constructors exceed the init-array capacity");
}
let fini = old_dyn.collect_where(|relocation| relocation.offset == fini_array.address)?;
if fini.len() != 1
@@ -986,10 +1038,10 @@ fn constructor_profile(
{
return invalid("fini-array relocation is not a single RELATIVE record");
}
Ok(Some(ConstructorProfile {
Ok(ConstructorProfile {
init_addends,
fini_addend: fini[0].addend as u64,
}))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -1540,7 +1592,7 @@ fn materialize_static_elf_tables(
symbol_patch_data: &[u8],
auxiliary_data: &[u8],
auxiliary_symbol_seed: Option<u32>,
constructor_module: Option<&[u8]>,
constructor_module: Option<(&[u8], u32)>,
) -> Result<(ElfLayout, ElfMaterializationReport, u64)> {
let names = layout.section_names(source)?;
let indices = required_section_indices(&names)?;
@@ -1753,14 +1805,9 @@ fn materialize_static_elf_tables(
})?;
let init_array = layout.section_headers[init_index];
let fini_array = layout.section_headers[fini_index];
let profile =
constructor_profile(module, output, layout, init_array, fini_array, old_dyn)?
.ok_or_else(|| {
Error::Invalid(
"constructor control module 0x98 has no validated constructor table"
.to_owned(),
)
})?;
let profile = constructor_profile(
module, output, source, layout, init_array, fini_array, old_dyn,
)?;
Some((profile, init_index, fini_index, init_array, fini_array))
}
None => None,
@@ -2333,7 +2380,17 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
.transpose()?;
let constructor_module = artifacts
.get(&0x98)
.map(|artifact| read_file(&artifact.path))
.map(|artifact| -> Result<_> {
let entry = artifact
.entry_offset
.filter(|entry| *entry != 0)
.ok_or_else(|| {
Error::Invalid(
"constructor module 0x98 lacks its descriptor entry offset".to_owned(),
)
})?;
Ok((read_file(&artifact.path)?, entry))
})
.transpose()?;
let config = Module9bConfig::parse(&module)?;
@@ -2486,7 +2543,9 @@ pub fn restore_libil2cpp(options: &RestoreOptions) -> Result<RestoreReport> {
&symbol_patch_data,
&decoded,
auxiliary_symbol_seed,
constructor_module.as_deref(),
constructor_module
.as_ref()
.map(|(image, entry)| (image.as_slice(), *entry)),
)?;
restored_layout = new_layout;
materialization = Some(report);
@@ -2684,13 +2743,17 @@ mod tests {
assert_eq!(relocations[1].symbol(), 2);
}
#[test]
fn discovers_auxiliary_symbol_permutation_seed() {
let mut module = vec![0_u8; 0x80];
let seed_offset = 0x40;
fn auxiliary_seed_module(helper_offset: u32) -> Vec<u8> {
let mut module = vec![0_u8; 0x100];
let seed_offset = 0xf0;
for (offset, value) in [
(0x10, 0x9000_0000), // ADRP X0, page 0
(0x14, 0xf940_4000), // LDR X0, [X0, #0x80]
(0x18, 0xb940_0001), // LDR W1, [X0]
(0x1c, 0x1b01_7c21), // MUL W1, W1, W1
(0x80, seed_offset as u32),
(seed_offset - 0x20, 0x8f_u32),
(seed_offset - 0x1c, 0x15ec),
(seed_offset - 0x1c, helper_offset),
(seed_offset - 0x18, 0x81),
(seed_offset - 0x10, 0x81),
(seed_offset - 8, 1),
@@ -2702,10 +2765,270 @@ mod tests {
] {
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
}
module
}
#[test]
fn discovers_auxiliary_symbol_seed_with_relocated_helper() {
// The configuration ends exactly at the module boundary.
for helper_offset in [0x15ec, 0x164c, 0x2400] {
assert_eq!(
discover_auxiliary_symbol_seed(&auxiliary_seed_module(helper_offset))
.expect("symbol seed"),
0x4db4_d9ec
);
}
}
#[test]
fn auxiliary_symbol_seed_requires_a_valid_decoder_reference() {
for (offset, value) in [
(0x10, 0xd503_201f_u32), // no ADRP
(0x14, 0xf940_4020), // wrong page register
(0x18, 0xb940_0002), // wrong seed register for MUL
(0x1c, 0x1b02_7c21), // multiplication by another value
(0x80, 0xec), // GOT points at a different field
(0x80, 0x1000), // GOT points outside the module
(0xd4, 0), // null helper offset
(0xd4, 0x164d), // unaligned helper offset
(0xe8, 2), // unsupported mode
] {
let mut module = auxiliary_seed_module(0x164c);
module[offset..offset + 4].copy_from_slice(&value.to_le_bytes());
assert!(discover_auxiliary_symbol_seed(&module).is_err());
}
for length in [0, 0x10, 0x20, 0xff] {
assert!(
discover_auxiliary_symbol_seed(&auxiliary_seed_module(0x164c)[..length]).is_err()
);
}
}
#[test]
fn auxiliary_symbol_seed_trials_all_candidates_and_rejects_ambiguity() {
let mut module = auxiliary_seed_module(0x164c);
module.copy_within(0xd0..0x100, 0x40);
// An unreferenced configuration-shaped decoy must not hide the real one.
assert_eq!(
discover_auxiliary_symbol_seed(&module).expect("symbol seed"),
discover_auxiliary_symbol_seed(&module).unwrap(),
0x4db4_d9ec
);
module.copy_within(0x10..0x20, 0x20);
module[0x24..0x28].copy_from_slice(&0xf940_4400_u32.to_le_bytes());
module[0x88..0x90].copy_from_slice(&0x60_u64.to_le_bytes());
assert!(
discover_auxiliary_symbol_seed(&module)
.unwrap_err()
.to_string()
.contains("2 validated")
);
}
#[test]
fn auxiliary_seed_reference_supports_negative_page_displacements() {
let mut module = auxiliary_seed_module(0x164c);
module.resize(0x2010, 0);
module.copy_within(0x10..0x20, 0x2000);
module[0x10..0x20].fill(0);
// ADRP X0, PC page - 2 pages; GOT and seed remain in page zero.
module[0x2000..0x2004].copy_from_slice(&0xd0ff_ffe0_u32.to_le_bytes());
assert_eq!(
discover_auxiliary_symbol_seed(&module).unwrap(),
0x4db4_d9ec
);
}
#[test]
fn plt_call_features_follow_validated_range_extension_veneers() {
let mut image = vec![0_u8; 0x130];
for (offset, word) in [
(0x100, 0x2a1f_03e1_u32), // MOV W1, WZR
(0x104, 0x9400_03ff), // BL 0x2000
(0x108, 0x3400_0040), // CBZ W0
(0x110, 0x2a1f_03e1), // MOV W1, WZR
(0x114, 0x9400_0003), // BL 0x1020 (veneer)
(0x118, 0x3400_0040), // CBZ W0
(0x120, 0xb000_0010), // ADRP X16, 0x2000
(0x124, 0x9100_0210), // ADD X16, X16, #0
(0x128, 0xd61f_0200), // BR X16
] {
image[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0x100,
virtual_address: 0x1000,
file_size: 0x30,
memory_size: 0x30,
flags: PF_R | 1,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
assert_eq!(
plt_call_features(&image, &layout, 0x2000).unwrap(),
PltCallFeatures {
calls: 2,
zero_arg1: 2,
w0_zero_branch: 2,
..PltCallFeatures::default()
}
);
let original = image.clone();
for (offset, word) in [
(0x120, 0xb000_000f_u32), // ADRP uses a different register
(0x124, 0x9100_0610), // veneer targets a different address
(0x128, 0xd63f_0200), // BLR would overwrite LR
(0x128, 0xd61f_0220), // BR uses a different register
] {
image.copy_from_slice(&original);
image[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
assert_eq!(plt_call_features(&image, &layout, 0x2000).unwrap().calls, 1);
}
assert!(aarch64_plt_veneer_target(&original[0x120..0x128], 0x1020).is_none());
image.copy_from_slice(&original);
for (offset, word) in [
(0x120, 0xb000_0011_u32),
(0x124, 0x9100_0231),
(0x128, 0xd61f_0220),
] {
image[offset..offset + 4].copy_from_slice(&word.to_le_bytes());
}
assert_eq!(plt_call_features(&image, &layout, 0x2000).unwrap().calls, 2);
}
#[test]
fn constructor_profile_replaces_the_bootstrap_and_preserves_linker_order() {
let mut module = vec![0_u8; 0x100];
// Entry calls a selector that dispatches image + 0x400.
for (index, instruction) in [0x9400_0010_u32, 0xd65f_03c0].into_iter().enumerate() {
module[index * 4..index * 4 + 4].copy_from_slice(&instruction.to_le_bytes());
}
for (index, instruction) in [
0xd100_43ff_u32, // SUB SP, SP, #16
0xf900_03fe, // STR LR, [SP]
0xf940_0413, // LDR X19, [X0, #8]
0x5280_8000, // MOV W0, #0x400
0x8b00_0260, // ADD X0, X19, X0
0xd63f_0000, // BLR X0
0x5280_0020, // MOV W0, #1
0xf940_03fe, // LDR LR, [SP]
0x9100_43ff, // ADD SP, SP, #16
0xd65f_03c0, // RET
]
.into_iter()
.enumerate()
{
module[0x40 + index * 4..0x44 + index * 4].copy_from_slice(&instruction.to_le_bytes());
}
let mut protected = vec![0_u8; 0x8000];
for pair in 0..15_u32 {
let first = pair * 2 + 1;
for (offset, instruction) in [
(
0x1000 + pair as usize * 4,
0xa9bf_03e0 | ((first + 1) << 10) | first,
),
(
0x1044 + (14 - pair) as usize * 4,
0xa8c1_03e0 | ((first + 1) << 10) | first,
),
] {
protected[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
}
for (offset, instruction) in [
(0x103c, 0x9100_03e0_u32),
(0x1040, 0x9400_0070),
(0x1080, AARCH64_RET),
] {
protected[offset..offset + 4].copy_from_slice(&instruction.to_le_bytes());
}
let mut restored = protected.clone();
restored[0x1000..0x1084].fill(0);
restored[0x2000..0x2004].copy_from_slice(&AARCH64_RET.to_le_bytes());
restored[0x6000..0x6004].copy_from_slice(&0xd503_201f_u32.to_le_bytes());
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0,
file_size: 0x8000,
memory_size: 0x8000,
flags: PF_R | 1,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
let init = SectionHeader {
name: 0,
section_type: 14,
flags: 3,
address: 0x7000,
offset: 0x7000,
size: 24,
link: 0,
info: 0,
alignment: 8,
entry_size: 8,
};
let fini = SectionHeader {
address: 0x7100,
offset: 0x7100,
size: 8,
..init
};
for (original, expected) in [
([0x1000, 0x6000, 0x2000], vec![0x400, 0x6000]),
([0x6000, 0x1000, 0x2000], vec![0x6000, 0x400]),
] {
let mut data = Vec::new();
for (slot, addend) in original.into_iter().enumerate() {
Rela {
offset: 0x7000 + slot as u64 * 8,
info: u64::from(R_AARCH64_RELATIVE),
addend,
}
.encode(&mut data);
}
Rela {
offset: 0x7100,
info: u64::from(R_AARCH64_RELATIVE),
addend: 0x500,
}
.encode(&mut data);
let table = RelocationTable {
data: &data,
offset: 0,
count: 4,
remap: None,
};
let profile = constructor_profile(
(&module, 0),
&restored,
&protected,
&layout,
init,
fini,
table,
)
.unwrap();
assert_eq!(profile.init_addends, expected);
assert_eq!(profile.fini_addend, 0x500);
}
protected[0x1000] ^= 1;
assert!(!is_stage1_constructor(&protected, &layout, 0x1000));
}
#[test]
@@ -2762,47 +3085,4 @@ mod tests {
]
);
}
#[test]
fn discovers_validated_constructor_table() {
const COUNT: usize = 43;
let mut source = vec![0_u8; 0x1000];
let mut module = vec![0_u8; 0x1000];
let table = 0x40;
module[table..table + 4].copy_from_slice(&(COUNT as u32).to_le_bytes());
for index in 0..COUNT {
let constructor = 0x100 + index * 4;
let fallback = 0x500 + index * 4;
let instruction = 0xa000_0000_u32 + index as u32;
source[constructor..constructor + 4].copy_from_slice(&instruction.to_le_bytes());
source[fallback..fallback + 4].copy_from_slice(&AARCH64_RET.to_le_bytes());
let record = table + 8 + index * 16;
module[record..record + 4].copy_from_slice(&(constructor as u32).to_le_bytes());
module[record + 4..record + 8].copy_from_slice(&(fallback as u32).to_le_bytes());
module[record + 8..record + 12].copy_from_slice(&instruction.to_le_bytes());
}
let layout = ElfLayout {
entrypoint: 0,
program_header_offset: 0,
program_header_size: 0x38,
program_header_count: 1,
program_headers: vec![LoadSegment {
offset: 0,
virtual_address: 0,
file_size: source.len() as u64,
memory_size: source.len() as u64,
flags: PF_R,
alignment: 0x1000,
}],
section_headers: Vec::new(),
section_name_index: 0,
private_section_index: 0,
};
let constructors = discover_suppressed_constructors(&module, &source, &layout, COUNT)
.expect("constructor table")
.expect("validated constructor table");
assert_eq!(constructors.len(), COUNT);
assert_eq!(constructors[0], 0x100);
assert_eq!(constructors[COUNT - 1], 0x100 + ((COUNT - 1) * 4) as u64);
}
}