Skip to main content

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}