fix: kernel hardening pass 2 — page tracking, queue overflow, region tracking

Page lifecycle:
- ProcessMemory now tracks (PhysicalAddress, count) pairs for each
  allocation (up to 16). destroy_process iterates and frees all
  pages back to the pool. No more permanent page leaks on exit/fault.
- RequestPages re-adds MemoryRegion entries so ReleasePages works.
  Release test un-ignored, passes.
- RequestPages: VA allocation moved after pool allocation succeeds,
  preventing VA leak on OOM.

IPC hardening:
- WaitQueue overflow (>16 waiters): returns WouldBlock error instead
  of silently dropping the thread. Thread is not blocked if enqueue
  fails.
- debug_assert on pending_messages[sender].take() — invariant that
  a sender in wait queue must have a pending message.

Scheduler:
- remove() now clears current thread if the removed thread was
  current on its core.
- add() asserts push succeeded; bitmap bit only set if thread was
  actually added to RunList.

Cleanup:
- Priority::DEFAULT replaces Priority::new(16).unwrap() in
  spawn_thread and spawn_thread_in.

36 kernel tests pass, 0 ignored.
This commit is contained in:
2026-10-18 22:31:16 +02:00
parent ea2ac5d1d9
commit d009818caf
8 changed files with 78 additions and 34 deletions

View File

@@ -1,16 +1,19 @@
use types::address::VirtualAddress; use types::address::{PhysicalAddress, VirtualAddress};
use types::id::ProcessId; use types::id::ProcessId;
use crate::error::KernelError; use crate::error::KernelError;
const USER_HEAP_START: usize = 0x40000; const USER_HEAP_START: usize = 0x40000;
const PAGE_SIZE: usize = 4096; const PAGE_SIZE: usize = 4096;
const MAX_ALLOCATIONS: usize = 16;
pub struct ProcessMemory { pub struct ProcessMemory {
process: ProcessId, process: ProcessId,
page_count: usize, page_count: usize,
limit: usize, limit: usize,
next_heap_va: usize, next_heap_va: usize,
allocations: [(PhysicalAddress, usize); MAX_ALLOCATIONS],
alloc_count: usize,
} }
impl ProcessMemory { impl ProcessMemory {
@@ -20,6 +23,8 @@ impl ProcessMemory {
page_count: 0, page_count: 0,
limit, limit,
next_heap_va: USER_HEAP_START, next_heap_va: USER_HEAP_START,
allocations: [(PhysicalAddress::from_raw(0), 0); MAX_ALLOCATIONS],
alloc_count: 0,
} }
} }
@@ -29,6 +34,17 @@ impl ProcessMemory {
VirtualAddress::from_raw(va) VirtualAddress::from_raw(va)
} }
pub fn record_allocation(&mut self, pa: PhysicalAddress, count: usize) {
if self.alloc_count < MAX_ALLOCATIONS {
self.allocations[self.alloc_count] = (pa, count);
self.alloc_count += 1;
}
}
pub fn allocations(&self) -> &[(PhysicalAddress, usize)] {
&self.allocations[..self.alloc_count]
}
pub fn process(&self) -> ProcessId { pub fn process(&self) -> ProcessId {
self.process self.process
} }

View File

@@ -68,7 +68,11 @@ impl Scheduler for MultiCoreScheduler {
fn remove(&mut self, thread: ThreadId) { fn remove(&mut self, thread: ThreadId) {
let idx = thread.raw() as usize; let idx = thread.raw() as usize;
if let Some(placement) = self.placements[idx].take() { if let Some(placement) = self.placements[idx].take() {
self.cores[placement.core.raw() as usize].remove(thread, placement.priority); let core_idx = placement.core.raw() as usize;
self.cores[core_idx].remove(thread, placement.priority);
if self.cores[core_idx].current() == Some(thread) {
self.cores[core_idx].clear_current();
}
} }
} }

View File

@@ -77,8 +77,11 @@ impl PriorityQueue {
pub fn add(&mut self, thread: ThreadId, priority: Priority) { pub fn add(&mut self, thread: ThreadId, priority: Priority) {
let idx = priority.value() as usize; let idx = priority.value() as usize;
self.queues[idx].push(thread); let added = self.queues[idx].push(thread);
self.bitmap |= 1 << idx; debug_assert!(added, "scheduler run list full for priority {}", idx);
if added {
self.bitmap |= 1 << idx;
}
} }
pub fn remove(&mut self, thread: ThreadId, priority: Priority) { pub fn remove(&mut self, thread: ThreadId, priority: Priority) {

View File

@@ -234,12 +234,8 @@ impl<P: Platform> Kernel<P> {
// Free allocated pages back to pool // Free allocated pages back to pool
if let Some(pm) = &self.process_memory[pid.raw() as usize] { if let Some(pm) = &self.process_memory[pid.raw() as usize] {
let page_count = pm.page_count(); for &(pa, count) in pm.allocations() {
if page_count > 0 { self.page_pool.free(pa, count);
// Pages were allocated from the pool but we don't track individual
// addresses. The page pool uses contiguous allocation, so we can't
// free without knowing the base address. Mark as returned for accounting.
// Full page tracking is a future improvement.
} }
} }

View File

@@ -99,7 +99,12 @@ impl<P: Platform> Kernel<P> {
domain::error::KernelError::InvalidEndpoint(ep_id), domain::error::KernelError::InvalidEndpoint(ep_id),
)), )),
}; };
let _ = endpoint.enqueue_sender(caller_tid); if endpoint.enqueue_sender(caller_tid).is_none() {
self.pending_messages[caller_tid.raw() as usize] = None;
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::WouldBlock,
));
}
let caller = match self.threads.get_mut(caller_tid) { let caller = match self.threads.get_mut(caller_tid) {
Ok(t) => t, Ok(t) => t,
@@ -149,11 +154,9 @@ impl<P: Platform> Kernel<P> {
)), )),
}; };
let message = self.pending_messages[sender_tid.raw() as usize] let message = self.pending_messages[sender_tid.raw() as usize].take();
.take() debug_assert!(message.is_some(), "sender in wait queue had no pending message");
// A sender in the wait queue must have a pending message. let message = message.unwrap_or_else(|| domain::message::Message::empty());
// If not, the kernel state is inconsistent.
.unwrap_or_else(|| domain::message::Message::empty());
self.pending_results[sender_tid.raw() as usize] = Some(SyscallResult::Ok); self.pending_results[sender_tid.raw() as usize] = Some(SyscallResult::Ok);
@@ -174,7 +177,11 @@ impl<P: Platform> Kernel<P> {
domain::error::KernelError::InvalidEndpoint(ep_id), domain::error::KernelError::InvalidEndpoint(ep_id),
)), )),
}; };
let _ = endpoint.enqueue_receiver(caller_tid); if endpoint.enqueue_receiver(caller_tid).is_none() {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::WouldBlock,
));
}
let caller = match self.threads.get_mut(caller_tid) { let caller = match self.threads.get_mut(caller_tid) {
Ok(t) => t, Ok(t) => t,

View File

@@ -13,9 +13,8 @@ impl<P: Platform> Kernel<P> {
use domain::scheduler::Scheduler; use domain::scheduler::Scheduler;
use domain::thread::SavedContext; use domain::thread::SavedContext;
let priority = types::bounded::Priority::new(16) let priority = types::bounded::Priority::DEFAULT;
.unwrap_or_else(|| unreachable!()); let time_slice = types::time::TimeSlice::DEFAULT;
let time_slice = types::time::TimeSlice::new(100);
let context = SavedContext::with_entry_and_stack(entry, stack); let context = SavedContext::with_entry_and_stack(entry, stack);
let tid = match self.threads.spawn_with_context(pid, priority, time_slice, None, context) { let tid = match self.threads.spawn_with_context(pid, priority, time_slice, None, context) {
@@ -97,9 +96,8 @@ impl<P: Platform> Kernel<P> {
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)), Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
}; };
let priority = types::bounded::Priority::new(16) let priority = types::bounded::Priority::DEFAULT;
.unwrap_or_else(|| unreachable!()); let time_slice = types::time::TimeSlice::DEFAULT;
let time_slice = types::time::TimeSlice::new(100);
let context = SavedContext::with_entry_and_stack(entry, stack); let context = SavedContext::with_entry_and_stack(entry, stack);
let tid = match self.threads.spawn_with_context(target_pid, priority, time_slice, None, context) { let tid = match self.threads.spawn_with_context(target_pid, priority, time_slice, None, context) {

View File

@@ -12,21 +12,21 @@ impl<P: Platform> Kernel<P> {
) -> SyscallOutcome { ) -> SyscallOutcome {
let pages = count.value() as usize; let pages = count.value() as usize;
let pm = match &mut self.process_memory[pid.raw() as usize] { // Check limits first (separate borrow)
Some(pm) => pm, match &mut self.process_memory[pid.raw() as usize] {
Some(pm) => {
if pm.grant(pages).is_err() {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
}
}
None => return SyscallOutcome::Complete(SyscallResult::Error( None => return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::InvalidProcess(pid), domain::error::KernelError::InvalidProcess(pid),
)), )),
};
if pm.grant(pages).is_err() {
return SyscallOutcome::Complete(SyscallResult::Error(
domain::error::KernelError::OutOfMemory,
));
} }
let user_va = pm.alloc_user_va(pages); // Allocate physical pages from pool
let phys_addr = match self.page_pool.allocate(pages) { let phys_addr = match self.page_pool.allocate(pages) {
Some(a) => a, Some(a) => a,
None => { None => {
@@ -39,12 +39,33 @@ impl<P: Platform> Kernel<P> {
} }
}; };
// Allocate user VA and record allocation (only after pool succeeds)
let user_va = match &mut self.process_memory[pid.raw() as usize] {
Some(pm) => {
let va = pm.alloc_user_va(pages);
pm.record_allocation(phys_addr, pages);
va
}
None => unreachable!(),
};
// Map pages
for i in 0..pages { for i in 0..pages {
let page_va = VirtualAddress::from_raw(user_va.raw() + i * 4096); let page_va = VirtualAddress::from_raw(user_va.raw() + i * 4096);
let page_pa = PhysicalAddress::from_raw(phys_addr.raw() + i * 4096); let page_pa = PhysicalAddress::from_raw(phys_addr.raw() + i * 4096);
self.platform.map_user_page(pid, page_va, page_pa, true, false); self.platform.map_user_page(pid, page_va, page_pa, true, false);
} }
// Track region for ReleasePages
let region = domain::memory::MemoryRegion::new(
types::id::RegionId::from_raw(0),
pid,
phys_addr,
count,
types::generation::Generation::initial(),
);
let _ = self.regions.insert(region);
SyscallOutcome::Complete(SyscallResult::Value(user_va.raw())) SyscallOutcome::Complete(SyscallResult::Value(user_va.raw()))
} }

View File

@@ -27,7 +27,6 @@ fn request_pages_reduces_pool() {
} }
#[test] #[test]
#[ignore] // RequestPages no longer creates regions — ReleasePages needs redesign
fn release_pages_frees_and_restores_pool() { fn release_pages_frees_and_restores_pool() {
let (mut k, _) = kernel_with_process(); let (mut k, _) = kernel_with_process();
let before = k.free_page_count(); let before = k.free_page_count();