Skip to main content

asmkit/core/
patch.rs

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