1 use std::io;
2 use std::mem::ManuallyDrop;
3 use std::ptr;
4 
5 use cranelift_module::ModuleResult;
6 
7 use super::{BranchProtection, JITMemoryKind, JITMemoryProvider};
8 
align_up(addr: usize, align: usize) -> usize9 fn align_up(addr: usize, align: usize) -> usize {
10     debug_assert!(align.is_power_of_two());
11     (addr + align - 1) & !(align - 1)
12 }
13 
14 #[derive(Debug)]
15 struct Segment {
16     ptr: *mut u8,
17     len: usize,
18     position: usize,
19     target_prot: region::Protection,
20     finalized: bool,
21 }
22 
23 impl Segment {
new(ptr: *mut u8, len: usize, target_prot: region::Protection) -> Self24     fn new(ptr: *mut u8, len: usize, target_prot: region::Protection) -> Self {
25         // Segments are created on page boundaries.
26         debug_assert_eq!(ptr as usize % region::page::size(), 0);
27         debug_assert_eq!(len % region::page::size(), 0);
28         let mut segment = Segment {
29             ptr,
30             len,
31             target_prot,
32             position: 0,
33             finalized: false,
34         };
35         // Set segment to read-write for initialization. The target permissions
36         // will be applied in `finalize`.
37         segment.set_rw();
38         segment
39     }
40 
set_rw(&mut self)41     fn set_rw(&mut self) {
42         unsafe {
43             region::protect(self.ptr, self.len, region::Protection::READ_WRITE)
44                 .expect("unable to change memory protection for jit memory segment");
45         }
46     }
47 
finalize(&mut self, branch_protection: BranchProtection)48     fn finalize(&mut self, branch_protection: BranchProtection) {
49         if self.finalized {
50             return;
51         }
52 
53         // Executable regions are handled separately to correctly deal with
54         // branch protection and cache coherence.
55         if self.target_prot == region::Protection::READ_EXECUTE {
56             super::set_readable_and_executable(self.ptr, self.len, branch_protection)
57                 .expect("unable to set memory protection for jit memory segment");
58         } else {
59             unsafe {
60                 region::protect(self.ptr, self.len, self.target_prot)
61                     .expect("unable to change memory protection for jit memory segment");
62             }
63         }
64         self.finalized = true;
65     }
66 
67     // Note: We do pointer arithmetic on `ptr` passed to `Segment::new` here.
68     // This assumes that `ptr` is valid for `len` bytes, or will result in UB.
allocate(&mut self, size: usize, align: usize) -> *mut u869     fn allocate(&mut self, size: usize, align: usize) -> *mut u8 {
70         assert!(self.has_space_for(size, align));
71         self.position = align_up(self.position, align);
72         let ptr = unsafe { self.ptr.add(self.position) };
73         self.position += size;
74         ptr
75     }
76 
has_space_for(&self, size: usize, align: usize) -> bool77     fn has_space_for(&self, size: usize, align: usize) -> bool {
78         !self.finalized && align_up(self.position, align) + size <= self.len
79     }
80 }
81 
82 /// `ArenaMemoryProvider` allocates segments from a contiguous memory region
83 /// that is reserved up-front.
84 ///
85 /// The arena's memory is initially allocated with PROT_NONE and gradually
86 /// updated as the JIT requires more space. This approach allows for stable
87 /// addresses throughout the lifetime of the JIT.
88 ///
89 /// Depending on the underlying platform, requesting large parts of the address
90 /// space to be allocated might fail. This implementation currently doesn't do
91 /// overcommit on Windows.
92 ///
93 /// Note: Memory will be leaked by default unless
94 /// [`JITMemoryProvider::free_memory`] is called to ensure function pointers
95 /// remain valid for the remainder of the program's life.
96 pub struct ArenaMemoryProvider {
97     alloc: ManuallyDrop<Option<region::Allocation>>,
98     ptr: *mut u8,
99     size: usize,
100     position: usize,
101     segments: Vec<Segment>,
102 }
103 
104 unsafe impl Send for ArenaMemoryProvider {}
105 
106 impl ArenaMemoryProvider {
107     /// Create a new memory region with the given size.
new_with_size(reserve_size: usize) -> Result<Self, region::Error>108     pub fn new_with_size(reserve_size: usize) -> Result<Self, region::Error> {
109         let size = align_up(reserve_size, region::page::size());
110         // Note: The region crate uses `MEM_RESERVE | MEM_COMMIT` on Windows.
111         // This means that allocations that exceed the page file plus system
112         // memory will fail here.
113         // https://github.com/darfink/region-rs/pull/34
114         let mut alloc = region::alloc(size, region::Protection::NONE)?;
115         let ptr = alloc.as_mut_ptr();
116 
117         Ok(Self {
118             alloc: ManuallyDrop::new(Some(alloc)),
119             segments: Vec::new(),
120             ptr,
121             size,
122             position: 0,
123         })
124     }
125 
allocate_inner( &mut self, size: usize, align: u64, protection: region::Protection, ) -> io::Result<*mut u8>126     fn allocate_inner(
127         &mut self,
128         size: usize,
129         align: u64,
130         protection: region::Protection,
131     ) -> io::Result<*mut u8> {
132         let align = usize::try_from(align).expect("alignment too big");
133         assert!(
134             align <= region::page::size(),
135             "alignment over page size is not supported"
136         );
137 
138         // Note: Add a fast path without a linear scan over segments here?
139 
140         // Can we fit this allocation into an existing segment?
141         if let Some(segment) = self.segments.iter_mut().find(|seg| {
142             seg.target_prot == protection && !seg.finalized && seg.has_space_for(size, align)
143         }) {
144             return Ok(segment.allocate(size, align));
145         }
146 
147         // Can we resize the last segment?
148         if let Some(segment) = self.segments.iter_mut().last() {
149             if segment.target_prot == protection && !segment.finalized {
150                 let additional_size = align_up(size, region::page::size());
151 
152                 // If our reserved arena can fit the additional size, extend the
153                 // last segment.
154                 if self.position + additional_size <= self.size {
155                     segment.len += additional_size;
156                     segment.set_rw();
157                     self.position += additional_size;
158                     return Ok(segment.allocate(size, align));
159                 }
160             }
161         }
162 
163         // Allocate new segment for given size and alignment.
164         self.allocate_segment(size, protection)?;
165         let i = self.segments.len() - 1;
166         Ok(self.segments[i].allocate(size, align))
167     }
168 
allocate_segment( &mut self, size: usize, target_prot: region::Protection, ) -> Result<(), io::Error>169     fn allocate_segment(
170         &mut self,
171         size: usize,
172         target_prot: region::Protection,
173     ) -> Result<(), io::Error> {
174         let size = align_up(size, region::page::size());
175         let ptr = unsafe { self.ptr.add(self.position) };
176         if self.position + size > self.size {
177             return Err(io::Error::new(
178                 io::ErrorKind::Other,
179                 "pre-allocated jit memory region exhausted",
180             ));
181         }
182         self.position += size;
183         self.segments.push(Segment::new(ptr, size, target_prot));
184         Ok(())
185     }
186 
finalize(&mut self, branch_protection: BranchProtection)187     pub(crate) fn finalize(&mut self, branch_protection: BranchProtection) {
188         for segment in &mut self.segments {
189             segment.finalize(branch_protection);
190         }
191 
192         // Flush any in-flight instructions from the pipeline
193         wasmtime_jit_icache_coherence::pipeline_flush_mt().expect("Failed pipeline flush");
194     }
195 
196     /// Frees the allocated memory region, which would be leaked otherwise.
197     /// Likely to invalidate existing function pointers, causing unsafety.
free_memory(&mut self)198     pub(crate) unsafe fn free_memory(&mut self) {
199         if self.ptr == ptr::null_mut() {
200             return;
201         }
202         self.segments.clear();
203         // Drop the allocation, freeing memory.
204         let _: Option<region::Allocation> = self.alloc.take();
205         self.ptr = ptr::null_mut();
206     }
207 }
208 
209 impl Drop for ArenaMemoryProvider {
drop(&mut self)210     fn drop(&mut self) {
211         if self.ptr == ptr::null_mut() {
212             return;
213         }
214         let is_live = self.segments.iter().any(|seg| seg.finalized);
215         if !is_live {
216             // Only free memory if it's not been finalized yet.
217             // Otherwise, leak it since JIT memory may still be in use.
218             unsafe { self.free_memory() };
219         }
220     }
221 }
222 
223 impl JITMemoryProvider for ArenaMemoryProvider {
allocate(&mut self, size: usize, align: u64, kind: JITMemoryKind) -> io::Result<*mut u8>224     fn allocate(&mut self, size: usize, align: u64, kind: JITMemoryKind) -> io::Result<*mut u8> {
225         self.allocate_inner(
226             size,
227             align,
228             match kind {
229                 JITMemoryKind::Executable => region::Protection::READ_EXECUTE,
230                 JITMemoryKind::Writable => region::Protection::READ_WRITE,
231                 JITMemoryKind::ReadOnly => region::Protection::READ,
232             },
233         )
234     }
235 
free_memory(&mut self)236     unsafe fn free_memory(&mut self) {
237         self.free_memory();
238     }
239 
finalize(&mut self, branch_protection: BranchProtection) -> ModuleResult<()>240     fn finalize(&mut self, branch_protection: BranchProtection) -> ModuleResult<()> {
241         self.finalize(branch_protection);
242         Ok(())
243     }
244 }
245 
246 #[cfg(test)]
247 mod tests {
248     use super::*;
249 
250     #[test]
alignment_ok()251     fn alignment_ok() {
252         let mut arena = ArenaMemoryProvider::new_with_size(1 << 20).unwrap();
253 
254         for align_log2 in 0..8 {
255             let align = 1usize << align_log2;
256             for size in 1..128 {
257                 let ptr = arena
258                     .allocate(size, align as u64, JITMemoryKind::Writable)
259                     .unwrap();
260                 // assert!(ptr.is_aligned_to(align));
261                 assert_eq!(ptr.addr() % align, 0);
262             }
263         }
264     }
265 
266     #[test]
267     #[cfg(all(target_pointer_width = "64", not(target_os = "windows")))]
268     // Windows: See https://github.com/darfink/region-rs/pull/34
large_virtual_allocation()269     fn large_virtual_allocation() {
270         // We should be able to request 1TB of virtual address space on 64-bit
271         // platforms. Physical memory should be committed as we go.
272         let reserve_size = 1 << 40;
273         let mut arena = ArenaMemoryProvider::new_with_size(reserve_size).unwrap();
274         let ptr = arena.allocate(1, 1, JITMemoryKind::Writable).unwrap();
275         assert_eq!(ptr.addr(), arena.ptr.addr());
276         arena.finalize(BranchProtection::None);
277         unsafe { ptr.write_volatile(42) };
278         unsafe { arena.free_memory() };
279     }
280 
281     #[test]
over_capacity()282     fn over_capacity() {
283         let mut arena = ArenaMemoryProvider::new_with_size(1 << 20).unwrap(); // 1 MB
284 
285         let _ = arena.allocate(900_000, 1, JITMemoryKind::Writable).unwrap();
286         let _ = arena
287             .allocate(200_000, 1, JITMemoryKind::Writable)
288             .unwrap_err();
289     }
290 
291     #[test]
test_is_send()292     fn test_is_send() {
293         fn assert_is_send<T: Send>() {}
294         assert_is_send::<ArenaMemoryProvider>();
295     }
296 }
297