Skip to main content

asmkit/core/
patch.rs

1//! Post-emit code patching (JSC MacroAssembler–style).
2//!
3//! # Workflow
4//!
5//! 1. During emission, call arch `patchable_*` helpers (or
6//!    [`CodeBuffer::reserve_patch_block`](crate::CodeBuffer::reserve_patch_block)).
7//!    These return self-describing [`PatchableSite`] / [`PatchableBlock`] handles.
8//! 2. Finalize with [`CodeBuffer::finish_patched`](crate::CodeBuffer::finish_patched)
9//!    so the [`PatchCatalog`] validates reachability (and the linker can rebase it).
10//! 3. Apply patches with **`unsafe`** methods on a JIT [`Span`] or `&mut [u8]`.
11//!    Patching does **not** go through [`CodeBufferFinalized`].
12//!
13//! # JSC correspondence
14//!
15//! | JSC | asmkit |
16//! |---|---|
17//! | `PatchableJump` / near call | [`PatchableSite`] + [`PatchableSite::retarget`] |
18//! | `DataLabel32` / `DataLabelPtr` | [`PatchableBlock`] + [`PatchableBlock::repatch_u32`] / [`repatch_u64`](PatchableBlock::repatch_u64) |
19//! | `padBeforePatch` + custom stub | [`reserve_patch_block`](crate::CodeBuffer::reserve_patch_block) → [`PatchableBlock::rewrite`] |
20//! | `repatchJump` on a code pointer | `unsafe` apply on [`Span`] / `&mut [u8]` |
21//!
22//! # Site vs block vs custom region
23//!
24//! - **Site** — a fixed-width displacement field (`LabelUse`) for jumps/calls; retarget by
25//!   code offset within the same image.
26//! - **Block** — a reserved byte range (immediate field or whole insn sequence); rewrite or
27//!   repatch integer payloads; shorter rewrites are nop-padded.
28//! - **Custom** — [`reserve_patch_block`](crate::CodeBuffer::reserve_patch_block) plants a
29//!   nop island you fill later with [`PatchableBlock::rewrite`].
30//!
31//! # Safety
32//!
33//! Handles from `patchable_*` describe locations in the buffer that produced them. Applying a
34//! handle is still `unsafe`: the bytes/`Span` must be that image (after finalize/link with
35//! stable offsets), and the caller must synchronize concurrent execution of the patched region.
36//! [`PatchableSite::new`] / [`PatchableBlock::new`] are `unsafe` for the same reason when
37//! constructing handles by hand.
38//!
39//! [`PatchCatalog`] remains for `finish_patched` validation and linker rebasing. After rebase,
40//! convert entries with [`PatchSite::to_patchable`] / [`PatchBlock::to_patchable`].
41
42use smallvec::SmallVec;
43
44use crate::{
45    AsmError,
46    core::{
47        arch_traits::Arch,
48        buffer::{CodeBufferFinalized, CodeOffset, LabelUse},
49    },
50};
51
52#[cfg(feature = "jit")]
53use crate::core::jit_allocator::{JitAllocator, Span};
54
55/// Catalog index for a patch block (linker / introspection).
56#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct PatchBlockId(u32);
58
59impl PatchBlockId {
60    pub(crate) const fn from_index(index: usize) -> Self {
61        Self(index as u32)
62    }
63
64    pub const fn index(self) -> usize {
65        self.0 as usize
66    }
67}
68
69/// Catalog index for a patch site (linker / introspection).
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub struct PatchSiteId(u32);
72
73impl PatchSiteId {
74    pub(crate) const fn from_index(index: usize) -> Self {
75        Self(index as u32)
76    }
77
78    pub const fn index(self) -> usize {
79        self.0 as usize
80    }
81}
82
83/// Catalog entry for a rewritable byte range.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub struct PatchBlock {
86    pub offset: CodeOffset,
87    pub size: CodeOffset,
88    pub align: CodeOffset,
89}
90
91impl PatchBlock {
92    /// Build a patch handle from a (possibly rebased) catalog entry.
93    pub const fn to_patchable(self, arch: Arch) -> PatchableBlock {
94        // SAFETY: catalog entries describe blocks recorded during emission.
95        unsafe { PatchableBlock::new(self.offset, self.size, arch) }
96    }
97}
98
99/// Catalog entry for a displacement field.
100#[derive(Clone, Copy, Debug, PartialEq, Eq)]
101pub struct PatchSite {
102    pub offset: CodeOffset,
103    pub kind: LabelUse,
104    pub current_target: CodeOffset,
105    pub addend: i64,
106}
107
108impl PatchSite {
109    /// Build a patch handle from a (possibly rebased) catalog entry.
110    pub const fn to_patchable(self) -> PatchableSite {
111        // SAFETY: catalog entries describe sites recorded during emission.
112        unsafe { PatchableSite::new(self.offset, self.kind, self.addend) }
113    }
114}
115
116/// Finalized patch metadata for validation and linker rebasing.
117#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct PatchCatalog {
119    arch: Arch,
120    blocks: SmallVec<[PatchBlock; 4]>,
121    sites: SmallVec<[PatchSite; 8]>,
122}
123
124impl PatchCatalog {
125    pub(crate) fn with_parts(
126        arch: Arch,
127        blocks: SmallVec<[PatchBlock; 4]>,
128        sites: SmallVec<[PatchSite; 8]>,
129    ) -> Self {
130        Self {
131            arch,
132            blocks,
133            sites,
134        }
135    }
136
137    pub fn arch(&self) -> Arch {
138        self.arch
139    }
140
141    pub fn is_empty(&self) -> bool {
142        self.blocks.is_empty() && self.sites.is_empty()
143    }
144
145    pub fn blocks(&self) -> &[PatchBlock] {
146        &self.blocks
147    }
148
149    pub fn sites(&self) -> &[PatchSite] {
150        &self.sites
151    }
152
153    pub fn block(&self, id: PatchBlockId) -> Option<&PatchBlock> {
154        self.blocks.get(id.index())
155    }
156
157    pub fn site(&self, id: PatchSiteId) -> Option<&PatchSite> {
158        self.sites.get(id.index())
159    }
160
161    pub fn site_mut(&mut self, id: PatchSiteId) -> Option<&mut PatchSite> {
162        self.sites.get_mut(id.index())
163    }
164}
165
166/// Self-describing handle for a patchable displacement (jump/call).
167#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
168pub struct PatchableSite {
169    offset: CodeOffset,
170    kind: LabelUse,
171    addend: i64,
172}
173
174impl PatchableSite {
175    /// Construct a site handle manually.
176    ///
177    /// # Safety
178    ///
179    /// `offset` in any image you later patch must be a valid displacement field of `kind`
180    /// with the same layout as when the site was emitted or recorded.
181    pub const unsafe fn new(offset: CodeOffset, kind: LabelUse, addend: i64) -> Self {
182        Self {
183            offset,
184            kind,
185            addend,
186        }
187    }
188
189    pub const fn offset(self) -> CodeOffset {
190        self.offset
191    }
192
193    pub const fn kind(self) -> LabelUse {
194        self.kind
195    }
196
197    pub const fn addend(self) -> i64 {
198        self.addend
199    }
200
201    /// Retarget this site in a mutable code image.
202    ///
203    /// # Safety
204    ///
205    /// `bytes` must be the code image this site was recorded against (same layout). The caller
206    /// synchronizes concurrent execution of the patched region.
207    pub unsafe fn retarget(
208        self,
209        bytes: &mut [u8],
210        target_offset: CodeOffset,
211    ) -> Result<(), AsmError> {
212        if !self.kind.can_reach(self.offset, target_offset) {
213            return Err(AsmError::TooLarge);
214        }
215        let patch_size = self.kind.patch_size();
216        let patch_end = (self.offset as usize)
217            .checked_add(patch_size)
218            .ok_or(AsmError::InvalidState)?;
219        if patch_end > bytes.len() {
220            return Err(AsmError::InvalidState);
221        }
222        let patch_slice = &mut bytes[self.offset as usize..patch_end];
223        self.kind
224            .patch_with_addend(patch_slice, self.offset, target_offset, self.addend);
225        Ok(())
226    }
227
228    /// Retarget this site in executable memory.
229    ///
230    /// # Safety
231    ///
232    /// `span` must be the loaded image this site was recorded against. The caller synchronizes
233    /// concurrent execution of the patched region.
234    #[cfg(feature = "jit")]
235    pub unsafe fn retarget_span(
236        self,
237        jit_allocator: &mut JitAllocator,
238        span: &mut Span,
239        target_offset: CodeOffset,
240    ) -> Result<(), AsmError> {
241        if !self.kind.can_reach(self.offset, target_offset) {
242            return Err(AsmError::TooLarge);
243        }
244        let patch_size = self.kind.patch_size();
245        let patch_end = (self.offset as usize)
246            .checked_add(patch_size)
247            .ok_or(AsmError::InvalidState)?;
248        if patch_end > span.size() {
249            return Err(AsmError::InvalidState);
250        }
251
252        unsafe {
253            jit_allocator.write(span, |span| {
254                let patch_ptr = span.rw().add(self.offset as usize);
255                let patch_slice = core::slice::from_raw_parts_mut(patch_ptr, patch_size);
256                self.kind.patch_with_addend(
257                    patch_slice,
258                    self.offset,
259                    target_offset,
260                    self.addend,
261                );
262            })?;
263        }
264        Ok(())
265    }
266}
267
268/// Self-describing handle for a rewritable code region (immediate or custom block).
269#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
270pub struct PatchableBlock {
271    offset: CodeOffset,
272    size: CodeOffset,
273    arch: Arch,
274}
275
276impl PatchableBlock {
277    /// Construct a block handle manually.
278    ///
279    /// # Safety
280    ///
281    /// `offset..offset+size` in any image you later patch must be a reserved patch region for
282    /// `arch` (instruction alignment and nop-fill rules apply).
283    pub const unsafe fn new(offset: CodeOffset, size: CodeOffset, arch: Arch) -> Self {
284        Self {
285            offset,
286            size,
287            arch,
288        }
289    }
290
291    pub const fn offset(self) -> CodeOffset {
292        self.offset
293    }
294
295    pub const fn size(self) -> CodeOffset {
296        self.size
297    }
298
299    pub const fn arch(self) -> Arch {
300        self.arch
301    }
302
303    /// Overwrite this block; shorter payloads are padded with architecture nops.
304    ///
305    /// # Safety
306    ///
307    /// `bytes` must be the code image this block was recorded against. The caller synchronizes
308    /// concurrent execution of the patched region.
309    pub unsafe fn rewrite(self, bytes: &mut [u8], new_bytes: &[u8]) -> Result<(), AsmError> {
310        if new_bytes.len() > self.size as usize {
311            return Err(AsmError::TooLarge);
312        }
313        let instruction_alignment = minimum_patch_alignment(self.arch) as usize;
314        if new_bytes.len() % instruction_alignment != 0 {
315            return Err(AsmError::InvalidArgument);
316        }
317        let block_end = (self.offset as usize)
318            .checked_add(self.size as usize)
319            .ok_or(AsmError::InvalidState)?;
320        if block_end > bytes.len() {
321            return Err(AsmError::InvalidState);
322        }
323
324        let block = &mut bytes[self.offset as usize..block_end];
325        block[..new_bytes.len()].copy_from_slice(new_bytes);
326        fill_with_nops(self.arch, &mut block[new_bytes.len()..])?;
327        Ok(())
328    }
329
330    /// Write a little-endian `u32` into a 4-byte block (x86 `patchable_mov` on `Gp32`, etc.).
331    ///
332    /// # Safety
333    ///
334    /// See [`rewrite`](Self::rewrite).
335    pub unsafe fn repatch_u32(self, bytes: &mut [u8], value: u32) -> Result<(), AsmError> {
336        if self.size != 4 {
337            return Err(AsmError::InvalidArgument);
338        }
339        unsafe { self.rewrite(bytes, &value.to_le_bytes()) }
340    }
341
342    /// Write a little-endian `u64` into an 8-byte block (x86 `patchable_mov` on `Gp64`, etc.).
343    ///
344    /// # Safety
345    ///
346    /// See [`rewrite`](Self::rewrite).
347    pub unsafe fn repatch_u64(self, bytes: &mut [u8], value: u64) -> Result<(), AsmError> {
348        if self.size != 8 {
349            return Err(AsmError::InvalidArgument);
350        }
351        unsafe { self.rewrite(bytes, &value.to_le_bytes()) }
352    }
353
354    /// Overwrite this block in executable memory.
355    ///
356    /// # Safety
357    ///
358    /// `span` must be the loaded image this block was recorded against.
359    #[cfg(feature = "jit")]
360    pub unsafe fn rewrite_span(
361        self,
362        jit_allocator: &mut JitAllocator,
363        span: &mut Span,
364        new_bytes: &[u8],
365    ) -> Result<(), AsmError> {
366        if new_bytes.len() > self.size as usize {
367            return Err(AsmError::TooLarge);
368        }
369        let instruction_alignment = minimum_patch_alignment(self.arch) as usize;
370        if new_bytes.len() % instruction_alignment != 0 {
371            return Err(AsmError::InvalidArgument);
372        }
373        let block_end = (self.offset as usize)
374            .checked_add(self.size as usize)
375            .ok_or(AsmError::InvalidState)?;
376        if block_end > span.size() {
377            return Err(AsmError::InvalidState);
378        }
379
380        let mut fill_result = Ok(());
381        unsafe {
382            jit_allocator.write(span, |span| {
383                let block_ptr = span.rw().add(self.offset as usize);
384                block_ptr.copy_from_nonoverlapping(new_bytes.as_ptr(), new_bytes.len());
385                let tail = core::slice::from_raw_parts_mut(
386                    block_ptr.add(new_bytes.len()),
387                    self.size as usize - new_bytes.len(),
388                );
389                fill_result = fill_with_nops(self.arch, tail);
390            })?;
391        }
392        fill_result
393    }
394
395    /// Write a little-endian `u32` into a 4-byte block in executable memory.
396    ///
397    /// # Safety
398    ///
399    /// See [`rewrite_span`](Self::rewrite_span).
400    #[cfg(feature = "jit")]
401    pub unsafe fn repatch_u32_span(
402        self,
403        jit_allocator: &mut JitAllocator,
404        span: &mut Span,
405        value: u32,
406    ) -> Result<(), AsmError> {
407        if self.size != 4 {
408            return Err(AsmError::InvalidArgument);
409        }
410        unsafe { self.rewrite_span(jit_allocator, span, &value.to_le_bytes()) }
411    }
412
413    /// Write a little-endian `u64` into an 8-byte block in executable memory.
414    ///
415    /// # Safety
416    ///
417    /// See [`rewrite_span`](Self::rewrite_span).
418    #[cfg(feature = "jit")]
419    pub unsafe fn repatch_u64_span(
420        self,
421        jit_allocator: &mut JitAllocator,
422        span: &mut Span,
423        value: u64,
424    ) -> Result<(), AsmError> {
425        if self.size != 8 {
426            return Err(AsmError::InvalidArgument);
427        }
428        unsafe { self.rewrite_span(jit_allocator, span, &value.to_le_bytes()) }
429    }
430}
431
432pub fn minimum_patch_alignment(arch: Arch) -> CodeOffset {
433    match arch {
434        Arch::AArch64 | Arch::RISCV32 | Arch::RISCV64 => 4,
435        _ => 1,
436    }
437}
438
439pub fn fill_with_nops(arch: Arch, buffer: &mut [u8]) -> Result<(), AsmError> {
440    let pattern: &[u8] = match arch {
441        Arch::X86 | Arch::X64 => &[0x90],
442        Arch::AArch64 => &[0x1f, 0x20, 0x03, 0xd5],
443        Arch::RISCV32 | Arch::RISCV64 => &[0x13, 0x00, 0x00, 0x00],
444        _ => return Err(AsmError::InvalidArgument),
445    };
446
447    if pattern.len() > 1 && buffer.len() % pattern.len() != 0 {
448        return Err(AsmError::InvalidArgument);
449    }
450
451    for chunk in buffer.chunks_mut(pattern.len()) {
452        chunk.copy_from_slice(pattern);
453    }
454
455    Ok(())
456}
457
458impl CodeBufferFinalized {
459    pub fn patch_catalog(&self) -> &PatchCatalog {
460        &self.patch_catalog
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn retarget_rejects_out_of_range_slice() {
470        let site = unsafe { PatchableSite::new(62, LabelUse::X86JmpRel32, 0) };
471        let mut bytes = [0u8; 64];
472        assert_eq!(
473            unsafe { site.retarget(&mut bytes, 0) }.unwrap_err(),
474            AsmError::InvalidState
475        );
476    }
477
478    #[test]
479    fn rewrite_rejects_misaligned_payload_for_a64() {
480        let block = unsafe { PatchableBlock::new(0, 4, Arch::AArch64) };
481        let mut bytes = [0u8; 4];
482        assert_eq!(
483            unsafe { block.rewrite(&mut bytes, &[0]) }.unwrap_err(),
484            AsmError::InvalidArgument
485        );
486    }
487
488    #[test]
489    fn repatch_u32_round_trips() {
490        let block = unsafe { PatchableBlock::new(1, 4, Arch::X64) };
491        let mut bytes = [0xB8, 0, 0, 0, 0];
492        unsafe { block.repatch_u32(&mut bytes, 0x11223344).unwrap() };
493        assert_eq!(&bytes[1..], &[0x44, 0x33, 0x22, 0x11]);
494    }
495
496    #[cfg(feature = "jit")]
497    #[test]
498    fn span_patch_rejects_ranges_outside_span() {
499        use crate::core::jit_allocator::JitAllocatorOptions;
500
501        let mut allocator = JitAllocator::new(JitAllocatorOptions::default());
502        let mut span = allocator.alloc(64).unwrap();
503        let span_size = span.size() as CodeOffset;
504
505        let block = unsafe { PatchableBlock::new(span_size, 1, Arch::X64) };
506        assert_eq!(
507            unsafe { block.rewrite_span(&mut allocator, &mut span, &[0x90]) }.unwrap_err(),
508            AsmError::InvalidState
509        );
510
511        let site = unsafe { PatchableSite::new(span_size - 3, LabelUse::X86JmpRel32, 0) };
512        assert_eq!(
513            unsafe { site.retarget_span(&mut allocator, &mut span, 0) }.unwrap_err(),
514            AsmError::InvalidState
515        );
516    }
517}