use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use super::{
build_jump_table, build_link_plan, load_captured_args, load_captured_linker_args,
parse_symbol_table, run_link_plan, run_obj_plan, thin_build, validate_environment,
CapturedLinkerInvocation, CapturedRustcInvocation, HotpatchModuleCache, LinkerOs, PatchPlan,
};
struct StubCache {
needed_hash: u64,
aslr_reference: u64,
target_os: LinkerOs,
bytes: Vec<u8>,
}
pub struct Patcher {
package: String,
rustc_path: PathBuf,
linker_path: PathBuf,
cwd: PathBuf,
patch_out_dir: PathBuf,
target_os: LinkerOs,
original_cache: HotpatchModuleCache,
captured_rustc_args: HashMap<String, CapturedRustcInvocation>,
captured_linker_args: HashMap<String, CapturedLinkerInvocation>,
stub_cache: Mutex<Option<StubCache>>,
}
impl Patcher {
#[allow(clippy::too_many_arguments)]
pub fn new(
package: String,
rustc_path: PathBuf,
linker_path: PathBuf,
cwd: PathBuf,
patch_out_dir: PathBuf,
target_os: LinkerOs,
original_cache: HotpatchModuleCache,
captured_rustc_args: HashMap<String, CapturedRustcInvocation>,
captured_linker_args: HashMap<String, CapturedLinkerInvocation>,
) -> Self {
Self {
package,
rustc_path,
linker_path,
cwd,
patch_out_dir,
target_os,
original_cache,
captured_rustc_args,
captured_linker_args,
stub_cache: Mutex::new(None),
}
}
#[allow(clippy::too_many_arguments)]
pub fn initialize(
workspace_root: &Path,
package: String,
rustc_cache_dir: &Path,
linker_cache_dir: &Path,
real_linker: &Path,
original_binary: &Path,
target_os: LinkerOs,
target_triple: Option<&str>,
) -> Result<Self> {
let captured_rustc_args = load_captured_args(rustc_cache_dir, target_triple)
.with_context(|| format!("load rustc cache {}", rustc_cache_dir.display()))?;
let captured_linker_args = load_captured_linker_args(linker_cache_dir)
.with_context(|| format!("load linker cache {}", linker_cache_dir.display()))?;
let original_cache = HotpatchModuleCache::from_path(original_binary)
.with_context(|| format!("parse original binary {}", original_binary.display()))?;
let patch_out_dir = workspace_root.join("target/.whisker/patches");
let rustc_path = current_rustc();
Ok(Self::new(
package,
rustc_path,
real_linker.to_path_buf(),
workspace_root.to_path_buf(),
patch_out_dir,
target_os,
original_cache,
captured_rustc_args,
captured_linker_args,
))
}
pub async fn build_patch(
&self,
aslr_reference: u64,
crate_key: Option<&str>,
) -> Result<PatchPlan> {
let user_key = self.package.replace('-', "_");
let crate_key = crate_key
.map(str::to_owned)
.unwrap_or_else(|| user_key.clone());
let captured_rustc = self.captured_rustc_args.get(&crate_key).with_context(|| {
format!(
"no captured rustc invocation for crate `{crate_key}`; \
was the fat build run?",
)
})?;
let user_rustc = if crate_key != user_key {
Some(self.captured_rustc_args.get(&user_key).with_context(|| {
format!(
"sub-crate patch ({crate_key}) needs user crate `{user_key}` in the link \
(for the `whisker_aslr_anchor` symbol), but no captured rustc invocation \
is available for it",
)
})?)
} else {
None
};
let captured_linker = self.lookup_captured_linker().with_context(|| {
format!(
"no captured linker invocation for `{}`; was the fat build run with linker capture?",
self.package,
)
})?;
validate_environment(captured_rustc, &self.rustc_path)
.context("environment validation before thin rebuild")?;
let obj_plan = thin_build::build_obj_plan(captured_rustc, &self.patch_out_dir);
let object = run_obj_plan(&obj_plan, &self.rustc_path, &self.cwd)
.await
.context("rustc --emit=obj for thin patch")?;
let user_object_for_link: Option<PathBuf> = if let Some(user_rustc) = user_rustc {
let user_obj_plan = thin_build::build_obj_plan(user_rustc, &self.patch_out_dir);
let obj = run_obj_plan(&user_obj_plan, &self.rustc_path, &self.cwd)
.await
.context("rustc --emit=obj for user crate (sub-crate patch anchor source)")?;
Some(obj)
} else {
None
};
let extras: Vec<PathBuf> = if aslr_reference == 0 {
user_object_for_link.iter().cloned().collect()
} else {
let stub_path = self.patch_out_dir.join("aslr-stub.o");
let stub_bytes = self
.stub_bytes_for_objects(&object, user_object_for_link.as_deref(), aslr_reference)
.context("synthesize stub object")?;
std::fs::write(&stub_path, &stub_bytes)
.with_context(|| format!("write stub object to {}", stub_path.display()))?;
let mut e = vec![stub_path];
if let Some(uo) = user_object_for_link.as_ref() {
e.push(uo.clone());
}
if matches!(self.target_os, LinkerOs::Linux) {
e.push(self.original_cache.lib.clone());
}
e
};
let output_dylib = self.expected_patch_path();
let extra_exports: &[&str] = match self.target_os {
LinkerOs::Macos => &["_whisker_aslr_anchor", "_whisker_app_main", "_whisker_tick"],
_ => &[],
};
let link_plan = build_link_plan(
&captured_linker.args,
&object,
&output_dylib,
self.target_os,
&extras,
extra_exports,
);
let new_dylib = run_link_plan(&link_plan, &self.linker_path, &self.cwd)
.await
.context("link patch dylib (object + stub)")?;
let new_symbols = parse_symbol_table(&new_dylib)
.with_context(|| format!("parse {}", new_dylib.display()))?;
let new_base_address = read_image_base(&new_dylib)?;
Ok(build_jump_table(
&self.original_cache.symbols,
&new_symbols,
new_dylib,
self.original_cache.aslr_reference,
new_base_address,
))
}
fn stub_bytes_for_objects(
&self,
object: &Path,
extra: Option<&Path>,
aslr_reference: u64,
) -> Result<Vec<u8>> {
let mut paths: Vec<&Path> = vec![object];
if let Some(p) = extra {
paths.push(p);
}
let needed =
super::compute_needed_symbols_multi(&paths).context("compute_needed_symbols_multi")?;
let needed_hash = hash_needed(&needed);
if let Ok(guard) = self.stub_cache.lock() {
if let Some(cached) = guard.as_ref() {
if cached.needed_hash == needed_hash
&& cached.aslr_reference == aslr_reference
&& cached.target_os == self.target_os
{
return Ok(cached.bytes.clone());
}
}
}
let bytes = super::build_stub_for_needed(
&needed,
&self.original_cache,
self.target_os,
aslr_reference,
)
.context("build_stub_for_needed")?;
if let Ok(mut guard) = self.stub_cache.lock() {
*guard = Some(StubCache {
needed_hash,
aslr_reference,
target_os: self.target_os,
bytes: bytes.clone(),
});
}
Ok(bytes)
}
pub fn expected_patch_path(&self) -> PathBuf {
self.patch_out_dir.join(thin_build::library_filename_for_os(
&self.package,
self.target_os,
))
}
fn lookup_captured_linker(&self) -> Option<&CapturedLinkerInvocation> {
let stem_lib = format!("lib{}", self.package.replace('-', "_"));
let stem_bin = self.package.replace('-', "_");
let exts: &[&str] = match self.target_os {
LinkerOs::Macos => &[".dylib"],
LinkerOs::Linux => &[".so"],
LinkerOs::Other => &[".dll"],
};
let mut best: Option<&CapturedLinkerInvocation> = None;
for inv in self.captured_linker_args.values() {
let Some(out) = inv.output.as_deref() else {
continue;
};
let Some(name) = Path::new(out).file_name().and_then(|n| n.to_str()) else {
continue;
};
let matches_ext = exts.iter().any(|ext| name.ends_with(ext));
if !matches_ext {
continue;
}
let matches_stem = name.starts_with(&stem_lib) || name.starts_with(&stem_bin);
if !matches_stem {
continue;
}
best = match best {
Some(prev) if prev.timestamp_micros >= inv.timestamp_micros => Some(prev),
_ => Some(inv),
};
}
best
}
}
fn current_rustc() -> PathBuf {
PathBuf::from(std::env::var_os("RUSTC").unwrap_or_else(|| "rustc".into()))
}
fn hash_needed(needed: &[String]) -> u64 {
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01B3;
let mut h = FNV_OFFSET;
for name in needed {
for b in name.as_bytes() {
h ^= *b as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h ^= 0xff;
h = h.wrapping_mul(FNV_PRIME);
}
h
}
fn read_image_base(path: &Path) -> Result<u64> {
let table = parse_symbol_table(path).with_context(|| format!("parse {}", path.display()))?;
Ok(table
.by_name
.get("whisker_aslr_anchor")
.or_else(|| table.by_name.get("_whisker_aslr_anchor"))
.map(|s| s.address)
.unwrap_or(0))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hotpatch::SymbolTable;
fn empty_cache() -> HotpatchModuleCache {
HotpatchModuleCache {
lib: PathBuf::from("/orig.dylib"),
symbols: SymbolTable::default(),
aslr_reference: 0x1_0000_0000,
}
}
fn linker_inv(output: &str, ts: u128) -> CapturedLinkerInvocation {
CapturedLinkerInvocation {
output: Some(output.into()),
args: vec!["-shared".into()],
timestamp_micros: ts,
}
}
#[test]
fn new_holds_onto_its_inputs() {
let p = Patcher::new(
"demo".into(),
PathBuf::from("/usr/local/bin/rustc"),
PathBuf::from("/usr/bin/clang"),
PathBuf::from("/tmp/cwd"),
PathBuf::from("/tmp/patches"),
LinkerOs::Macos,
empty_cache(),
HashMap::new(),
HashMap::new(),
);
assert_eq!(p.package, "demo");
assert_eq!(
p.expected_patch_path(),
PathBuf::from("/tmp/patches")
.join(thin_build::library_filename_for_os("demo", LinkerOs::Macos,)),
);
}
fn patcher_with_linker_map(
target_os: LinkerOs,
package: &str,
linker: HashMap<String, CapturedLinkerInvocation>,
) -> Patcher {
Patcher::new(
package.into(),
"/rustc".into(),
"/cc".into(),
"/cwd".into(),
"/patches".into(),
target_os,
empty_cache(),
HashMap::new(),
linker,
)
}
#[test]
fn lookup_finds_macos_dylib_with_lib_prefix() {
let mut m = HashMap::new();
m.insert(
"libdemo-abc123.dylib".into(),
linker_inv("/cargo/target/debug/deps/libdemo-abc123.dylib", 100),
);
let p = patcher_with_linker_map(LinkerOs::Macos, "demo", m);
let inv = p.lookup_captured_linker().expect("found");
assert_eq!(inv.timestamp_micros, 100);
}
#[test]
fn lookup_finds_linux_so_with_underscored_crate_name() {
let mut m = HashMap::new();
m.insert(
"libhello_world.so".into(),
linker_inv("/cargo/target/debug/deps/libhello_world.so", 50),
);
let p = patcher_with_linker_map(LinkerOs::Linux, "hello-world", m);
let inv = p.lookup_captured_linker().expect("found");
assert_eq!(inv.timestamp_micros, 50);
}
#[test]
fn lookup_returns_most_recent_when_multiple_match() {
let mut m = HashMap::new();
m.insert(
"libdemo.dylib".into(),
linker_inv("/path/libdemo.dylib", 100),
);
m.insert(
"libdemo-abc.dylib".into(),
linker_inv("/path/libdemo-abc.dylib", 200),
);
let p = patcher_with_linker_map(LinkerOs::Macos, "demo", m);
let inv = p.lookup_captured_linker().expect("found");
assert_eq!(inv.timestamp_micros, 200);
}
#[test]
fn lookup_returns_none_when_no_extension_matches() {
let mut m = HashMap::new();
m.insert("libdemo.so".into(), linker_inv("/path/libdemo.so", 100));
let p = patcher_with_linker_map(LinkerOs::Macos, "demo", m);
assert!(p.lookup_captured_linker().is_none());
}
#[test]
fn lookup_returns_none_when_crate_name_doesnt_match() {
let mut m = HashMap::new();
m.insert(
"libother.dylib".into(),
linker_inv("/path/libother.dylib", 100),
);
let p = patcher_with_linker_map(LinkerOs::Macos, "demo", m);
assert!(p.lookup_captured_linker().is_none());
}
#[tokio::test]
async fn build_patch_errors_when_captured_rustc_args_missing() {
let p = Patcher::new(
"package-not-in-cache".into(),
"/rustc".into(),
"/cc".into(),
"/cwd".into(),
"/patches".into(),
LinkerOs::Macos,
empty_cache(),
HashMap::new(), HashMap::new(),
);
let err = p.build_patch(0, None).await.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("no captured rustc invocation"), "{msg}");
}
#[tokio::test]
async fn build_patch_errors_when_explicit_crate_key_missing() {
let p = Patcher::new(
"demo".into(),
"/rustc".into(),
"/cc".into(),
"/cwd".into(),
"/patches".into(),
LinkerOs::Macos,
empty_cache(),
HashMap::new(),
HashMap::new(),
);
let err = p.build_patch(0, Some("not_a_crate")).await.unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("not_a_crate"), "{msg}");
}
}