Skip to main content

juicebox_asm/
rt.rs

1// SPDX-License-Identifier: MIT
2//
3// Copyright (c) 2023, Johannes Stoelp <dev@memzero.de>
4
5//! Simple `mmap`ed runtime.
6//!
7//! This runtime supports adding code to executable pages and turn the added code into user
8//! specified function pointer.
9
10#[cfg(not(target_os = "linux"))]
11compile_error!("This runtime is only supported on linux");
12
13mod perf {
14    use std::fs;
15    use std::io::Write;
16
17    /// Provide support for the simple [perf jit interface][perf-jit].
18    ///
19    /// This allows a simple (static) jit runtime to generate meta data describing the generated
20    /// functions, which is used during post-processing by `perf report` to symbolize addresses
21    /// captured while executing jitted code.
22    ///
23    /// By the nature of this format, this can not be used for dynamic jit runtimes, which reuses
24    /// memory which previously contained jitted code.
25    ///
26    /// [perf-jit]: https://elixir.bootlin.com/linux/v6.6.6/source/tools/perf/Documentation/jit-interface.txt
27    pub(super) struct PerfMap {
28        file: std::fs::File,
29    }
30
31    impl PerfMap {
32        /// Create an empty perf map file.
33        pub(super) fn new() -> Self {
34            let name = format!("/tmp/perf-{}.map", unsafe { libc::getpid() });
35            let file = fs::OpenOptions::new()
36                .truncate(true)
37                .create(true)
38                .write(true)
39                .open(&name)
40                .unwrap_or_else(|_| panic!("Failed to open perf map file {}", name));
41
42            PerfMap { file }
43        }
44
45        /// Add an entry to the perf map file.
46        pub(super) fn add_entry(&mut self, start: usize, len: usize) {
47            // Each line has the following format, fields separated with spaces:
48            //   START SIZE NAME
49            //
50            // START and SIZE are hex numbers without 0x.
51            // NAME is the rest of the line, so it could contain special characters.
52            writeln!(self.file, "{:x} {:x} jitfn_{:x}", start, len, start)
53                .expect("Failed to write PerfMap entry");
54        }
55    }
56}
57
58/// A simple `mmap`ed runtime with executable pages.
59pub struct Runtime {
60    buf: *mut u8,
61    len: usize,
62    idx: usize,
63    perf: Option<perf::PerfMap>,
64}
65
66impl Runtime {
67    /// Create a new [Runtime] with a code buffer of 1 page.
68    ///
69    /// # Panics
70    ///
71    /// Panics if the `mmap` call fails.
72    pub fn new() -> Runtime {
73        Runtime::with_capacity(1)
74    }
75
76    /// Create a new [Runtime] with a code buffer of NPAGES pages.
77    ///
78    /// # Panics
79    ///
80    /// Panics if the length calculation overflows or the `mmap` call fails.
81    pub fn with_capacity(npages: usize) -> Runtime {
82        // Allocate a single page.
83        let len = npages.checked_mul(4096).unwrap();
84        let buf = unsafe {
85            libc::mmap(
86                std::ptr::null_mut(),
87                len,
88                libc::PROT_NONE,
89                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
90                0, /* fd */
91                0, /* off */
92            ) as *mut u8
93        };
94        assert_ne!(
95            buf.cast(),
96            libc::MAP_FAILED,
97            "Failed to mmap runtime code page"
98        );
99
100        Runtime {
101            buf,
102            len,
103            idx: 0,
104            perf: None,
105        }
106    }
107
108    /// Create a new [Runtime] which also generates static perf metat data.
109    ///
110    /// For each function added to the [Runtime], an entry will be generated in the
111    /// `/tmp/perf-<PID>.map` file, which `perf report` uses to symbolicate unknown addresses.
112    /// This is applicable for static runtimes only.
113    ///
114    /// # Panics
115    ///
116    /// Panics if the `mmap` call fails.
117    pub fn with_profile() -> Runtime {
118        let mut rt = Runtime::new();
119        rt.perf = Some(perf::PerfMap::new());
120        rt
121    }
122
123    /// Add the block of `code` to the runtime and a get function pointer of type `F`.
124    ///
125    /// # Panics
126    ///
127    /// Panics if the `code` does not fit on the `mmap`ed pages or is empty.
128    ///
129    /// # Safety
130    ///
131    /// The code added must fulfill the ABI of the specified function `F` and the returned function
132    /// pointer is only valid until the [`Runtime`] is dropped.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// let mut rt = juicebox_asm::Runtime::new();
138    ///
139    /// let code = [ 0x90 /* nop */, 0xc3 /* ret */ ];
140    /// let nop = unsafe { rt.add_code::<extern "C" fn()>(&code) };
141    ///
142    /// nop();
143    /// ```
144    pub unsafe fn add_code<F>(&mut self, code: impl AsRef<[u8]>) -> F {
145        // Get pointer to start of next free byte.
146        assert!(self.idx < self.len, "Runtime code page full");
147        let fn_start = self.buf.add(self.idx);
148
149        // Copy over code.
150        let code = code.as_ref();
151        assert!(!code.is_empty(), "Adding empty code not supported");
152        assert!(
153            code.len() <= (self.len - self.idx),
154            "Code does not fit on the runtime code page"
155        );
156        self.unprotect();
157        unsafe { std::ptr::copy_nonoverlapping(code.as_ptr(), fn_start, code.len()) };
158        self.protect();
159
160        // Increment index to next free byte.
161        self.idx += code.len();
162
163        // Add perf map entry.
164        if let Some(map) = &mut self.perf {
165            map.add_entry(fn_start as usize, code.len());
166        }
167
168        // Return function to newly added code.
169        unsafe { Self::as_fn::<F>(fn_start) }
170    }
171
172    /// Disassemble the code currently added to the runtime, using
173    /// [`ndisasm`](https://nasm.us/index.php) and print it to _stdout_. If
174    /// `ndisasm` is not available on the system this prints a warning and
175    /// becomes a nop.
176    ///
177    /// # Panics
178    ///
179    /// Panics if anything goes wrong with spawning, writing to or reading from
180    /// the `ndisasm` child process.
181    pub fn disasm(&self) {
182        assert!(self.idx <= self.len);
183        crate::disasm::disasm(unsafe { core::slice::from_raw_parts(self.buf, self.idx) });
184    }
185
186    /// Reinterpret the block of code pointed to by `fn_start` as `F`.
187    #[inline]
188    unsafe fn as_fn<F>(fn_start: *mut u8) -> F {
189        unsafe { std::mem::transmute_copy(&fn_start) }
190    }
191
192    /// Add write protection the underlying code page(s).
193    ///
194    /// # Panics
195    ///
196    /// Panics if the `mprotect` call fails.
197    fn protect(&mut self) {
198        unsafe {
199            // Remove write permissions from code page and allow to read-execute from it.
200            let ret = libc::mprotect(self.buf.cast(), self.len, libc::PROT_READ | libc::PROT_EXEC);
201            assert_eq!(ret, 0, "Failed to RX mprotect runtime code page");
202        }
203    }
204
205    /// Remove write protection the underlying code page(s).
206    ///
207    /// # Panics
208    ///
209    /// Panics if the `mprotect` call fails.
210    fn unprotect(&mut self) {
211        unsafe {
212            // Add write permissions to code page.
213            let ret = libc::mprotect(self.buf.cast(), self.len, libc::PROT_WRITE);
214            assert_eq!(ret, 0, "Failed to W mprotect runtime code page");
215        }
216    }
217}
218
219impl Drop for Runtime {
220    /// Unmaps the code page. This invalidates all the function pointer returned by
221    /// [`Runtime::add_code`].
222    fn drop(&mut self) {
223        unsafe {
224            let ret = libc::munmap(self.buf.cast(), self.len);
225            assert_eq!(ret, 0, "Failed to munmap runtime");
226        }
227    }
228}
229
230#[cfg(test)]
231mod test {
232    use super::*;
233
234    #[test]
235    fn test_code_max_size() {
236        let mut rt = Runtime::new();
237        let code = [0u8; 4096];
238        unsafe {
239            rt.add_code::<extern "C" fn()>(code);
240        }
241    }
242
243    #[test]
244    #[should_panic]
245    fn test_code_max_size_plus_1() {
246        let mut rt = Runtime::new();
247        let code = [0u8; 4097];
248        unsafe {
249            rt.add_code::<extern "C" fn()>(code);
250        }
251    }
252
253    #[test]
254    #[should_panic]
255    fn test_code_max_size_plus_1_2() {
256        let mut rt = Runtime::new();
257        let code = [0u8; 4096];
258        unsafe {
259            rt.add_code::<extern "C" fn()>(code);
260        }
261
262        let code = [0u8; 1];
263        unsafe {
264            rt.add_code::<extern "C" fn()>(code);
265        }
266    }
267
268    #[test]
269    fn test_capacity_code_max_size() {
270        let mut rt = Runtime::with_capacity(2);
271        let code = [0u8; 2 * 4096];
272        unsafe {
273            rt.add_code::<extern "C" fn()>(code);
274        }
275    }
276
277    #[test]
278    #[should_panic]
279    fn test_capacity_code_max_size_plus_1() {
280        let mut rt = Runtime::with_capacity(2);
281        let code = [0u8; 2 * 4096 + 1];
282        unsafe {
283            rt.add_code::<extern "C" fn()>(code);
284        }
285    }
286
287    #[test]
288    #[should_panic]
289    fn test_capacity_code_max_size_plus_1_2() {
290        let mut rt = Runtime::with_capacity(2);
291        let code = [0u8; 2 * 4096];
292        unsafe {
293            rt.add_code::<extern "C" fn()>(code);
294        }
295
296        let code = [0u8; 1];
297        unsafe {
298            rt.add_code::<extern "C" fn()>(code);
299        }
300    }
301
302    #[test]
303    #[should_panic]
304    fn test_empty_code() {
305        let mut rt = Runtime::new();
306        let code = [0u8; 0];
307        unsafe {
308            rt.add_code::<extern "C" fn()>(code);
309        }
310    }
311}