forge_alloc/hardening/
guard_page.rs1use core::cell::UnsafeCell;
16use core::ptr::NonNull;
17
18use forge_alloc_core::{
19 AllocError, Allocator, Deallocator, FixedRange, NonZeroLayout, OsBacked, ProtectFlags,
20};
21
22pub struct GuardPage<I: OsBacked> {
32 inner: I,
33 page_size: usize,
34 base: NonNull<u8>,
36 usable_size: usize,
38 cursor: UnsafeCell<usize>,
40}
41
42impl<I: OsBacked> GuardPage<I> {
43 pub fn new(inner: I, page_size: usize) -> Result<Self, AllocError> {
48 if page_size == 0 || !page_size.is_power_of_two() {
49 return Err(AllocError);
50 }
51 let region_size = inner.region_size();
52 let needed = 2usize
53 .checked_mul(page_size)
54 .and_then(|v| v.checked_add(1))
55 .ok_or(AllocError)?;
56 if region_size < needed {
57 return Err(AllocError);
58 }
59 let base_addr = inner.base_ptr().as_ptr() as usize;
60 if base_addr & (page_size - 1) != 0 {
61 return Err(AllocError);
62 }
63 if region_size & (page_size - 1) != 0 {
70 return Err(AllocError);
71 }
72
73 crate::backing::mmap_clear_last_os_error();
84 unsafe {
85 inner.protect(inner.base_ptr(), page_size, ProtectFlags::NONE);
86 }
87 if crate::backing::mmap_last_os_error().is_some() {
88 return Err(AllocError);
89 }
90 unsafe {
93 let tail = inner.base_ptr().as_ptr().add(region_size - page_size);
94 inner.protect(NonNull::new_unchecked(tail), page_size, ProtectFlags::NONE);
95 }
96 if crate::backing::mmap_last_os_error().is_some() {
97 return Err(AllocError);
98 }
99
100 let base = unsafe { NonNull::new_unchecked(inner.base_ptr().as_ptr().add(page_size)) };
102 let usable_size = region_size - 2 * page_size;
103
104 Ok(Self {
105 inner,
106 page_size,
107 base,
108 usable_size,
109 cursor: UnsafeCell::new(0),
110 })
111 }
112
113 #[inline]
115 pub fn inner(&self) -> &I {
116 &self.inner
117 }
118
119 #[inline]
121 pub fn page_size(&self) -> usize {
122 self.page_size
123 }
124
125 #[inline]
127 pub fn allocated(&self) -> usize {
128 unsafe { *self.cursor.get() }
130 }
131}
132
133unsafe impl<I: OsBacked> Deallocator for GuardPage<I> {
134 #[inline]
135 unsafe fn deallocate(&self, _ptr: NonNull<u8>, _layout: NonZeroLayout) {
136 }
138}
139
140unsafe impl<I: OsBacked> Allocator for GuardPage<I> {
141 fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
142 let align = layout.align().get();
143 let size = layout.size().get();
144 let base_addr = self.base.as_ptr() as usize;
145 unsafe {
147 let cursor_ptr = self.cursor.get();
148 let cur = *cursor_ptr;
149 let raw = base_addr.checked_add(cur).ok_or(AllocError)?;
150 let aligned = raw.checked_add(align - 1).ok_or(AllocError)? & !(align - 1);
151 let aligned_off = aligned - base_addr;
152 let end_off = aligned_off.checked_add(size).ok_or(AllocError)?;
153 if end_off > self.usable_size {
154 return Err(AllocError);
155 }
156 *cursor_ptr = end_off;
157 let p = self.base.as_ptr().add(aligned_off);
158 Ok(NonNull::slice_from_raw_parts(
159 NonNull::new_unchecked(p),
160 size,
161 ))
162 }
163 }
164
165 #[inline]
166 fn capacity_bytes(&self) -> Option<usize> {
167 Some(self.usable_size)
168 }
169
170 #[inline]
171 fn corruption_events(&self) -> u64 {
172 0
177 }
178
179 }
189
190impl<I: OsBacked> FixedRange for GuardPage<I> {
191 #[inline]
193 fn base(&self) -> NonNull<u8> {
194 self.base
195 }
196
197 #[inline]
199 fn size(&self) -> usize {
200 self.usable_size
201 }
202}
203
204unsafe impl<I: OsBacked + Send> Send for GuardPage<I> {}
206
207#[cfg(test)]
208#[cfg(feature = "std")]
209mod tests {
210 use super::*;
211 use crate::backing::{page_size, MmapBacked};
212
213 #[test]
214 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
215 fn construct_succeeds_for_large_region() {
216 let inner = MmapBacked::new(64 * 1024).unwrap();
217 let g = GuardPage::new(inner, page_size()).unwrap();
218 assert!(g.capacity_bytes().unwrap() >= 8 * 1024);
219 }
220
221 #[test]
222 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
223 fn construct_rejects_undersized_region() {
224 let inner = MmapBacked::new(4096).unwrap();
225 assert!(GuardPage::new(inner, page_size()).is_err());
226 }
227
228 #[test]
229 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
230 fn construct_rejects_non_power_of_two_page() {
231 let inner = MmapBacked::new(64 * 1024).unwrap();
232 assert!(GuardPage::new(inner, 3000).is_err());
233 }
234
235 #[test]
236 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
237 fn allocate_within_usable_doesnt_fault() {
238 let inner = MmapBacked::new(64 * 1024).unwrap();
239 let g = GuardPage::new(inner, page_size()).unwrap();
240 let layout = NonZeroLayout::from_size_align(256, 8).unwrap();
241 let block = g.allocate(layout).unwrap();
242 let p = block.cast::<u8>();
243 unsafe { core::ptr::write_bytes(p.as_ptr(), 0xAA, 256) };
244 }
245
246 #[test]
247 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
248 fn allocate_returns_ptr_past_head_guard() {
249 let inner = MmapBacked::new(64 * 1024).unwrap();
250 let inner_base = inner.base_ptr().as_ptr() as usize;
251 let g = GuardPage::new(inner, page_size()).unwrap();
252 let layout = NonZeroLayout::from_size_align(8, 8).unwrap();
253 let block = g.allocate(layout).unwrap();
254 let p_addr = block.cast::<u8>().as_ptr() as usize;
255 assert!(
256 p_addr >= inner_base + page_size(),
257 "allocation must sit beyond the head guard"
258 );
259 }
260
261 #[test]
262 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
263 fn allocate_rejects_oversized_request() {
264 let inner = MmapBacked::new(64 * 1024).unwrap();
265 let g = GuardPage::new(inner, page_size()).unwrap();
266 let huge = NonZeroLayout::from_size_align(64 * 1024, 1).unwrap();
267 assert!(g.allocate(huge).is_err());
268 }
269
270 #[test]
271 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / threads")]
272 fn fixed_range_excludes_guards() {
273 let inner = MmapBacked::new(64 * 1024).unwrap();
274 let inner_base = inner.base_ptr().as_ptr() as usize;
275 let inner_size = inner.region_size();
276 let g = GuardPage::new(inner, page_size()).unwrap();
277 assert_eq!(g.base().as_ptr() as usize, inner_base + page_size());
278 assert_eq!(g.size(), inner_size - 2 * page_size());
279 }
280
281 struct FailingProtect(MmapBacked);
287
288 unsafe impl Deallocator for FailingProtect {
289 unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: NonZeroLayout) {
290 unsafe { self.0.deallocate(ptr, layout) }
292 }
293 }
294 unsafe impl Allocator for FailingProtect {
295 fn allocate(&self, layout: NonZeroLayout) -> Result<NonNull<[u8]>, AllocError> {
296 self.0.allocate(layout)
297 }
298 }
299 unsafe impl OsBacked for FailingProtect {
300 fn base_ptr(&self) -> NonNull<u8> {
301 self.0.base_ptr()
302 }
303 fn region_size(&self) -> usize {
304 self.0.region_size()
305 }
306 unsafe fn release_pages(&self, ptr: NonNull<u8>, size: usize) {
307 unsafe { self.0.release_pages(ptr, size) }
309 }
310 unsafe fn protect(&self, _ptr: NonNull<u8>, _size: usize, _flags: ProtectFlags) {
311 let _ = std::fs::File::open("__forge_alloc_guard_install_should_fail__");
315 crate::backing::mmap_record_os_error();
316 }
317 }
318
319 #[test]
320 #[cfg_attr(miri, ignore = "miri-incompatible: mmap")]
321 fn construct_aborts_when_guard_protect_fails() {
322 let inner = FailingProtect(MmapBacked::new(64 * 1024).unwrap());
323 assert!(
327 GuardPage::new(inner, page_size()).is_err(),
328 "GuardPage::new must abort when a guard protect fails",
329 );
330 }
331
332 #[cfg(unix)]
338 mod trap {
339 use super::*;
340
341 unsafe fn child_faults_writing(addr: *mut u8) -> bool {
345 let pid = unsafe { libc::fork() };
348 assert!(pid >= 0, "fork failed");
349 if pid == 0 {
350 unsafe {
351 core::ptr::write_volatile(addr, 0xFFu8);
352 libc::_exit(0);
354 }
355 }
356 let mut status: libc::c_int = 0;
357 unsafe { libc::waitpid(pid, &mut status, 0) };
359 libc::WIFSIGNALED(status)
360 && (libc::WTERMSIG(status) == libc::SIGSEGV
361 || libc::WTERMSIG(status) == libc::SIGBUS)
362 }
363
364 #[test]
365 #[cfg_attr(miri, ignore = "miri-incompatible: mmap / fork / signals")]
366 fn head_and_tail_guards_trap_on_access() {
367 let inner = MmapBacked::new(64 * 1024).unwrap();
368 let inner_base = inner.base_ptr().as_ptr();
369 let region = inner.region_size();
370 let ps = page_size();
371 let g = GuardPage::new(inner, ps).unwrap();
372 unsafe {
373 let head = g.base().as_ptr().sub(1);
375 assert!(child_faults_writing(head), "head guard did not trap");
376 let tail = inner_base.add(region - ps);
378 assert!(child_faults_writing(tail), "tail guard did not trap");
379 }
380 }
381 }
382}