monocoque_core/alloc.rs
1//! Allocation primitives for Monocoque
2//!
3//! This module is the ONLY place where unsafe memory manipulation is allowed.
4//! All invariants are enforced here so the rest of the system can remain 100% safe.
5
6#![allow(unsafe_code)]
7
8use bytes::Bytes;
9use compio_buf::{IoBufMut, SetBufInit};
10use std::alloc::{Layout, alloc, dealloc};
11use std::ptr::NonNull;
12use std::sync::Arc;
13
14/// Size of one slab page.
15/// Tuned for cache locality and amortized allocation cost.
16pub const PAGE_SIZE: usize = 64 * 1024;
17
18/// Cache-line alignment to avoid false sharing.
19pub const PAGE_ALIGN: usize = 128;
20
21/// A slab page: a pinned chunk of memory.
22///
23/// Invariant:
24/// - Memory is allocated once and never moved.
25/// - Freed only when the last `Arc<Page>` is dropped.
26struct Page {
27 ptr: NonNull<u8>,
28 /// Actual allocation size in bytes. Equal to `PAGE_SIZE` for normal pages;
29 /// larger when a single request exceeds `PAGE_SIZE` (dedicated oversized page).
30 size: usize,
31}
32
33unsafe impl Send for Page {}
34unsafe impl Sync for Page {}
35
36impl Drop for Page {
37 fn drop(&mut self) {
38 unsafe {
39 let layout = Layout::from_size_align_unchecked(self.size, PAGE_ALIGN);
40 dealloc(self.ptr.as_ptr(), layout);
41 }
42 }
43}
44
45/// Owner passed into `Bytes::from_owner`
46///
47/// This guarantees:
48/// - The backing slab page stays alive as long as any Bytes exists.
49/// - No aliasing mutable access occurs after freeze.
50struct PageOwner {
51 page: Arc<Page>,
52}
53
54impl AsRef<[u8]> for PageOwner {
55 fn as_ref(&self) -> &[u8] {
56 unsafe { std::slice::from_raw_parts(self.page.ptr.as_ptr(), self.page.size) }
57 }
58}
59
60/// Mutable slab slice used ONLY during IO.
61///
62/// This type:
63/// - Implements `IoBufMut` so compio can safely DMA into it.
64/// - Is never exposed to user code.
65/// - Is frozen into immutable `Bytes` after IO completes.
66pub struct SlabMut {
67 page: Arc<Page>,
68 ptr: NonNull<u8>,
69 cap: usize,
70 len: usize,
71}
72
73unsafe impl Send for SlabMut {}
74unsafe impl Sync for SlabMut {}
75
76// SAFETY: SlabMut upholds IoBuf invariants:
77// - as_buf_ptr() returns a valid pointer to initialized memory region
78// - The memory region [ptr, ptr + len) contains initialized data
79// - The buffer is pinned and stable during IO operations
80unsafe impl compio_buf::IoBuf for SlabMut {
81 #[inline]
82 fn as_buf_ptr(&self) -> *const u8 {
83 self.ptr.as_ptr()
84 }
85
86 #[inline]
87 fn buf_len(&self) -> usize {
88 self.len
89 }
90
91 #[inline]
92 fn buf_capacity(&self) -> usize {
93 self.cap
94 }
95}
96
97// SAFETY: SlabMut upholds IoBufMut invariants:
98// - as_buf_mut_ptr() returns a valid pointer to a writable memory region
99// - The memory region [ptr, ptr + cap) is exclusively owned
100// - The buffer is pinned and stable during IO operations
101// - set_buf_init correctly updates the initialized length
102unsafe impl IoBufMut for SlabMut {
103 #[inline]
104 fn as_buf_mut_ptr(&mut self) -> *mut u8 {
105 self.ptr.as_ptr()
106 }
107}
108
109impl SetBufInit for SlabMut {
110 #[inline]
111 unsafe fn set_buf_init(&mut self, len: usize) {
112 debug_assert!(len <= self.cap);
113 self.len = len;
114 }
115}
116
117impl SlabMut {
118 /// Freeze this slab into immutable `Bytes`.
119 ///
120 /// # Safety invariants enforced here:
121 /// - `ptr` is inside `page`
122 /// - `len <= cap`
123 /// - After this call, no mutable access exists
124 #[must_use]
125 pub fn freeze(self) -> Bytes {
126 let base = self.page.ptr.as_ptr();
127 let offset = unsafe { self.ptr.as_ptr().offset_from(base) } as usize;
128
129 debug_assert!(offset + self.len <= self.page.size);
130
131 let owner = PageOwner { page: self.page };
132
133 // Create a Bytes covering the whole page, then slice.
134 let full = Bytes::from_owner(owner);
135 full.slice(offset..offset + self.len)
136 }
137}
138
139/// Arena used by the IO thread.
140///
141/// Not thread-safe by design.
142/// One arena per socket actor.
143pub struct IoArena {
144 current: Option<Arc<Page>>,
145 /// Usable size of `current` in bytes.
146 current_page_size: usize,
147 offset: usize,
148}
149
150impl Default for IoArena {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156impl IoArena {
157 #[must_use]
158 pub const fn new() -> Self {
159 Self {
160 current: None,
161 current_page_size: PAGE_SIZE,
162 offset: PAGE_SIZE, // force alloc on first use
163 }
164 }
165
166 /// Allocate a mutable buffer suitable for a single IO read.
167 ///
168 /// For `size <= PAGE_SIZE` multiple allocations share the same slab page
169 /// (bump-pointer, zero-overhead). For `size > PAGE_SIZE` a dedicated page
170 /// of exactly `size` bytes is allocated; subsequent small allocations start
171 /// a fresh normal page. No artificial ceiling on `size`.
172 ///
173 /// This guarantees:
174 /// - Stable memory address
175 /// - No reallocation
176 /// - No aliasing with other `SlabMut`
177 pub fn alloc_mut(&mut self, size: usize) -> SlabMut {
178 // For requests that exceed a normal page, allocate a dedicated page of
179 // the exact required size. This never applies in the common case
180 // (read_buffer_size <= PAGE_SIZE), so the branch is always-not-taken
181 // on the hot path and costs nothing in practice.
182 let page_size = size.max(PAGE_SIZE);
183
184 if self.current.is_none() || self.offset + size > self.current_page_size {
185 self.alloc_page(page_size);
186 }
187
188 let page = self.current.as_ref().unwrap().clone();
189
190 let ptr = unsafe { NonNull::new_unchecked(page.ptr.as_ptr().add(self.offset)) };
191
192 self.offset += size;
193
194 SlabMut {
195 page,
196 ptr,
197 cap: size,
198 len: 0,
199 }
200 }
201
202 #[inline(never)]
203 fn alloc_page(&mut self, page_size: usize) {
204 unsafe {
205 let layout = Layout::from_size_align_unchecked(page_size, PAGE_ALIGN);
206 let ptr = alloc(layout);
207 if ptr.is_null() {
208 std::alloc::handle_alloc_error(layout);
209 }
210
211 self.current = Some(Arc::new(Page {
212 ptr: NonNull::new_unchecked(ptr),
213 size: page_size,
214 }));
215 self.current_page_size = page_size;
216 self.offset = 0;
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224 use compio_buf::IoBuf;
225
226 #[test]
227 fn oversized_alloc_does_not_panic_or_corrupt() {
228 // Regression test: read_buffer_size > PAGE_SIZE used to cause a
229 // buffer overflow (io_uring writes past the page) and a panic in
230 // freeze() with "range end out of bounds".
231 let mut arena = IoArena::new();
232 let size = PAGE_SIZE + 1024;
233 let mut slab = arena.alloc_mut(size);
234 assert_eq!(slab.buf_capacity(), size);
235 // Simulate io_uring reporting `size` bytes written.
236 unsafe { slab.set_buf_init(size) };
237 let bytes = slab.freeze();
238 assert_eq!(bytes.len(), size);
239 }
240
241 #[test]
242 fn normal_allocs_still_share_a_page() {
243 // Two small allocations must come from the same backing page
244 // (bump-pointer, no extra syscall).
245 let mut arena = IoArena::new();
246 let slab1 = arena.alloc_mut(4096);
247 let slab2 = arena.alloc_mut(4096);
248 assert!(std::ptr::eq(
249 Arc::as_ptr(&slab1.page),
250 Arc::as_ptr(&slab2.page),
251 ));
252 }
253
254 #[test]
255 fn oversized_alloc_followed_by_normal_gets_fresh_page() {
256 // After a dedicated oversized page, the next normal allocation must
257 // not share that oversized page (offset would overflow it).
258 let mut arena = IoArena::new();
259 let large = arena.alloc_mut(PAGE_SIZE + 512);
260 let small = arena.alloc_mut(512);
261 assert!(!std::ptr::eq(
262 Arc::as_ptr(&large.page),
263 Arc::as_ptr(&small.page),
264 ));
265 }
266}