This fixes a lot of memory bugs which arose as a result of doing something different from the system malloc/free when allocators were rewritten in Rust. These changes originate from a different approach to writing the allocator in Rust: https://git.sdf.org/vitrine/wmaker/pulls/1/files#diff-04a0fd2319b9969373b75377716e45c836d22869 There are other function calls (to XFree) that need to be fixed, but that can be done in another commit. This one is already getting large.
284 lines
8.8 KiB
Rust
284 lines
8.8 KiB
Rust
//! Custom implementations of malloc/free/realloc.
|
|
//!
|
|
//! These are intended for use by C functions that need to allocate. Window
|
|
//! Maker originally provided [`wmalloc`], [`wfree`], and [`wrealloc`] for
|
|
//! customizable handling of memory exhaustion (to save workspace state before
|
|
//! aborting) and to allow optional use of the Boehm GC library. It also tracked
|
|
//! reference counts, via [`wretain`] and [`wrelease`].
|
|
//!
|
|
//! If everything gets rewritten in Rust, we won't need this module anymore. For
|
|
//! now, it helps to move our allocations into Rust so that it is more
|
|
//! straightforward to store Rust objects in heap memory that was allocated from
|
|
//! C. (Rust may have stricter requirements for heap-allocated segments than are
|
|
//! provided by arbitrary C allocators).
|
|
//!
|
|
//! TODO: We may want to restore handling of OOM errors. This would require
|
|
//! installing a customized Rust allocator, which isn't something you can do yet
|
|
//! in stable Rust. And, unless our rewrite ends up taking up obscenely more
|
|
//! memory than the baseline Window Maker code, it isn't really necessary in
|
|
//! this day and age.
|
|
|
|
use std::{alloc, ffi::{c_char, CStr}, mem, ptr::{self, NonNull}};
|
|
|
|
/// Tracks the layout and reference count of an allocated chunk of memory.
|
|
#[derive(Clone, Copy)]
|
|
struct Header {
|
|
ptr: NonNull<u8>,
|
|
payload_size: usize,
|
|
layout: alloc::Layout,
|
|
refcount: u16,
|
|
}
|
|
|
|
impl Header {
|
|
/// Recovers the `Header` for the allocated memory chunk `b`.
|
|
///
|
|
/// ## Safety
|
|
///
|
|
/// Callers must ensure that `b` is a live allocation from [`wmalloc`] or [`wrealloc`].
|
|
unsafe fn for_alloc_bytes(b: *mut u8) -> *mut Header {
|
|
unsafe {
|
|
b.sub(mem::size_of::<Header>())
|
|
.cast::<Header>()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Allocates at least `size` bytes and returns a pointer to them.
|
|
///
|
|
/// Returns null if `size` is 0.
|
|
pub fn alloc_bytes(size: usize) -> *mut u8 {
|
|
if size == 0 {
|
|
return ptr::null_mut();
|
|
}
|
|
let Ok(header_layout) = alloc::Layout::from_size_align(mem::size_of::<Header>(), 8) else {
|
|
return ptr::null_mut();
|
|
};
|
|
|
|
let Ok(layout) = alloc::Layout::from_size_align(size, 8) else {
|
|
return ptr::null_mut();
|
|
};
|
|
let Ok((full_layout, result_offset)) = header_layout.extend(layout) else {
|
|
return ptr::null_mut();
|
|
};
|
|
|
|
let full_segment = unsafe { alloc::alloc_zeroed(full_layout) };
|
|
if full_segment.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
let result = unsafe { full_segment.add(result_offset) };
|
|
if result.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
|
|
unsafe {
|
|
let header = result.sub(mem::size_of::<Header>()).cast::<Header>();
|
|
header.write_unaligned(Header {
|
|
ptr: NonNull::new_unchecked(full_segment),
|
|
payload_size: size,
|
|
layout: full_layout,
|
|
refcount: 0,
|
|
});
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
/// Allocates a segment with [`alloc_bytes`] and fills it with the contents of
|
|
/// `s`. The resulting string should be free'd by passing it to [`free_bytes`].
|
|
pub fn alloc_string(s: &CStr) -> *mut c_char {
|
|
let len = s.count_bytes() + 1;
|
|
let result = alloc_bytes(len).cast::<c_char>();
|
|
unsafe { ptr::copy_nonoverlapping(s.as_ptr().cast::<c_char>(), result, len); }
|
|
result
|
|
}
|
|
|
|
/// Frees the bytes pointed to by `b`.
|
|
///
|
|
/// ## Safety
|
|
///
|
|
/// Callers must ensure that `b` is a live allocation from [`wmalloc`] or [`wrealloc`].
|
|
pub unsafe fn free_bytes(b: *mut u8) {
|
|
if b.is_null() {
|
|
return;
|
|
}
|
|
unsafe {
|
|
let header = &*Header::for_alloc_bytes(b);
|
|
alloc::dealloc(header.ptr.as_ptr(), header.layout);
|
|
}
|
|
}
|
|
|
|
/// Functions to be called from C.
|
|
pub mod ffi {
|
|
use super::{alloc_bytes, free_bytes, Header};
|
|
|
|
use std::{ffi::c_void, ptr};
|
|
|
|
/// Allocates `size` bytes. Returns null if `size` is 0. Data will be
|
|
/// initialized to 0.
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wmalloc(size: usize) -> *mut c_void {
|
|
alloc_bytes(size).cast::<c_void>()
|
|
}
|
|
|
|
/// Frees `ptr`, which must have come from [`wmalloc`] or [`wrealloc`].
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wfree(ptr: *mut c_void) {
|
|
unsafe { free_bytes(ptr.cast::<u8>()); }
|
|
}
|
|
|
|
/// Resizes `ptr` to be at least `newsize` bytes in size, returning the
|
|
/// start of the new segment. If `newsize` is larger than `ptr`'s segment,
|
|
/// data in the new space will be initialized but have an arbitrary value.
|
|
///
|
|
/// ## Safety
|
|
///
|
|
/// If `ptr` is non-null, callers must ensure that it came from from
|
|
/// [`wmalloc`] or [`wrealloc`].
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wrealloc(ptr: *mut c_void, newsize: usize) -> *mut c_void {
|
|
if ptr.is_null() {
|
|
unsafe {
|
|
return wmalloc(newsize);
|
|
}
|
|
}
|
|
unsafe {
|
|
let result = wmalloc(newsize);
|
|
let result_header = ptr::read_unaligned(Header::for_alloc_bytes(result.cast()));
|
|
let ptr_header = ptr::read_unaligned(Header::for_alloc_bytes(ptr.cast()));
|
|
let copy_size = usize::min(
|
|
ptr_header.payload_size,
|
|
result_header.payload_size,
|
|
);
|
|
ptr::copy_nonoverlapping(ptr, result, copy_size);
|
|
wfree(ptr);
|
|
result
|
|
}
|
|
}
|
|
|
|
/// Bumps the refcount for `ptr`.
|
|
///
|
|
/// ## Safety
|
|
///
|
|
/// Callers must ensure that `b` is a live allocation from [`wmalloc`] or [`wrealloc`].
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wretain(ptr: *mut c_void) -> *mut c_void {
|
|
if ptr.is_null() {
|
|
return ptr::null_mut();
|
|
}
|
|
unsafe {
|
|
let header = Header::for_alloc_bytes(ptr.cast::<u8>());
|
|
(*header).refcount += 1;
|
|
}
|
|
ptr
|
|
}
|
|
|
|
/// Decrements the refcount for `ptr`. If this brings the refcount to 0,
|
|
/// frees `ptr`.
|
|
///
|
|
/// ## Safety
|
|
///
|
|
/// Callers must ensure that `ptr` is a live allocation from [`wmalloc`] or [`wrealloc`].
|
|
#[unsafe(no_mangle)]
|
|
pub unsafe extern "C" fn wrelease(ptr: *mut c_void) {
|
|
let ptr = ptr.cast::<u8>();
|
|
if ptr.is_null() {
|
|
return;
|
|
}
|
|
let header = unsafe { &mut *Header::for_alloc_bytes(ptr) };
|
|
match header.refcount {
|
|
0 | 1 => unsafe { free_bytes(ptr) },
|
|
_ => header.refcount -= 1,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use super::{alloc_bytes, alloc_string, ffi::{wfree, wmalloc, wrealloc}, free_bytes, Header};
|
|
|
|
use std::{ffi::CStr, mem, os::raw::c_void, ptr, slice};
|
|
|
|
#[test]
|
|
fn recover_header() {
|
|
unsafe {
|
|
let x = alloc_bytes(mem::size_of::<i64>());
|
|
let header = Header::for_alloc_bytes(x);
|
|
assert_eq!(header.cast::<u8>().add(mem::size_of::<Header>()), x);
|
|
// This may be allocator-dependent, but it's a reasonable sanity check for now.
|
|
assert!((*header).ptr.as_ptr() <= header.cast::<u8>());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn alloc_zero_returns_null() {
|
|
assert!(alloc_bytes(0).is_null());
|
|
}
|
|
|
|
#[test]
|
|
fn free_null() {
|
|
unsafe { free_bytes(ptr::null_mut()); }
|
|
}
|
|
|
|
#[test]
|
|
fn realloc_null() {
|
|
unsafe { assert!(wrealloc(ptr::null_mut(), 0).is_null()); }
|
|
}
|
|
|
|
#[test]
|
|
fn alloc_free_nonzero() {
|
|
let x = alloc_bytes(mem::size_of::<i64>()).cast::<i64>();
|
|
assert!(!x.is_null());
|
|
unsafe { *x = 42; }
|
|
assert_eq!(unsafe { *x }, 42);
|
|
unsafe { free_bytes(x.cast::<u8>()); }
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_allocs() {
|
|
unsafe {
|
|
let x = wmalloc(mem::size_of::<i64>());
|
|
*x.cast::<i64>() = 30;
|
|
let y = wmalloc(mem::size_of::<i32>());
|
|
*y.cast::<i32>() = 5;
|
|
let z = wmalloc(48);
|
|
*z.cast::<f32>() = 1.0;
|
|
wfree(x);
|
|
wfree(y);
|
|
wfree(z);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn realloc_nonzero() {
|
|
let x = alloc_bytes(mem::size_of::<i64>()).cast::<c_void>();
|
|
assert!(!x.is_null());
|
|
let y = unsafe { wrealloc(x, mem::size_of::<i32>()).cast::<i32>() };
|
|
assert!(!y.is_null());
|
|
unsafe { *y = 17; }
|
|
assert_eq!(unsafe { *y }, 17);
|
|
unsafe { free_bytes(y.cast::<u8>()); }
|
|
}
|
|
|
|
#[test]
|
|
fn realloc_retains_data() {
|
|
let x: *mut u8 = unsafe { wmalloc(10).cast() };
|
|
unsafe {
|
|
let xs = slice::from_raw_parts_mut(&mut *x, 10);
|
|
// We know that xs should be zeroed.
|
|
assert_eq!(xs, &[0u8; 10]);
|
|
for i in 0u8..10 {
|
|
xs[i as usize] = i;
|
|
}
|
|
assert_eq!(xs, (0..10).collect::<Vec::<u8>>());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn alloc_free_string() {
|
|
let s = alloc_string(c"hello");
|
|
assert!(!s.is_null());
|
|
assert_eq!(unsafe { CStr::from_ptr(s) }, c"hello");
|
|
unsafe { free_bytes(s.cast::<u8>()); }
|
|
}
|
|
}
|