use std::path::PathBuf;
use object::SymbolKind;
use subsecond_types::{AddressMap, JumpTable};
use super::symbol_table::SymbolTable;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct DiffReport {
pub removed: Vec<String>,
pub added: Vec<String>,
pub weak: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PatchPlan {
pub table: JumpTable,
pub report: DiffReport,
}
pub fn build_jump_table(
old: &SymbolTable,
new: &SymbolTable,
new_lib: PathBuf,
aslr_reference: u64,
new_base_address: u64,
) -> PatchPlan {
let mut map = AddressMap::default();
let mut report = DiffReport::default();
for (name, new_sym) in &new.by_name {
let old_sym = match old.by_name.get(name) {
Some(s) => s,
None => {
report.added.push(name.clone());
continue;
}
};
if !is_patchable(old_sym.kind) || !is_patchable(new_sym.kind) {
continue;
}
if old_sym.is_undefined || new_sym.is_undefined {
continue;
}
if old_sym.size == 0 && new_sym.size == 0 && cfg!(target_os = "linux") {
continue;
}
if old_sym.is_weak || new_sym.is_weak {
report.weak.push(name.clone());
}
map.insert(old_sym.address, new_sym.address);
}
for name in old.by_name.keys() {
if !new.by_name.contains_key(name) {
report.removed.push(name.clone());
}
}
report.removed.sort();
report.added.sort();
report.weak.sort();
PatchPlan {
table: JumpTable {
lib: new_lib,
map,
aslr_reference,
new_base_address,
ifunc_count: 0, },
report,
}
}
fn is_patchable(kind: SymbolKind) -> bool {
matches!(kind, SymbolKind::Text)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hotpatch::symbol_table::SymbolInfo;
use std::collections::HashMap;
fn text(addr: u64, size: u64) -> SymbolInfo {
SymbolInfo {
address: addr,
kind: SymbolKind::Text,
size,
is_undefined: false,
is_weak: false,
}
}
fn data(addr: u64, size: u64) -> SymbolInfo {
SymbolInfo {
address: addr,
kind: SymbolKind::Data,
size,
is_undefined: false,
is_weak: false,
}
}
fn weak(addr: u64, size: u64) -> SymbolInfo {
SymbolInfo {
address: addr,
kind: SymbolKind::Text,
size,
is_undefined: false,
is_weak: true,
}
}
fn undef() -> SymbolInfo {
SymbolInfo {
address: 0,
kind: SymbolKind::Text,
size: 0,
is_undefined: true,
is_weak: false,
}
}
fn t(entries: Vec<(&str, SymbolInfo)>) -> SymbolTable {
let mut by_name = HashMap::new();
for (n, s) in entries {
by_name.insert(n.to_string(), s);
}
SymbolTable { by_name }
}
fn lib() -> PathBuf {
PathBuf::from("/tmp/patch.dylib")
}
#[test]
fn identical_tables_produce_an_identity_like_map() {
let same = t(vec![("foo", text(0x1000, 32)), ("bar", text(0x2000, 16))]);
let plan = build_jump_table(&same, &same, lib(), 0, 0);
assert_eq!(plan.table.map.len(), 2);
assert_eq!(plan.table.map.get(&0x1000), Some(&0x1000));
assert_eq!(plan.table.map.get(&0x2000), Some(&0x2000));
assert!(plan.report.removed.is_empty());
assert!(plan.report.added.is_empty());
}
#[test]
fn moved_function_records_old_to_new_address() {
let old = t(vec![("app", text(0x1000, 100))]);
let new = t(vec![("app", text(0x1500, 120))]);
let plan = build_jump_table(&old, &new, lib(), 0, 0);
assert_eq!(plan.table.map.len(), 1);
assert_eq!(plan.table.map.get(&0x1000), Some(&0x1500));
}
#[test]
fn aslr_and_base_address_are_propagated_into_the_table() {
let same = t(vec![("foo", text(0x1000, 32))]);
let plan = build_jump_table(&same, &same, lib(), 0xCAFE_BABE, 0xDEAD_BEEF);
assert_eq!(plan.table.aslr_reference, 0xCAFE_BABE);
assert_eq!(plan.table.new_base_address, 0xDEAD_BEEF);
assert_eq!(plan.table.lib, PathBuf::from("/tmp/patch.dylib"));
assert_eq!(plan.table.ifunc_count, 0);
}
#[test]
fn data_symbols_are_skipped() {
let old = t(vec![("g", data(0x4000, 8))]);
let new = t(vec![("g", data(0x4100, 8))]);
assert!(build_jump_table(&old, &new, lib(), 0, 0)
.table
.map
.is_empty());
}
#[test]
fn undefined_symbols_are_skipped_either_side() {
let old = t(vec![("undef", undef()), ("def", text(0x1000, 16))]);
let new = t(vec![("undef", text(0x9000, 16)), ("def", undef())]);
let plan = build_jump_table(&old, &new, lib(), 0, 0);
assert!(plan.table.map.is_empty());
}
#[test]
#[cfg(target_os = "linux")]
fn zero_sized_symbols_are_skipped_on_elf() {
let old = t(vec![("plt_stub", text(0x1000, 0))]);
let new = t(vec![("plt_stub", text(0x1100, 0))]);
assert!(build_jump_table(&old, &new, lib(), 0, 0)
.table
.map
.is_empty());
}
#[test]
#[cfg(any(target_os = "macos", target_os = "ios"))]
fn zero_sized_symbols_are_kept_on_mach_o() {
let old = t(vec![("foo", text(0x1000, 0))]);
let new = t(vec![("foo", text(0x1100, 0))]);
let plan = build_jump_table(&old, &new, lib(), 0, 0);
assert_eq!(plan.table.map.len(), 1);
assert_eq!(plan.table.map.get(&0x1000), Some(&0x1100));
}
#[test]
fn added_and_removed_show_up_in_the_report() {
let old = t(vec![("kept", text(0x1000, 16)), ("gone", text(0x2000, 16))]);
let new = t(vec![
("kept", text(0x1100, 16)),
("brand_new", text(0x3000, 16)),
]);
let plan = build_jump_table(&old, &new, lib(), 0, 0);
assert_eq!(plan.report.removed, vec!["gone".to_string()]);
assert_eq!(plan.report.added, vec!["brand_new".to_string()]);
assert_eq!(plan.table.map.len(), 1);
assert_eq!(plan.table.map.get(&0x1000), Some(&0x1100));
}
#[test]
fn weak_symbol_is_emitted_but_listed_in_report() {
let old = t(vec![("maybe", weak(0x1000, 16))]);
let new = t(vec![("maybe", weak(0x1100, 16))]);
let plan = build_jump_table(&old, &new, lib(), 0, 0);
assert_eq!(plan.table.map.len(), 1);
assert_eq!(plan.report.weak, vec!["maybe".to_string()]);
}
#[test]
fn report_lists_are_sorted_for_stable_diagnostics() {
let old = t(vec![
("c", text(0x1, 1)),
("a", text(0x2, 1)),
("b", text(0x3, 1)),
]);
let new = t(vec![
("z", text(0x4, 1)),
("y", text(0x5, 1)),
("x", text(0x6, 1)),
]);
let plan = build_jump_table(&old, &new, lib(), 0, 0);
assert_eq!(plan.report.removed, vec!["a", "b", "c"]);
assert_eq!(plan.report.added, vec!["x", "y", "z"]);
}
#[test]
fn empty_inputs_produce_empty_outputs() {
let plan = build_jump_table(
&SymbolTable::default(),
&SymbolTable::default(),
lib(),
0,
0,
);
assert!(plan.table.map.is_empty());
assert!(plan.report.added.is_empty());
assert!(plan.report.removed.is_empty());
assert!(plan.report.weak.is_empty());
}
}