From bf2524579fece9437b68945084eae21e6873345e Mon Sep 17 00:00:00 2001 From: acentelles Date: Sun, 30 Aug 2026 20:56:42 -0400 Subject: [PATCH] fix(os-linux): only forward exact whole-region munmaps to kfree sys_munmap forwarded any (addr, len) straight to kfree, but the kernel heap only supports freeing the exact (pointer, layout) pairs it handed out, so POSIX-legal repeated, partial, or interior munmaps corrupted the free list or tripped the allocator's bad-free assert inside the trap handler, aborting the guest. musl std guests reach these paths through raw libc::munmap users and reservation trimming. Track the regions sys_mmap hands out in a fixed-capacity GlobalCell table (single-core, no interrupts, cooperative scheduling; same soundness argument as the scheduler's own state) and forward only a munmap whose base and page-rounded length exactly match a live entry; every other munmap keeps the pages mapped (a bounded leak, since a kmalloc-backed region cannot be partially returned) and reports success per POSIX. If the table fills, sys_mmap frees the fresh region and fails with -ENOMEM rather than handing out a mapping whose unmap could never be validated. --- crates/zeroos-os-linux/src/handlers/memory.rs | 124 +++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/crates/zeroos-os-linux/src/handlers/memory.rs b/crates/zeroos-os-linux/src/handlers/memory.rs index 51a497b..5798e7e 100644 --- a/crates/zeroos-os-linux/src/handlers/memory.rs +++ b/crates/zeroos-os-linux/src/handlers/memory.rs @@ -1,10 +1,60 @@ use core::alloc::Layout; use foundation::kfn; +use foundation::utils::GlobalCell; use libc; const PAGE_SIZE: usize = 4096; +/// Maximum concurrent anonymous mappings. Each entry is two words; the table +/// is a fixed 16 KiB static so mapping bookkeeping never allocates from the +/// heap it accounts for. +const MAX_MMAP_REGIONS: usize = 1024; + +/// Live `(base, page-rounded size)` pairs handed out by [`sys_mmap`]; +/// `size == 0` marks a free slot. +/// +/// The kernel heap only supports freeing the exact `(pointer, layout)` pairs +/// it allocated, while POSIX allows `munmap` to release part of a mapping, +/// span several mappings, or name a range with no mappings at all. Forwarding +/// such calls to `kfree` corrupts the heap free list, so [`sys_munmap`] only +/// frees a munmap that exactly matches a live entry here. +/// +/// The unsynchronized cell is sound for the same reason as the scheduler's +/// own `GlobalOption` state: ZeroOS is single-core with no interrupts and no +/// preemption, and thread scheduling is cooperative, so at most one logical +/// flow of execution touches kernel state at a time. +static MMAP_REGIONS: GlobalCell<[(usize, usize); MAX_MMAP_REGIONS]> = + GlobalCell::new([(0, 0); MAX_MMAP_REGIONS]); + +/// Records a region handed out by [`sys_mmap`]; `false` means the table is +/// full and the caller must not hand the region to the guest. +fn record_region(regions: &mut [(usize, usize)], base: usize, size: usize) -> bool { + match regions.iter_mut().find(|(_, size)| *size == 0) { + Some(slot) => { + *slot = (base, size); + true + } + None => false, + } +} + +/// Clears and reports a live region exactly matching `(base, size)`; `false` +/// means the range is not an exact whole-region unmap and must not reach +/// `kfree`. +fn take_exact_region(regions: &mut [(usize, usize)], base: usize, size: usize) -> bool { + match regions + .iter_mut() + .find(|entry| **entry == (base, size) && size != 0) + { + Some(slot) => { + *slot = (0, 0); + true + } + None => false, + } +} + pub fn sys_brk(_brk: usize) -> isize { -(libc::ENOMEM as isize) } @@ -52,6 +102,12 @@ pub fn sys_mmap( if ptr.is_null() { return -(libc::ENOMEM as isize); } + if !MMAP_REGIONS.with_mut(|regions| record_region(regions, ptr as usize, size)) { + // Fail fast rather than hand out a mapping whose unmap could never + // be validated (and therefore never freed). + kfn::memory::kfree(ptr, layout); + return -(libc::ENOMEM as isize); + } unsafe { core::ptr::write_bytes(ptr, 0, size); } @@ -74,7 +130,13 @@ pub fn sys_munmap(addr: usize, len: usize) -> isize { Ok(l) => l, Err(_) => return -(libc::EINVAL as isize), }; - kfn::memory::kfree(addr as *mut u8, layout); + if MMAP_REGIONS.with_mut(|regions| take_exact_region(regions, addr, size)) { + kfn::memory::kfree(addr as *mut u8, layout); + return 0; + } + // POSIX allows partial, repeated, spanning, and no-mapping munmaps; the + // kernel heap cannot release part of a kmalloc region, so keep the pages + // mapped (a bounded leak) and report success. 0 } @@ -91,3 +153,63 @@ pub fn sys_mprotect(addr: usize, len: usize, prot: usize) -> isize { } 0 } + +#[cfg(test)] +mod tests { + use super::{record_region, take_exact_region, MAX_MMAP_REGIONS, PAGE_SIZE}; + + #[test] + fn exact_unmap_matches_once() { + let mut regions = [(0usize, 0usize); 4]; + assert!(record_region(&mut regions, 0x8000_0000, 16 * PAGE_SIZE)); + assert!(take_exact_region(&mut regions, 0x8000_0000, 16 * PAGE_SIZE)); + // Repeated unmap of the released range must not match again. + assert!(!take_exact_region( + &mut regions, + 0x8000_0000, + 16 * PAGE_SIZE + )); + } + + #[test] + fn non_exact_unmaps_do_not_match() { + let mut regions = [(0usize, 0usize); 4]; + assert!(record_region(&mut regions, 0x8000_0000, 16 * PAGE_SIZE)); + // Partial (tail), interior, spanning, and unknown ranges. + assert!(!take_exact_region( + &mut regions, + 0x8000_0000 + 8 * PAGE_SIZE, + 8 * PAGE_SIZE + )); + assert!(!take_exact_region(&mut regions, 0x8000_0000, 8 * PAGE_SIZE)); + assert!(!take_exact_region( + &mut regions, + 0x8000_0000, + 32 * PAGE_SIZE + )); + assert!(!take_exact_region( + &mut regions, + 0x9000_0000, + 16 * PAGE_SIZE + )); + // The region stays live and its exact unmap still matches. + assert!(take_exact_region(&mut regions, 0x8000_0000, 16 * PAGE_SIZE)); + } + + #[test] + fn free_slot_sentinel_never_matches() { + let mut regions = [(0usize, 0usize); 4]; + assert!(!take_exact_region(&mut regions, 0, 0)); + } + + #[test] + fn full_table_refuses_and_slots_are_reused() { + let mut regions = [(0usize, 0usize); MAX_MMAP_REGIONS]; + for i in 0..MAX_MMAP_REGIONS { + assert!(record_region(&mut regions, (i + 1) * PAGE_SIZE, PAGE_SIZE)); + } + assert!(!record_region(&mut regions, usize::MAX & !0xfff, PAGE_SIZE)); + assert!(take_exact_region(&mut regions, PAGE_SIZE, PAGE_SIZE)); + assert!(record_region(&mut regions, 42 * PAGE_SIZE, 2 * PAGE_SIZE)); + } +}