Skip to main content

logicaffeine_forge/
patch.rs

1//! Relocation patchers — pure byte math, compiled (and unit-tested) on EVERY
2//! host so the x86-64 patchers are exercised on an arm64 dev machine and vice
3//! versa. Each function rewrites one relocation site in a staging buffer given
4//! the site's final runtime address and its target's address.
5
6use crate::stencil_model::RelocKind;
7
8/// Patch errors carry enough context to identify the failing site.
9#[derive(Debug, PartialEq, Eq)]
10pub enum PatchError {
11    /// Branch/load displacement does not fit the encoding.
12    OutOfRange {
13        /// Relocation kind name.
14        kind: &'static str,
15        /// Address of the patch site.
16        site: u64,
17        /// Address the site must reach.
18        target: u64,
19    },
20    /// arm64 targets must be 4-byte aligned for branches.
21    Misaligned {
22        /// Relocation kind name.
23        kind: &'static str,
24        /// The misaligned target address.
25        target: u64,
26    },
27    /// The value is not aligned for the access size encoded at the site.
28    BadScale {
29        /// Relocation kind name.
30        kind: &'static str,
31        /// The target address.
32        target: u64,
33        /// Access-size shift encoded at the site.
34        scale: u8,
35    },
36}
37
38impl std::fmt::Display for PatchError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        match self {
41            PatchError::OutOfRange { kind, site, target } => {
42                write!(f, "{kind}: target {target:#x} out of range of site {site:#x}")
43            }
44            PatchError::Misaligned { kind, target } => {
45                write!(f, "{kind}: target {target:#x} is not 4-byte aligned")
46            }
47            PatchError::BadScale { kind, target, scale } => {
48                write!(f, "{kind}: target {target:#x} not aligned for scale {scale}")
49            }
50        }
51    }
52}
53
54impl std::error::Error for PatchError {}
55
56fn read_u32(buf: &[u8], off: usize) -> u32 {
57    u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
58}
59
60fn write_u32(buf: &mut [u8], off: usize, v: u32) {
61    buf[off..off + 4].copy_from_slice(&v.to_le_bytes());
62}
63
64/// aarch64 `b`/`bl` imm26: PC-relative, ±128 MiB, 4-byte units. Preserves the
65/// opcode bits, rewriting only the immediate.
66pub fn patch_aarch64_branch26(
67    buf: &mut [u8],
68    off: usize,
69    site_addr: u64,
70    target: u64,
71) -> Result<(), PatchError> {
72    if target % 4 != 0 {
73        return Err(PatchError::Misaligned { kind: "branch26", target });
74    }
75    let delta = (target as i64).wrapping_sub(site_addr as i64);
76    if !(-(1 << 27)..(1 << 27)).contains(&delta) {
77        return Err(PatchError::OutOfRange { kind: "branch26", site: site_addr, target });
78    }
79    let imm26 = ((delta >> 2) as u32) & 0x03FF_FFFF;
80    let insn = read_u32(buf, off);
81    write_u32(buf, off, (insn & 0xFC00_0000) | imm26);
82    Ok(())
83}
84
85/// aarch64 `adrp` hi21: the PAGE delta (4 KiB pages), split immlo\[30:29\] /
86/// immhi\[23:5\]. Range ±4 GiB.
87pub fn patch_aarch64_page21(
88    buf: &mut [u8],
89    off: usize,
90    site_addr: u64,
91    target: u64,
92) -> Result<(), PatchError> {
93    let page_delta = ((target as i64) >> 12).wrapping_sub((site_addr as i64) >> 12);
94    if !(-(1 << 20)..(1 << 20)).contains(&page_delta) {
95        return Err(PatchError::OutOfRange { kind: "page21", site: site_addr, target });
96    }
97    let imm = page_delta as u32;
98    let immlo = imm & 0b11;
99    let immhi = (imm >> 2) & 0x7FFFF;
100    let insn = read_u32(buf, off);
101    let cleared = insn & !((0b11 << 29) | (0x7FFFF << 5));
102    write_u32(buf, off, cleared | (immlo << 29) | (immhi << 5));
103    Ok(())
104}
105
106/// aarch64 `add`/`ldr` lo12: the low 12 bits of the target, shifted right by
107/// the access-size scale for loads/stores.
108pub fn patch_aarch64_pageoff12(
109    buf: &mut [u8],
110    off: usize,
111    target: u64,
112    scale: u8,
113) -> Result<(), PatchError> {
114    let lo12 = (target & 0xFFF) as u32;
115    if scale > 0 && lo12 & ((1 << scale) - 1) != 0 {
116        return Err(PatchError::BadScale { kind: "pageoff12", target, scale });
117    }
118    let imm12 = lo12 >> scale;
119    let insn = read_u32(buf, off);
120    let cleared = insn & !(0xFFF << 10);
121    write_u32(buf, off, cleared | (imm12 << 10));
122    Ok(())
123}
124
125/// x86-64 rip-relative disp32 (`jmp`/`call`/`lea`/`mov`):
126/// `disp = target + addend − field_address`, with the rip-after-displacement
127/// distance already NORMALIZED into the addend at extraction time (ELF bakes
128/// it in natively; build.rs adjusts Mach-O and COFF to match).
129pub fn patch_x64_rel32(
130    buf: &mut [u8],
131    off: usize,
132    site_addr: u64,
133    target: u64,
134    addend: i64,
135) -> Result<(), PatchError> {
136    let delta = (target as i64).wrapping_add(addend).wrapping_sub(site_addr as i64);
137    let disp = i32::try_from(delta)
138        .map_err(|_| PatchError::OutOfRange { kind: "rel32", site: site_addr, target })?;
139    buf[off..off + 4].copy_from_slice(&disp.to_le_bytes());
140    Ok(())
141}
142
143/// 8-byte absolute address, little-endian.
144pub fn patch_abs64(buf: &mut [u8], off: usize, target: u64, addend: i64) {
145    let v = (target as i64).wrapping_add(addend) as u64;
146    buf[off..off + 8].copy_from_slice(&v.to_le_bytes());
147}
148
149/// Whether this reloc kind resolves through a pointer slot (GOT/IAT/refptr) —
150/// the buffer layout must provide one and point the site at it.
151pub fn is_indirect(kind: RelocKind) -> bool {
152    matches!(kind, RelocKind::GotPage21 | RelocKind::GotPageOff12 | RelocKind::GotRel32)
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    // ---- aarch64 branch26: hand-assembled expectations from the ARM ARM ----
160
161    #[test]
162    fn b26_forward_8_bytes_encodes_0x14000002() {
163        // `b +8` at address 0: imm26 = 8/4 = 2 → 0x14000002.
164        let mut buf = vec![0x00, 0x00, 0x00, 0x14];
165        patch_aarch64_branch26(&mut buf, 0, 0, 8).unwrap();
166        assert_eq!(buf, vec![0x02, 0x00, 0x00, 0x14]);
167    }
168
169    #[test]
170    fn b26_backward_4_bytes_encodes_0x17ffffff() {
171        // `b -4`: imm26 = -1 → 0x17FFFFFF.
172        let mut buf = vec![0x00, 0x00, 0x00, 0x14];
173        patch_aarch64_branch26(&mut buf, 0, 4, 0).unwrap();
174        assert_eq!(buf, vec![0xFF, 0xFF, 0xFF, 0x17]);
175    }
176
177    #[test]
178    fn b26_preserves_opcode_bits_including_bl() {
179        // A BL site keeps its BL opcode (0x94000000 family).
180        let mut buf = vec![0x00, 0x00, 0x00, 0x94];
181        patch_aarch64_branch26(&mut buf, 0, 0x1000, 0x1010).unwrap();
182        assert_eq!(buf, vec![0x04, 0x00, 0x00, 0x94]);
183    }
184
185    #[test]
186    fn b26_range_and_alignment_errors() {
187        let mut buf = vec![0u8; 4];
188        // Beyond ±128 MiB.
189        assert!(matches!(
190            patch_aarch64_branch26(&mut buf, 0, 0, 1 << 28),
191            Err(PatchError::OutOfRange { .. })
192        ));
193        // At the exact positive edge: 2^27 - 4 is in range.
194        assert!(patch_aarch64_branch26(&mut buf, 0, 0, (1 << 27) - 4).is_ok());
195        // Misaligned target.
196        assert!(matches!(
197            patch_aarch64_branch26(&mut buf, 0, 0, 6),
198            Err(PatchError::Misaligned { .. })
199        ));
200    }
201
202    // ---- aarch64 adrp ----
203
204    #[test]
205    fn adrp_same_page_encodes_zero_imm() {
206        // adrp x0, . → immlo=0 immhi=0; base opcode 0x90000000.
207        let mut buf = vec![0x00, 0x00, 0x00, 0x90];
208        patch_aarch64_page21(&mut buf, 0, 0x4000, 0x4FFF).unwrap();
209        assert_eq!(read_u32(&buf, 0), 0x9000_0000);
210    }
211
212    #[test]
213    fn adrp_immhi_immlo_split_matches_hand_encoding() {
214        // +5 pages: imm=5 → immlo=0b01, immhi=1.
215        // encoding: 0x90000000 | (1 << 29) | (1 << 5).
216        let mut buf = vec![0x00, 0x00, 0x00, 0x90];
217        patch_aarch64_page21(&mut buf, 0, 0, 5 << 12).unwrap();
218        assert_eq!(read_u32(&buf, 0), 0x9000_0000 | (1 << 29) | (1 << 5));
219    }
220
221    #[test]
222    fn adrp_beyond_4gb_is_err() {
223        let mut buf = vec![0x00, 0x00, 0x00, 0x90];
224        assert!(matches!(
225            patch_aarch64_page21(&mut buf, 0, 0, 1 << 33),
226            Err(PatchError::OutOfRange { .. })
227        ));
228    }
229
230    // ---- aarch64 lo12 ----
231
232    #[test]
233    fn pageoff12_on_add_is_unscaled() {
234        // add x0, x0, #imm12: base 0x91000000; target lo12 = 0x123.
235        let mut buf = vec![0x00, 0x00, 0x00, 0x91];
236        patch_aarch64_pageoff12(&mut buf, 0, 0x123, 0).unwrap();
237        assert_eq!(read_u32(&buf, 0), 0x9100_0000 | (0x123 << 10));
238    }
239
240    #[test]
241    fn pageoff12_on_ldr64_scales_offset_by_8() {
242        // ldr x0, [x0, #imm12*8]: base 0xF9400000; lo12 = 0x18 → imm12 = 3.
243        let mut buf = vec![0x00, 0x00, 0x40, 0xF9];
244        patch_aarch64_pageoff12(&mut buf, 0, 0x18, 3).unwrap();
245        assert_eq!(read_u32(&buf, 0), 0xF940_0000 | (3 << 10));
246    }
247
248    #[test]
249    fn pageoff12_value_unaligned_for_scale_is_err() {
250        let mut buf = vec![0x00, 0x00, 0x40, 0xF9];
251        assert!(matches!(
252            patch_aarch64_pageoff12(&mut buf, 0, 0x1C, 3),
253            Err(PatchError::BadScale { .. })
254        ));
255    }
256
257    // ---- x86-64 rel32 ----
258
259    #[test]
260    fn rel32_jmp_to_next_instruction_encodes_zero() {
261        // jmp rel32: opcode at 0x1000, disp field at 0x1001, next insn at
262        // 0x1005. With the normalized addend (-4): disp = S - 4 - P = 0.
263        let mut buf = vec![0xE9, 0xAA, 0xAA, 0xAA, 0xAA];
264        patch_x64_rel32(&mut buf, 1, 0x1001, 0x1005, -4).unwrap();
265        assert_eq!(&buf[1..], &[0, 0, 0, 0]);
266    }
267
268    #[test]
269    fn rel32_backward_with_normalized_addend() {
270        // Field at 0x2000 (rip 0x2004), target 0x1000, normalized addend -4:
271        // disp = 0x1000 - 4 - 0x2000 = -0x1004.
272        let mut buf = vec![0u8; 4];
273        patch_x64_rel32(&mut buf, 0, 0x2000, 0x1000, -4).unwrap();
274        assert_eq!(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), -0x1004);
275        // COFF REL32_1 (one trailing byte): one more byte of distance.
276        patch_x64_rel32(&mut buf, 0, 0x2000, 0x1000, -5).unwrap();
277        assert_eq!(i32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]), -0x1005);
278    }
279
280    #[test]
281    fn rel32_beyond_2gb_is_err() {
282        let mut buf = vec![0u8; 4];
283        assert!(matches!(
284            patch_x64_rel32(&mut buf, 0, 0, 1 << 32, 0),
285            Err(PatchError::OutOfRange { .. })
286        ));
287    }
288
289    // ---- abs64 ----
290
291    #[test]
292    fn abs64_writes_little_endian_with_addend() {
293        let mut buf = vec![0u8; 8];
294        patch_abs64(&mut buf, 0, 0x1122_3344_5566_7788, 0x10);
295        assert_eq!(buf, 0x1122_3344_5566_7798u64.to_le_bytes());
296    }
297
298    // ---- two-implementation differential over a displacement grid ----------
299
300    /// An independent from-the-spec encoder for B/BL imm26 (ARM ARM C6.2.26):
301    /// imm26 = (target - pc) / 4, masked into bits [25:0].
302    fn reference_branch26(insn: u32, pc: u64, target: u64) -> u32 {
303        let delta = target.wrapping_sub(pc) as i64;
304        (insn & 0xFC00_0000) | (((delta as u64 >> 2) as u32) & 0x03FF_FFFF)
305    }
306
307    #[test]
308    fn branch26_matches_reference_encoder_across_grid() {
309        let base: u64 = 0x10_0000;
310        for k in 0..10_000u64 {
311            // Mixed forward/backward displacements across the range.
312            let delta: i64 = ((k as i64) - 5_000) * 25_036;
313            let target = (base as i64 + delta) as u64 & !3;
314            let mut buf = vec![0x00, 0x00, 0x00, 0x14];
315            patch_aarch64_branch26(&mut buf, 0, base, target).unwrap();
316            assert_eq!(
317                read_u32(&buf, 0),
318                reference_branch26(0x1400_0000, base, target),
319                "delta {delta}"
320            );
321        }
322    }
323}