Skip to main content

scx_rustland_core/
alloc.rs

1// Copyright (c) Andrea Righi <andrea.righi@linux.dev>
2
3// Buddy allocator code imported from https://github.com/jjyr/buddy-alloc
4// and distributed under the terms of the MIT license.
5//
6// MIT License:
7//
8// Permission is hereby granted, free of charge, to any person obtaining a copy
9// of this software and associated documentation files (the "Software"), to deal
10// in the Software without restriction, including without limitation the rights
11// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12// copies of the Software, and to permit persons to whom the Software is
13// furnished to do so, subject to the following conditions:
14//
15// The above copyright notice and this permission notice shall be included in
16// all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24// THE SOFTWARE.
25
26// This software may be used and distributed according to the terms of the
27// GNU General Public License version 2.
28
29use seccomp::*;
30use std::alloc::{GlobalAlloc, Layout};
31use std::fs::File;
32use std::io::{BufRead, BufReader, Write};
33use std::sync::Mutex;
34use std::sync::atomic::{AtomicI32, Ordering};
35
36/// Buddy allocator
37///
38/// The following code is strongly based on <https://github.com/jjyr/buddy-alloc> and imported
39/// directly here to make packaging easier.
40
41const OOM_MSG: &str = "requires more memory space to initialize BuddyAlloc";
42const LEAF_ALIGN_ERROR_MSG: &str = "leaf size must be align to 16 bytes";
43/// required align to 16 bytes, since Node takes 16 bytes on 64-bits machine.
44pub const MIN_LEAF_SIZE_ALIGN: usize = 16;
45
46const fn block_size_2base(k: usize, leaf2base: usize) -> usize {
47    (1 << k) << leaf2base
48}
49
50const fn nblock(k: usize, entries_size: usize) -> usize {
51    1 << (entries_size - k - 1)
52}
53
54const fn roundup(n: usize, sz2base: usize) -> usize {
55    (((n - 1) >> sz2base) + 1) << sz2base
56}
57
58fn log2(mut n: usize) -> usize {
59    let mut k = 0;
60    while n > 1 {
61        k += 1;
62        n >>= 1;
63    }
64    k
65}
66
67fn bit_isset(bit_array: *const u8, i: usize) -> bool {
68    unsafe {
69        let b = bit_array.add(i >> 3);
70        let m = 1 << (i % 8);
71        *b & m == m
72    }
73}
74
75fn bit_set(bit_array: *mut u8, i: usize) {
76    unsafe {
77        let b = bit_array.add(i >> 3);
78        let m = 1 << (i % 8);
79        *b |= m;
80    }
81}
82
83fn bit_clear(bit_array: *mut u8, i: usize) {
84    debug_assert!(bit_isset(bit_array, i));
85    unsafe {
86        let b = bit_array.add(i >> 3);
87        let m = 1 << (i % 8);
88        *b &= !m;
89    }
90}
91
92// find a min k that great than n bytes
93pub fn first_up_k(n: usize, leaf_size: usize) -> usize {
94    let mut k = 0;
95    let mut size = leaf_size;
96    while size < n {
97        k += 1;
98        size <<= 1;
99    }
100    k
101}
102
103struct Node {
104    next: *mut Node,
105    prev: *mut Node,
106}
107
108impl Node {
109    fn init(list: *mut Node) {
110        unsafe {
111            (*list).next = list;
112            (*list).prev = list;
113        }
114    }
115
116    fn remove(list: *mut Node) {
117        unsafe {
118            (*(*list).prev).next = (*list).next;
119            (*(*list).next).prev = (*list).prev;
120        }
121    }
122
123    fn pop(list: *mut Node) -> *mut Node {
124        debug_assert!(!Self::is_empty(list));
125        let n_list = unsafe { (*list).next };
126        Self::remove(n_list);
127        n_list
128    }
129
130    fn push(list: *mut Node, p: *mut u8) {
131        let p = p.cast::<Node>();
132        unsafe {
133            let n_list: Node = Node {
134                prev: list,
135                next: (*list).next,
136            };
137            // pointer aligned to 16 bytes(MIN_LEAF_SIZE_ALIGN), so it's safe to use write
138            p.write(n_list);
139            (*(*list).next).prev = p;
140            (*list).next = p;
141        }
142    }
143
144    fn is_empty(list: *const Node) -> bool {
145        unsafe { std::ptr::eq((*list).next, list) }
146    }
147}
148
149struct Entry {
150    free: *mut Node,
151    /// Bit array to keep tracking alloc
152    alloc: *mut u8,
153    /// Bit array to keep tracking split
154    split: *mut u8,
155}
156
157impl Default for Entry {
158    fn default() -> Self {
159        Entry {
160            free: core::ptr::null_mut(),
161            alloc: core::ptr::null_mut(),
162            split: core::ptr::null_mut(),
163        }
164    }
165}
166
167#[derive(Clone, Copy)]
168pub struct BuddyAllocParam {
169    /// Base addr: the start address
170    base_addr: *const u8,
171    /// Len: available bytes from the start address
172    len: usize,
173    /// Leaf size: the min size to allocate
174    leaf_size: usize,
175    /// Zero filled: in many cases, provided address might already be zero filled,
176    /// in which case we can reduce re-filling zeros to the data again.
177    zero_filled: bool,
178}
179
180impl BuddyAllocParam {
181    /// Base addr: the start address
182    /// Len: available bytes from the start address
183    /// Leaf size: the min size to allocate
184    pub const fn new(base_addr: *const u8, len: usize, leaf_size: usize) -> Self {
185        BuddyAllocParam {
186            base_addr,
187            len,
188            leaf_size,
189            zero_filled: false,
190        }
191    }
192}
193
194pub struct BuddyAlloc {
195    /// memory start addr
196    base_addr: usize,
197    /// memory end addr
198    end_addr: usize,
199    /// unavailable memories at end_addr
200    unavailable: usize,
201    entries: *mut Entry,
202    entries_size: usize,
203    /// min size of a block, represent in 1 << leaf2base
204    leaf2base: usize,
205}
206
207impl BuddyAlloc {
208    /// # Safety
209    ///
210    /// The `base_addr..(base_addr + len)` must be allocated before using,
211    /// and must guarantee no others write to the memory range, to avoid undefined behaviors.
212    /// The new function panic if memory space not enough for initialize BuddyAlloc.
213    pub unsafe fn new(param: BuddyAllocParam) -> Self {
214        unsafe {
215            let BuddyAllocParam {
216                base_addr,
217                len,
218                leaf_size,
219                zero_filled,
220            } = param;
221            let mut base_addr = base_addr as usize;
222            let end_addr = base_addr + len;
223            assert!(
224                leaf_size % MIN_LEAF_SIZE_ALIGN == 0 && leaf_size != 0,
225                "{}",
226                LEAF_ALIGN_ERROR_MSG
227            );
228            let leaf2base = log2(leaf_size);
229            base_addr = roundup(base_addr, leaf2base);
230            // we use (k + 1)-th entry's split flag to test existence of k-th entry's blocks;
231            // according to this convention, we make a dummy (entries_size - 1)-th entry.
232            // so we plus 2 on entries_size.
233            let entries_size = log2((end_addr - base_addr) >> leaf2base) + 2;
234
235            // alloc buddy allocator memory
236            let used_bytes = core::mem::size_of::<Entry>() * entries_size;
237            debug_assert!(end_addr >= base_addr + used_bytes, "{}", OOM_MSG);
238            let entries = base_addr as *mut Entry;
239            base_addr += used_bytes;
240
241            let buddy_list_size = core::mem::size_of::<Node>();
242            // init entries free
243            for k in 0..entries_size {
244                // use one bit for per memory block
245                debug_assert!(end_addr >= base_addr + buddy_list_size, "{}", OOM_MSG);
246                let entry = entries.add(k).as_mut().expect("entry");
247                entry.free = base_addr as *mut Node;
248                if !zero_filled {
249                    core::ptr::write_bytes(entry.free, 0, buddy_list_size);
250                }
251                Node::init(entry.free);
252                base_addr += buddy_list_size;
253            }
254
255            // init alloc
256            for k in 0..entries_size {
257                // use one bit for per memory block
258                // use shift instead `/`, 8 == 1 << 3
259                let used_bytes = roundup(nblock(k, entries_size), 3) >> 3;
260                debug_assert!(end_addr >= base_addr + used_bytes, "{}", OOM_MSG);
261                let entry = entries.add(k).as_mut().expect("entry");
262                entry.alloc = base_addr as *mut u8;
263                // mark all blocks as allocated
264                if !zero_filled {
265                    core::ptr::write_bytes(entry.alloc, 0, used_bytes);
266                }
267                base_addr += used_bytes;
268            }
269
270            // init split
271            for k in 1..entries_size {
272                // use one bit for per memory block
273                // use shift instead `/`, 8 == 1 << 3
274                let used_bytes = roundup(nblock(k, entries_size), 3) >> 3;
275                debug_assert!(end_addr >= base_addr + used_bytes, "{}", OOM_MSG);
276                let entry = entries.add(k).as_mut().expect("entry");
277                entry.split = base_addr as *mut u8;
278                if !zero_filled {
279                    core::ptr::write_bytes(entry.split, 0, used_bytes);
280                }
281                base_addr += used_bytes;
282            }
283
284            // align base_addr to leaf size
285            base_addr = roundup(base_addr, leaf2base);
286            assert!(end_addr >= base_addr, "{}", OOM_MSG);
287            debug_assert_eq!(
288                (base_addr >> leaf2base) << leaf2base,
289                base_addr,
290                "misalignment"
291            );
292
293            let mut allocator = BuddyAlloc {
294                base_addr,
295                end_addr,
296                entries,
297                entries_size,
298                leaf2base,
299                unavailable: 0,
300            };
301            allocator.init_free_list();
302            allocator
303        }
304    }
305
306    fn init_free_list(&mut self) {
307        let mut base_addr = self.base_addr;
308        let end_addr = self.end_addr;
309        let entries_size = self.entries_size;
310
311        // try alloc blocks
312        for k in (0..(entries_size - 1)).rev() {
313            let block_size = block_size_2base(k, self.leaf2base);
314            let entry = self.entry(k);
315            let parent_entry = self.entry(k + 1);
316
317            // alloc free blocks
318            while base_addr + block_size <= end_addr {
319                debug_assert!(!bit_isset(
320                    entry.alloc,
321                    self.block_index(k, base_addr as *const u8)
322                ));
323                Node::push(entry.free, base_addr as *mut u8);
324                // mark parent's split and alloc
325                let block_index = self.block_index(k, base_addr as *const u8);
326                if block_index & 1 == 0 {
327                    let parent_index = self.block_index(k + 1, base_addr as *const u8);
328                    bit_set(parent_entry.alloc, parent_index);
329                    bit_set(parent_entry.split, parent_index);
330                }
331                base_addr += block_size;
332            }
333
334            // mark unavailable blocks as allocated
335            let n = nblock(k, entries_size);
336            let unavailable_block_index = self.block_index(k, base_addr as *const u8);
337            debug_assert!(unavailable_block_index < n);
338            bit_set(entry.alloc, unavailable_block_index);
339        }
340
341        self.unavailable = end_addr - base_addr;
342    }
343
344    pub fn malloc(&mut self, nbytes: usize) -> *mut u8 {
345        let fk = first_up_k(nbytes, 1 << self.leaf2base);
346        let mut k = match (fk..self.entries_size).find(|&k| !Node::is_empty(self.entry(k).free)) {
347            Some(k) => k,
348            None => return core::ptr::null_mut(),
349        };
350        let p: *mut u8 = Node::pop(self.entry(k).free) as *mut u8;
351        bit_set(self.entry(k).alloc, self.block_index(k, p));
352        while k > fk {
353            let q: *mut u8 = (p as usize + block_size_2base(k - 1, self.leaf2base)) as *mut u8;
354            bit_set(self.entry(k).split, self.block_index(k, p));
355            let parent_entry = self.entry(k - 1);
356            bit_set(parent_entry.alloc, self.block_index(k - 1, p));
357            debug_assert!(!bit_isset(parent_entry.alloc, self.block_index(k - 1, q)));
358            Node::push(parent_entry.free, q);
359            k -= 1;
360        }
361        debug_assert_eq!(
362            ((p as usize) >> self.leaf2base) << self.leaf2base,
363            p as usize,
364            "misalignment"
365        );
366        p
367    }
368
369    pub fn free(&mut self, mut p: *mut u8) {
370        let mut k = self.find_k_for_p(p);
371        while k < (self.entries_size - 1) {
372            let block_index = self.block_index(k, p);
373            let entry = self.entry(k);
374            bit_clear(entry.alloc, block_index);
375            let is_head = block_index & 1 == 0;
376            let buddy = if is_head {
377                block_index + 1
378            } else {
379                block_index - 1
380            };
381            if bit_isset(entry.alloc, buddy) {
382                break;
383            }
384            // merge buddy since its free
385            // 1. clear split of k + 1
386            // 2. set p to the address of merged block
387            // 3. repeat for k = k + 1 until reach MAX_K
388            // 4. push p back to k entry free list
389            let q = self.block_addr(k, buddy);
390            Node::remove(q as *mut Node);
391            if !is_head {
392                p = q as *mut u8;
393            }
394            bit_clear(self.entry(k + 1).split, self.block_index(k + 1, p));
395            k += 1;
396        }
397        debug_assert!(!bit_isset(self.entry(k).alloc, self.block_index(k, p)));
398        Node::push(self.entry(k).free, p);
399    }
400
401    fn entry(&self, i: usize) -> &Entry {
402        debug_assert!(i < self.entries_size, "index out of range");
403        unsafe { self.entries.add(i).as_ref().expect("entry") }
404    }
405
406    /// find k for p
407    fn find_k_for_p(&self, p: *const u8) -> usize {
408        for k in 0..(self.entries_size - 1) {
409            if bit_isset(self.entry(k + 1).split, self.block_index(k + 1, p)) {
410                debug_assert!(bit_isset(self.entry(k).alloc, self.block_index(k, p)));
411                return k;
412            }
413        }
414        0
415    }
416
417    /// block index of p under k
418    fn block_index(&self, k: usize, p: *const u8) -> usize {
419        if (p as usize) < self.base_addr {
420            // TODO handle this outside
421            panic!("out of memory");
422        }
423        let n = p as usize - self.base_addr;
424        // equal to: n / block_size_2base(k, self.leaf2base);
425        let index = (n >> k) >> self.leaf2base;
426        debug_assert!(index < nblock(k, self.entries_size));
427        index
428    }
429
430    /// block addr of index under k
431    fn block_addr(&self, k: usize, i: usize) -> usize {
432        // equal to: i * block_size_2base(k, self.leaf2base);
433        let n = (i << k) << self.leaf2base;
434        self.base_addr + n
435    }
436}
437
438// Main allocator class.
439pub struct UserAllocator {
440    buddy_alloc_param: BuddyAllocParam,
441    inner_buddy_alloc: Mutex<Option<BuddyAlloc>>,
442}
443
444impl UserAllocator {
445    pub const fn new(buddy_alloc_param: BuddyAllocParam) -> Self {
446        UserAllocator {
447            inner_buddy_alloc: Mutex::new(None),
448            buddy_alloc_param,
449        }
450    }
451
452    unsafe fn fetch_buddy_alloc<R, F: FnOnce(&mut BuddyAlloc) -> R>(&self, f: F) -> R {
453        unsafe {
454            let mut inner = self.inner_buddy_alloc.lock().unwrap();
455            let alloc = inner.get_or_insert_with(|| BuddyAlloc::new(self.buddy_alloc_param));
456            f(alloc)
457        }
458    }
459
460    // Enable a seccomp filter that sends a SIGSYS when mmap() is called.
461    pub fn disable_mmap(&self) -> Result<(), Box<dyn std::error::Error>> {
462        let mut ctx = seccomp::Context::default(Action::Allow)?;
463        let syscall_nr = libc::SYS_mmap as usize;
464        let cmp = Compare::arg(0)
465            .using(Op::MaskedEq)
466            .with(0)
467            .build()
468            .ok_or("Failed to build seccomp filter")?;
469
470        let rule = Rule::new(syscall_nr, cmp, Action::Errno(libc::EPERM));
471        ctx.add_rule(rule)?;
472        ctx.load()?;
473
474        Ok(())
475    }
476
477    pub fn lock_memory(&self) {
478        unsafe {
479            VM.save().ok();
480
481            // Call setrlimit to set the locked-in-memory limit to unlimited.
482            let new_rlimit = libc::rlimit {
483                rlim_cur: libc::RLIM_INFINITY,
484                rlim_max: libc::RLIM_INFINITY,
485            };
486            let res = libc::setrlimit(libc::RLIMIT_MEMLOCK, &new_rlimit);
487            if res != 0 {
488                panic!("setrlimit failed with error code: {}", res);
489            }
490
491            // Lock all memory to prevent being paged out.
492            let res = libc::mlockall(libc::MCL_CURRENT | libc::MCL_FUTURE);
493            if res != 0 {
494                panic!("mlockall failed with error code: {}", res);
495            }
496
497            // Hint the kernel to use huge pages for the memory arena.
498            let ptr = (&raw mut HEAP.0) as *mut u8 as *mut libc::c_void;
499            libc::madvise(ptr, HEAP_SIZE, libc::MADV_HUGEPAGE);
500        }
501    }
502
503    pub fn unlock_memory(&self) {
504        VM.restore().ok();
505    }
506}
507
508// Override global allocator methods.
509unsafe impl GlobalAlloc for UserAllocator {
510    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
511        unsafe {
512            let bytes = layout.size();
513            self.fetch_buddy_alloc(|alloc| alloc.malloc(bytes))
514        }
515    }
516
517    unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
518        unsafe {
519            self.fetch_buddy_alloc(|alloc| alloc.free(ptr));
520        }
521    }
522}
523
524unsafe impl Sync for UserAllocator {}
525
526// Buddy allocator parameters.
527const HEAP_SIZE: usize = 64 * 1024 * 1024; // 64M
528const LEAF_SIZE: usize = 64;
529
530#[repr(align(4096))]
531struct AlignedHeap<const N: usize>([u8; N]);
532
533// Statically pre-allocated memory arena.
534static mut HEAP: AlignedHeap<HEAP_SIZE> = AlignedHeap([0u8; HEAP_SIZE]);
535
536// Override default memory allocator.
537//
538// To prevent potential deadlock conditions under heavy loads, any scheduler that delegates
539// scheduling decisions to user-space should avoid triggering page faults.
540//
541// To address this issue, replace the global allocator with a custom one (UserAllocator),
542// designed to operate on a pre-allocated buffer. This, coupled with the memory locking achieved
543// through mlockall(), prevents page faults from occurring during the execution of the user-space
544// scheduler.
545#[cfg_attr(not(test), global_allocator)]
546pub static ALLOCATOR: UserAllocator = unsafe {
547    UserAllocator::new(BuddyAllocParam::new(
548        (&raw const HEAP.0) as *const u8,
549        HEAP_SIZE,
550        LEAF_SIZE,
551    ))
552};
553
554// State of special sysctl VM settings.
555//
556// This is used to save the previous state of some procfs settings that must be changed by the
557// user-space scheduler.
558struct VmSettings {
559    // We cannot allow page faults in the user-space scheduler, otherwise we may hit deadlock
560    // conditions: a kthread may need to run to resolve the page fault, but the user-space
561    // scheduler is waiting on the page fault to be resolved => deadlock.
562    //
563    // To prevent this from happening automatically enforce vm.compact_unevictable_allowed=0 when
564    // the scheduler is running, to disable compaction of unevictable memory pages and make sure
565    // that the scheduler never faults.
566    //
567    // The original value will be restored when the user-space scheduler exits.
568    saved_compact_unevictable_allowed: AtomicI32,
569}
570
571impl VmSettings {
572    // Return the content of a procfs file as i32.
573    fn read_procfs(&self, file_path: &str) -> Result<i32, String> {
574        // Attempt to open the file
575        let file = File::open(file_path)
576            .map_err(|err| format!("Failed to open {}: {}", file_path, err))?;
577
578        let reader = BufReader::new(file);
579
580        let line = reader
581            .lines()
582            .next()
583            .ok_or_else(|| format!("File is empty: {}", file_path))?
584            .map_err(|err| format!("Failed to read line from {}: {}", file_path, err))?;
585
586        line.trim()
587            .parse::<i32>()
588            .map_err(|err| format!("Failed to parse {}: {}", file_path, err))
589    }
590
591    // Write an i32 to a file in procfs.
592    fn write_procfs(&self, file_path: &str, value: i32) -> Result<(), String> {
593        // Attempt to create or open the file
594        let mut file = File::create(file_path)
595            .map_err(|err| format!("Failed to open {}: {}", file_path, err))?;
596
597        // Attempt to write the value to the file
598        write!(file, "{}", value)
599            .map_err(|err| format!("Failed to write to {}: {}", file_path, err))
600    }
601
602    // Save all the sysctl VM settings in the internal state.
603    fn save(&self) -> Result<(), String> {
604        let compact_unevictable_allowed = "/proc/sys/vm/compact_unevictable_allowed";
605        let value = self.read_procfs(compact_unevictable_allowed)?;
606        self.saved_compact_unevictable_allowed
607            .store(value, Ordering::Relaxed);
608        self.write_procfs(compact_unevictable_allowed, 0)?;
609
610        Ok(())
611    }
612
613    // Restore all the previous sysctl vm settings.
614    fn restore(&self) -> Result<(), String> {
615        let compact_unevictable_allowed = "/proc/sys/vm/compact_unevictable_allowed";
616        let value = self
617            .saved_compact_unevictable_allowed
618            .load(Ordering::Relaxed);
619        self.write_procfs(compact_unevictable_allowed, value)?;
620
621        Ok(())
622    }
623}
624
625// Special sysctl VM settings.
626static VM: VmSettings = VmSettings {
627    saved_compact_unevictable_allowed: AtomicI32::new(0),
628};