closure_ffi/
jit_alloc.rs

1//! Abstractions around allocators that provide dual-mapped memory with XOR protection rules (one RW
2//! view and one RX view) suitable for emitting code at runtime.
3//!
4//! Meant to be an abstraction over the `jit-allocator` crate's API so that it can be swapped with
5//! user-provided allocators.
6//!
7//! See the [`JitAlloc`] trait for more information.
8
9#[allow(unused_imports)]
10use core::ops::Deref;
11
12/// Anonymous error that may be returned by [`JitAlloc`] implementations when [`JitAlloc::alloc`] or
13/// [`JitAlloc::release`] fail.
14#[derive(Debug)]
15pub struct JitAllocError;
16
17/// Values to use with [`JitAlloc::protect_jit_memory`].
18#[derive(Debug, Clone, Copy, Eq, PartialEq)]
19pub enum ProtectJitAccess {
20    /// Protect JIT memory with Read+Write permissions.
21    ReadWrite = 0,
22    /// Protect JIT memory with Read+Execute permissions.
23    ReadExecute = 1,
24}
25
26/// Generic allocator providing virtual memory suitable for emitting code at runtime.
27///
28/// The API is meant to be a thin abstraction over the `jit-allocator` crate's API, to allow it
29/// to be swapped with other allocators.
30pub trait JitAlloc {
31    /// Allocates `size` bytes in the executable memory region.
32    /// Returns two pointers. One points to Read-Execute mapping and another to Read-Write mapping.
33    /// All code writes *must* go to the Read-Write mapping.
34    fn alloc(&self, size: usize) -> Result<(*const u8, *mut u8), JitAllocError>;
35
36    /// Releases the memory allocated by `alloc`.
37    ///
38    /// # Safety
39    /// - `rx_ptr` must have been returned from `alloc`
40    /// - `rx_ptr` must have been allocated from this allocator
41    /// - `rx_ptr` must not have been passed to `release` before
42    /// - `rx_ptr` must point to read-execute part of memory returned from `alloc`.
43    unsafe fn release(&self, rx_ptr: *const u8) -> Result<(), JitAllocError>;
44
45    /// On hardened architectures with `MAP_JIT`-like memory flags, set the access for the current
46    /// thread.
47    ///
48    /// This is expected to be a no-op on most platforms, but should be called before writing
49    /// or executing JIT memory.
50    ///
51    /// # Safety
52    ///
53    /// - `ptr` must point at least `size` bytes of readable memory.
54    unsafe fn protect_jit_memory(&self, ptr: *const u8, size: usize, access: ProtectJitAccess);
55
56    /// Flushes the instruction cache for (at least) the given slice of executable memory. Should be
57    /// called after the JIT memory is ready to be executed.
58    ///
59    /// On architectures with shared data/instruction caches, like x86_64, this is a no-op.
60    ///
61    /// # Safety
62    /// - `rx_ptr` must point at least `size` bytes of Read-Execute memory.
63    unsafe fn flush_instruction_cache(&self, rx_ptr: *const u8, size: usize);
64}
65
66impl<J: JitAlloc, D: Deref<Target = J>> JitAlloc for D {
67    fn alloc(&self, size: usize) -> Result<(*const u8, *mut u8), JitAllocError> {
68        (**self).alloc(size)
69    }
70
71    unsafe fn release(&self, rx_ptr: *const u8) -> Result<(), JitAllocError> {
72        (**self).release(rx_ptr)
73    }
74
75    #[inline(always)]
76    unsafe fn flush_instruction_cache(&self, rx_ptr: *const u8, size: usize) {
77        (**self).flush_instruction_cache(rx_ptr, size);
78    }
79
80    #[inline(always)]
81    unsafe fn protect_jit_memory(&self, ptr: *const u8, size: usize, access: ProtectJitAccess) {
82        (**self).protect_jit_memory(ptr, size, access);
83    }
84}
85
86#[cfg(feature = "global_jit_alloc")]
87/// The default, global JIT allocator.
88///
89/// When the `default_jit_alloc` feature is enabled, this is currently implemented as a ZST
90/// deferring to a static [`jit_allocator2::JitAllocator`] behind a [`std::sync::Mutex`] (or a
91/// [`spin::Mutex`] under `no_std`).
92///
93/// When the `default_jit_alloc` feature is not enabled, defers to a [`JitAlloc`] implementation
94/// provided by a downstream crate using the [`global_jit_alloc`] macro.
95#[derive(Default, Clone, Copy)]
96pub struct GlobalJitAlloc;
97
98#[cfg(feature = "default_jit_alloc")]
99mod default_jit_alloc {
100    use jit_allocator2::JitAllocator;
101
102    use super::*;
103
104    #[inline(always)]
105    fn convert_access(access: ProtectJitAccess) -> jit_allocator2::ProtectJitAccess {
106        match access {
107            ProtectJitAccess::ReadExecute => jit_allocator2::ProtectJitAccess::ReadExecute,
108            ProtectJitAccess::ReadWrite => jit_allocator2::ProtectJitAccess::ReadWrite,
109        }
110    }
111
112    fn flush_instruction_cache(rx_ptr: *const u8, size: usize) {
113        #[cfg(all(target_arch = "arm", target_os = "linux"))]
114        unsafe {
115            const __ARM_NR_CACHEFLUSH: i32 = 0x0f0002;
116            libc::syscall(__ARM_NR_CACHEFLUSH, rx_ptr, rx_ptr.byte_add(size), 0);
117            return;
118        }
119        #[allow(unreachable_code)]
120        jit_allocator2::flush_instruction_cache(rx_ptr, size);
121    }
122
123    #[cfg(not(feature = "std"))]
124    static GLOBAL_JIT_ALLOC: spin::Mutex<Option<alloc::boxed::Box<JitAllocator>>> =
125        spin::Mutex::new(None);
126    #[cfg(feature = "std")]
127    static GLOBAL_JIT_ALLOC: std::sync::Mutex<Option<alloc::boxed::Box<JitAllocator>>> =
128        std::sync::Mutex::new(None);
129
130    impl super::GlobalJitAlloc {
131        fn use_alloc<T>(&self, action: impl FnOnce(&mut JitAllocator) -> T) -> T {
132            #[cfg(not(feature = "std"))]
133            let mut maybe_alloc = GLOBAL_JIT_ALLOC.lock();
134            #[cfg(feature = "std")]
135            let mut maybe_alloc = GLOBAL_JIT_ALLOC.lock().unwrap();
136
137            let alloc = maybe_alloc.get_or_insert_with(|| JitAllocator::new(Default::default()));
138            action(alloc)
139        }
140    }
141
142    #[cfg_attr(docsrs, doc(cfg(feature = "global_jit_alloc")))]
143    impl JitAlloc for super::GlobalJitAlloc {
144        fn alloc(&self, size: usize) -> Result<(*const u8, *mut u8), JitAllocError> {
145            self.use_alloc(|a| a.alloc(size)).map_err(|_| JitAllocError)
146        }
147
148        unsafe fn release(&self, rx_ptr: *const u8) -> Result<(), JitAllocError> {
149            self.use_alloc(|a| a.release(rx_ptr)).map_err(|_| JitAllocError)
150        }
151
152        #[inline(always)]
153        unsafe fn flush_instruction_cache(&self, rx_ptr: *const u8, size: usize) {
154            flush_instruction_cache(rx_ptr, size);
155        }
156
157        #[inline(always)]
158        unsafe fn protect_jit_memory(
159            &self,
160            _ptr: *const u8,
161            _size: usize,
162            access: ProtectJitAccess,
163        ) {
164            jit_allocator2::protect_jit_memory(convert_access(access));
165        }
166    }
167
168    #[cfg(feature = "std")]
169    pub(super) mod thread_jit_alloc {
170        use core::{cell::UnsafeCell, marker::PhantomData};
171        use std::{boxed::Box, thread_local};
172
173        use jit_allocator2::JitAllocator;
174
175        #[allow(unused_imports)]
176        use super::*;
177
178        thread_local! {
179            static THREAD_JIT_ALLOC: UnsafeCell<Box<JitAllocator>> =
180                UnsafeCell::new(JitAllocator::new(Default::default()));
181        }
182
183        /// Marker type providing access to a thread-local JIT allocator.
184        ///
185        /// Unlike [`GlobalJitAlloc`], this allocator is neither [`Send`] nor [`Sync`].
186        #[derive(Default, Clone)]
187        pub struct ThreadJitAlloc(PhantomData<*mut ()>);
188
189        impl JitAlloc for ThreadJitAlloc {
190            fn alloc(&self, size: usize) -> Result<(*const u8, *mut u8), JitAllocError> {
191                THREAD_JIT_ALLOC
192                    .with(|a| unsafe { &mut *a.get() }.alloc(size))
193                    .map_err(|_| JitAllocError)
194            }
195
196            unsafe fn release(&self, rx_ptr: *const u8) -> Result<(), JitAllocError> {
197                THREAD_JIT_ALLOC
198                    .with(|a| unsafe { &mut *a.get() }.release(rx_ptr))
199                    .map_err(|_| JitAllocError)
200            }
201
202            #[inline(always)]
203            unsafe fn flush_instruction_cache(&self, rx_ptr: *const u8, size: usize) {
204                flush_instruction_cache(rx_ptr, size);
205            }
206
207            #[inline(always)]
208            unsafe fn protect_jit_memory(
209                &self,
210                _ptr: *const u8,
211                _size: usize,
212                access: ProtectJitAccess,
213            ) {
214                jit_allocator2::protect_jit_memory(convert_access(access));
215            }
216        }
217    }
218}
219#[cfg(all(feature = "default_jit_alloc", feature = "std"))]
220#[doc(inline)]
221pub use default_jit_alloc::thread_jit_alloc::ThreadJitAlloc;
222
223/// Defines a global [`JitAlloc`] implementation which [`GlobalJitAlloc`] will defer to.
224///
225/// The macro can either take a path to a static variable or an unsafe block resolving to a
226/// `&'static JitAlloc`:
227///
228/// ```ignore
229/// static GLOBAL_JIT: MyJitAlloc = MyJitAlloc::new();
230/// global_jit_alloc!(GLOBAL_JIT);
231/// ```
232///
233/// ```ignore
234/// use std::sync::OnceLock;
235///
236/// global_jit_alloc!(unsafe {
237///     static WRAPPED_JIT: OnceLock<MyJitAlloc> = OnceLock::new();
238///     WRAPPED_JIT.get_or_init(|| MyJitAlloc::new())
239/// });
240/// ```
241///
242/// The block form must be marked with `unsafe` as sometimes returning a different impl can lead to
243/// UB, and you are responsible to make sure this doesn't happen.
244#[macro_export]
245#[cfg(any(
246    docsrs,
247    all(feature = "global_jit_alloc", not(feature = "default_jit_alloc")),
248))]
249#[cfg_attr(
250    docsrs,
251    doc(cfg(all(feature = "global_jit_alloc", not(feature = "default_jit_alloc"))))
252)]
253macro_rules! global_jit_alloc {
254    (unsafe $provider:block) => {
255        #[no_mangle]
256        extern "Rust" fn _closure_ffi_3_global_jit_alloc(
257        ) -> &'static (dyn $crate::jit_alloc::JitAlloc + Sync) {
258            unsafe { $provider }
259        }
260    };
261    ($static_var:path) => {
262        #[no_mangle]
263        extern "Rust" fn _closure_ffi_3_global_jit_alloc(
264        ) -> &'static (dyn $crate::jit_alloc::JitAlloc + Sync) {
265            &$static_var
266        }
267    };
268}
269#[cfg(any(
270    docsrs,
271    all(feature = "global_jit_alloc", not(feature = "default_jit_alloc"))
272))]
273#[cfg_attr(
274    docsrs,
275    doc(cfg(all(feature = "global_jit_alloc", not(feature = "default_jit_alloc"))))
276)]
277pub use global_jit_alloc;
278
279#[cfg(all(feature = "global_jit_alloc", not(feature = "default_jit_alloc")))]
280mod custom_jit_alloc {
281    use super::{GlobalJitAlloc, JitAlloc, JitAllocError, ProtectJitAccess};
282
283    extern "Rust" {
284        fn _closure_ffi_3_global_jit_alloc() -> &'static (dyn JitAlloc + Sync);
285    }
286
287    fn get_global_jit_alloc() -> &'static dyn JitAlloc {
288        unsafe { _closure_ffi_3_global_jit_alloc() }
289    }
290
291    impl JitAlloc for GlobalJitAlloc {
292        fn alloc(&self, size: usize) -> Result<(*const u8, *mut u8), JitAllocError> {
293            get_global_jit_alloc().alloc(size)
294        }
295
296        unsafe fn release(&self, rx_ptr: *const u8) -> Result<(), JitAllocError> {
297            get_global_jit_alloc().release(rx_ptr)
298        }
299
300        unsafe fn flush_instruction_cache(&self, rx_ptr: *const u8, size: usize) {
301            get_global_jit_alloc().flush_instruction_cache(rx_ptr, size);
302        }
303
304        unsafe fn protect_jit_memory(&self, ptr: *const u8, size: usize, access: ProtectJitAccess) {
305            get_global_jit_alloc().protect_jit_memory(ptr, size, access);
306        }
307    }
308}