use crate::stencil_model::RelocKind;
#[derive(Debug, PartialEq, Eq)]
pub enum PatchError {
OutOfRange {
kind: &'static str,
site: u64,
target: u64,
},
Misaligned {
kind: &'static str,
target: u64,
},
BadScale {
kind: &'static str,
target: u64,
scale: u8,
},
}
impl std::fmt::Display for PatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PatchError::OutOfRange { kind, site, target } => {
write!(f, "{kind}: target {target:#x} out of range of site {site:#x}")
}
PatchError::Misaligned { kind, target } => {
write!(f, "{kind}: target {target:#x} is not 4-byte aligned")
}
PatchError::BadScale { kind, target, scale } => {
write!(f, "{kind}: target {target:#x} not aligned for scale {scale}")
}
}
}
}
impl std::error::Error for PatchError {}
fn read_u32(buf: &[u8], off: usize) -> u32 {
u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
}
fn write_u32(buf: &mut [u8], off: usize, v: u32) {
buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
}
pub fn patch_aarch64_branch26(
buf: &mut [u8],
off: usize,
site_addr: u64,
target: u64,
) -> Result<(), PatchError> {
if target % 4 != 0 {
return Err(PatchError::Misaligned { kind: "branch26", target });
}
let delta = (target as i64).wrapping_sub(site_addr as i64);
if !(-(1 << 27)..(1 << 27)).contains(&delta) {
return Err(PatchError::OutOfRange { kind: "branch26", site: site_addr, target });
}
let imm26 = ((delta >> 2) as u32) & 0x03FF_FFFF;
let insn = read_u32(buf, off);
write_u32(buf, off, (insn & 0xFC00_0000) | imm26);
Ok(())
}
pub fn patch_aarch64_page21(
buf: &mut [u8],
off: usize,
site_addr: u64,
target: u64,
) -> Result<(), PatchError> {
let page_delta = ((target as i64) >> 12).wrapping_sub((site_addr as i64) >> 12);
if !(-(1 << 20)..(1 << 20)).contains(&page_delta) {
return Err(PatchError::OutOfRange { kind: "page21", site: site_addr, target });
}
let imm = page_delta as u32;
let immlo = imm & 0b11;
let immhi = (imm >> 2) & 0x7FFFF;
let insn = read_u32(buf, off);
let cleared = insn & !((0b11 << 29) | (0x7FFFF << 5));
write_u32(buf, off, cleared | (immlo << 29) | (immhi << 5));
Ok(())
}
pub fn patch_aarch64_pageoff12(
buf: &mut [u8],
off: usize,
target: u64,
scale: u8,
) -> Result<(), PatchError> {
let lo12 = (target & 0xFFF) as u32;
if scale > 0 && lo12 & ((1 << scale) - 1) != 0 {
return Err(PatchError::BadScale { kind: "pageoff12", target, scale });
}
let imm12 = lo12 >> scale;
let insn = read_u32(buf, off);
let cleared = insn & !(0xFFF << 10);
write_u32(buf, off, cleared | (imm12 << 10));
Ok(())
}
pub fn patch_x64_rel32(
buf: &mut [u8],
off: usize,
site_addr: u64,
target: u64,
addend: i64,
) -> Result<(), PatchError> {
let delta = (target as i64).wrapping_add(addend).wrapping_sub(site_addr as i64);
let disp = i32::try_from(delta)
.map_err(|_| PatchError::OutOfRange { kind: "rel32", site: site_addr, target })?;
buf[off..off + 4].copy_from_slice(&disp.to_le_bytes());
Ok(())
}
pub fn patch_abs64(buf: &mut [u8], off: usize, target: u64, addend: i64) {
let v = (target as i64).wrapping_add(addend) as u64;
buf[off..off + 8].copy_from_slice(&v.to_le_bytes());
}
pub fn is_indirect(kind: RelocKind) -> bool {
matches!(kind, RelocKind::GotPage21 | RelocKind::GotPageOff12 | RelocKind::GotRel32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn b26_forward_8_bytes_encodes_0x14000002() {
let mut buf = vec![0x00, 0x00, 0x00, 0x14];
patch_aarch64_branch26(&mut buf, 0, 0, 8).unwrap();
assert_eq!(buf, vec![0x02, 0x00, 0x00, 0x14]);
}
#[test]
fn b26_backward_4_bytes_encodes_0x17ffffff() {
let mut buf = vec![0x00, 0x00, 0x00, 0x14];
patch_aarch64_branch26(&mut buf, 0, 4, 0).unwrap();
assert_eq!(buf, vec![0xFF, 0xFF, 0xFF, 0x17]);
}
#[test]
fn b26_preserves_opcode_bits_including_bl() {
let mut buf = vec![0x00, 0x00, 0x00, 0x94];
patch_aarch64_branch26(&mut buf, 0, 0x1000, 0x1010).unwrap();
assert_eq!(buf, vec![0x04, 0x00, 0x00, 0x94]);
}
#[test]
fn b26_range_and_alignment_errors() {
let mut buf = vec![0u8; 4];
assert!(matches!(
patch_aarch64_branch26(&mut buf, 0, 0, 1 << 28),
Err(PatchError::OutOfRange { .. })
));
assert!(patch_aarch64_branch26(&mut buf, 0, 0, (1 << 27) - 4).is_ok());
assert!(matches!(
patch_aarch64_branch26(&mut buf, 0, 0, 6),
Err(PatchError::Misaligned { .. })
));
}
#[test]
fn adrp_same_page_encodes_zero_imm() {
let mut buf = vec![0x00, 0x00, 0x00, 0x90];
patch_aarch64_page21(&mut buf, 0, 0x4000, 0x4FFF).unwrap();
assert_eq!(read_u32(&buf, 0), 0x9000_0000);
}
#[test]
fn adrp_immhi_immlo_split_matches_hand_encoding() {
let mut buf = vec![0x00, 0x00, 0x00, 0x90];
patch_aarch64_page21(&mut buf, 0, 0, 5 << 12).unwrap();
assert_eq!(read_u32(&buf, 0), 0x9000_0000 | (1 << 29) | (1 << 5));
}
#[test]
fn adrp_beyond_4gb_is_err() {
let mut buf = vec![0x00, 0x00, 0x00, 0x90];
assert!(matches!(
patch_aarch64_page21(&mut buf, 0, 0, 1 << 33),
Err(PatchError::OutOfRange { .. })
));
}
#[test]
fn pageoff12_on_add_is_unscaled() {
let mut buf = vec![0x00, 0x00, 0x00, 0x91];
patch_aarch64_pageoff12(&mut buf, 0, 0x123, 0).unwrap();
assert_eq!(read_u32(&buf, 0), 0x9100_0000 | (0x123 << 10));
}
#[test]
fn pageoff12_on_ldr64_scales_offset_by_8() {
let mut buf = vec![0x00, 0x00, 0x40, 0xF9];
patch_aarch64_pageoff12(&mut buf, 0, 0x18, 3).unwrap();
assert_eq!(read_u32(&buf, 0), 0xF940_0000 | (3 << 10));
}
#[test]
fn pageoff12_value_unaligned_for_scale_is_err() {
let mut buf = vec![0x00, 0x00, 0x40, 0xF9];
assert!(matches!(
patch_aarch64_pageoff12(&mut buf, 0, 0x1C, 3),
Err(PatchError::BadScale { .. })
));
}
#[test]
fn rel32_jmp_to_next_instruction_encodes_zero() {
let mut buf = vec![0xE9, 0xAA, 0xAA, 0xAA, 0xAA];
patch_x64_rel32(&mut buf, 1, 0x1001, 0x1005, -4).unwrap();
assert_eq!(&buf[1..], &[0, 0, 0, 0]);
}
#[test]
fn rel32_backward_with_normalized_addend() {
let mut buf = vec![0u8; 4];
patch_x64_rel32(&mut buf, 0, 0x2000, 0x1000, -4).unwrap();
assert_eq!(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), -0x1004);
patch_x64_rel32(&mut buf, 0, 0x2000, 0x1000, -5).unwrap();
assert_eq!(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), -0x1005);
}
#[test]
fn rel32_beyond_2gb_is_err() {
let mut buf = vec![0u8; 4];
assert!(matches!(
patch_x64_rel32(&mut buf, 0, 0, 1 << 32, 0),
Err(PatchError::OutOfRange { .. })
));
}
#[test]
fn abs64_writes_little_endian_with_addend() {
let mut buf = vec![0u8; 8];
patch_abs64(&mut buf, 0, 0x1122_3344_5566_7788, 0x10);
assert_eq!(buf, 0x1122_3344_5566_7798u64.to_le_bytes());
}
fn reference_branch26(insn: u32, pc: u64, target: u64) -> u32 {
let delta = target.wrapping_sub(pc) as i64;
(insn & 0xFC00_0000) | (((delta as u64 >> 2) as u32) & 0x03FF_FFFF)
}
#[test]
fn branch26_matches_reference_encoder_across_grid() {
let base: u64 = 0x10_0000;
for k in 0..10_000u64 {
let delta: i64 = ((k as i64) - 5_000) * 25_036;
let target = (base as i64 + delta) as u64 & !3;
let mut buf = vec![0x00, 0x00, 0x00, 0x14];
patch_aarch64_branch26(&mut buf, 0, base, target).unwrap();
assert_eq!(
read_u32(&buf, 0),
reference_branch26(0x1400_0000, base, target),
"delta {delta}"
);
}
}
}