feat(kernel): kernel-owned memory model

- FrameTable: per-frame owner/use, replaces PagePool + ProcessMemory
- Quota per process, page tables charged to owner
- Regions for shared mem + device MMIO, torn down w/ owner
- Platform: create/destroy AS, map/unmap, zero_frame; drop translate_va
- RequestPages/ReleasePages/MapPage(move) checked vs kernel records
- user window 0x1000..32MiB, private pts by construction
- boot loader + device setup go through kernel api
- kernel built in place, boot allocator bounded
- isolation tests, randomized invariant test, FrameTable kani proofs
- fixes: inflated release, unmapped release, dst va unchecked,
  heap overflow, unbounded pt alloc, unzeroed frames, double free
This commit is contained in:
2026-10-26 19:40:50 +01:00
parent e931485ca7
commit 8d2c40cb1e
43 changed files with 2815 additions and 1172 deletions

View File

@@ -0,0 +1,217 @@
//! Physical frame ownership. The kernel's single source of truth for what
//! every frame of the user pool is used for and who is charged for it.
use types::{
address::{PhysicalAddress, VirtualAddress},
id::{ProcessId, RegionId},
mapping::PagePerms,
};
#[cfg(test)]
#[path = "tests.rs"]
mod tests;
#[cfg(kani)]
#[path = "proofs.rs"]
mod proofs;
pub const PAGE_SIZE: usize = 4096;
/// Frames the kernel can track. The board's user pool must fit.
pub const MAX_FRAMES: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FrameUse {
/// Mapped exactly once, at `page` in `owner`'s address space.
User {
owner: ProcessId,
page: u32,
perms: PagePerms,
},
/// Holds part of `owner`'s page tables.
PageTable { owner: ProcessId },
/// Backs a shared memory region.
Region { region: RegionId },
}
impl FrameUse {
/// A user mapping at `va`, which must be page aligned.
pub fn user(owner: ProcessId, va: VirtualAddress, perms: PagePerms) -> Option<Self> {
if !va.raw().is_multiple_of(PAGE_SIZE) {
return None;
}
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
Some(Self::User { owner, page, perms })
}
}
pub struct FrameTable<const N: usize = MAX_FRAMES> {
base: PhysicalAddress,
total: usize,
frames: [Option<FrameUse>; N],
free: usize,
}
impl<const N: usize> FrameTable<N> {
pub fn new(base: PhysicalAddress, total: usize) -> Self {
let total = total.min(N);
Self {
base,
total,
frames: [None; N],
free: total,
}
}
pub fn base(&self) -> PhysicalAddress {
self.base
}
pub fn capacity(&self) -> usize {
self.total
}
pub fn free_count(&self) -> usize {
self.free
}
fn index_of(&self, pa: PhysicalAddress) -> Option<usize> {
let offset = pa.raw().checked_sub(self.base.raw())?;
if !offset.is_multiple_of(PAGE_SIZE) {
return None;
}
let idx = offset / PAGE_SIZE;
(idx < self.total).then_some(idx)
}
fn address_of(&self, idx: usize) -> PhysicalAddress {
self.base.offset(idx)
}
pub fn get(&self, pa: PhysicalAddress) -> Option<&FrameUse> {
self.frames[self.index_of(pa)?].as_ref()
}
pub fn alloc(&mut self, usage: FrameUse) -> Option<PhysicalAddress> {
let idx = self.frames[..self.total].iter().position(Option::is_none)?;
self.frames[idx] = Some(usage);
self.free -= 1;
Some(self.address_of(idx))
}
/// Allocates `count` physically adjacent frames; frame `i` gets `usage(i)`.
pub fn alloc_contiguous(
&mut self,
count: usize,
usage: impl Fn(usize) -> FrameUse,
) -> Option<PhysicalAddress> {
if count == 0 {
return None;
}
let mut run = 0;
let mut start = None;
for idx in 0..self.total {
if self.frames[idx].is_some() {
run = 0;
continue;
}
run += 1;
if run == count {
start = Some(idx + 1 - count);
break;
}
}
let start = start?;
for i in 0..count {
self.frames[start + i] = Some(usage(i));
}
self.free -= count;
Some(self.address_of(start))
}
/// Frees an allocated frame and reports what it was used for.
pub fn free(&mut self, pa: PhysicalAddress) -> Option<FrameUse> {
let idx = self.index_of(pa)?;
let usage = self.frames[idx].take()?;
self.free += 1;
Some(usage)
}
/// Changes the use of an allocated frame (e.g. moving it to another
/// process). Fails on free frames.
pub fn retag(&mut self, pa: PhysicalAddress, usage: FrameUse) -> bool {
match self.index_of(pa).and_then(|i| self.frames[i].as_mut()) {
Some(slot) => {
*slot = usage;
true
}
None => false,
}
}
/// The frame mapped at `va` in `owner`, if `owner` holds it as a user page.
pub fn find_user(
&self,
owner: ProcessId,
va: VirtualAddress,
) -> Option<(PhysicalAddress, PagePerms)> {
if !va.raw().is_multiple_of(PAGE_SIZE) {
return None;
}
let page = u32::try_from(va.raw() / PAGE_SIZE).ok()?;
self.frames[..self.total]
.iter()
.enumerate()
.find_map(|(idx, f)| match f {
Some(FrameUse::User {
owner: o,
page: p,
perms,
}) if *o == owner && *p == page => Some((self.address_of(idx), *perms)),
_ => None,
})
}
/// Every allocated frame and its use.
pub fn iter(&self) -> impl Iterator<Item = (PhysicalAddress, &FrameUse)> + '_ {
self.frames[..self.total]
.iter()
.enumerate()
.filter_map(|(idx, f)| f.as_ref().map(|u| (self.address_of(idx), u)))
}
/// User pages of `owner` as (frame, virtual address) pairs.
pub fn user_frames_of(
&self,
owner: ProcessId,
) -> impl Iterator<Item = (PhysicalAddress, VirtualAddress)> + '_ {
self.frames[..self.total]
.iter()
.enumerate()
.filter_map(move |(idx, f)| match f {
Some(FrameUse::User { owner: o, page, .. }) if *o == owner => Some((
self.address_of(idx),
VirtualAddress::from_raw(*page as usize * PAGE_SIZE),
)),
_ => None,
})
}
/// Frees every user and page-table frame charged to `owner`; returns
/// how many were freed.
pub fn free_owned_by(&mut self, owner: ProcessId) -> usize {
let mut freed = 0;
for slot in &mut self.frames[..self.total] {
let owned = matches!(
slot,
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if *o == owner
);
if owned {
*slot = None;
freed += 1;
}
}
self.free += freed;
freed
}
}

View File

@@ -0,0 +1,195 @@
//! Bounded proofs over every reachable state of a small frame table.
//! Each harness starts from an arbitrary *well-formed* table (free count
//! matches the number of empty slots) and checks one operation.
use super::*;
use types::address::{PhysicalAddress, VirtualAddress};
use types::id::{ProcessId, RegionId};
use types::mapping::PagePerms;
const N: usize = 4;
const BASE: usize = 0x8400_0000;
fn any_perms() -> PagePerms {
match kani::any::<u8>() % 3 {
0 => PagePerms::ReadOnly,
1 => PagePerms::ReadWrite,
_ => PagePerms::ReadExecute,
}
}
fn any_use() -> FrameUse {
// Small id ranges keep owner collisions likely, which is where the
// interesting cases are.
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
match kani::any::<u8>() % 3 {
0 => FrameUse::User {
owner,
page: kani::any::<u32>() % 4,
perms: any_perms(),
},
1 => FrameUse::PageTable { owner },
_ => FrameUse::Region {
region: RegionId::from_raw(kani::any::<u16>() % 2),
},
}
}
fn any_slot() -> Option<FrameUse> {
if kani::any() { Some(any_use()) } else { None }
}
fn any_table() -> FrameTable<N> {
let frames = [any_slot(), any_slot(), any_slot(), any_slot()];
let free = frames.iter().filter(|f| f.is_none()).count();
FrameTable {
base: PhysicalAddress::from_raw(BASE),
total: N,
frames,
free,
}
}
fn well_formed(t: &FrameTable<N>) -> bool {
t.free == t.frames[..t.total].iter().filter(|f| f.is_none()).count()
}
fn any_pa() -> PhysicalAddress {
// Anything from below the pool to past its end, aligned or not.
PhysicalAddress::from_raw(BASE - 4096 + kani::any::<usize>() % ((N + 2) * 4096))
}
#[kani::proof]
#[kani::unwind(6)]
fn alloc_only_hands_out_free_frames() {
let mut t = any_table();
let before = t.frames;
let usage = any_use();
match t.alloc(usage) {
Some(pa) => {
let idx = (pa.raw() - BASE) / PAGE_SIZE;
assert!(idx < N);
assert!(before[idx].is_none());
assert!(t.frames[idx] == Some(usage));
for i in 0..N {
if i != idx {
assert!(t.frames[i] == before[i]);
}
}
}
None => {
assert!(before.iter().all(Option::is_some));
assert!(t.frames == before);
}
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn free_releases_exactly_one_allocated_frame_or_nothing() {
let mut t = any_table();
let before = t.frames;
let pa = any_pa();
match t.free(pa) {
Some(usage) => {
let idx = (pa.raw() - BASE) / PAGE_SIZE;
assert!(pa.raw() % PAGE_SIZE == 0);
assert!(before[idx] == Some(usage));
assert!(t.frames[idx].is_none());
for i in 0..N {
if i != idx {
assert!(t.frames[i] == before[i]);
}
}
}
None => assert!(t.frames == before),
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn retag_never_allocates() {
let mut t = any_table();
let before = t.frames;
let pa = any_pa();
let usage = any_use();
let changed = t.retag(pa, usage);
for i in 0..N {
if before[i].is_none() {
assert!(t.frames[i].is_none());
}
}
if !changed {
assert!(t.frames == before);
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn free_owned_by_spares_everyone_else() {
let mut t = any_table();
let before = t.frames;
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
t.free_owned_by(owner);
for i in 0..N {
let was_owned = matches!(
before[i],
Some(FrameUse::User { owner: o, .. } | FrameUse::PageTable { owner: o }) if o == owner
);
if was_owned {
assert!(t.frames[i].is_none());
} else {
assert!(t.frames[i] == before[i]);
}
}
assert!(well_formed(&t));
}
#[kani::proof]
#[kani::unwind(6)]
fn find_user_only_reports_the_owners_mapping() {
let t = any_table();
let owner = ProcessId::from_raw(kani::any::<u16>() % 3);
let va = VirtualAddress::from_raw(kani::any::<usize>() % (5 * 4096));
if let Some((pa, perms)) = t.find_user(owner, va) {
let idx = (pa.raw() - BASE) / PAGE_SIZE;
assert!(idx < N);
assert!(va.raw() % PAGE_SIZE == 0);
assert!(
t.frames[idx]
== Some(FrameUse::User {
owner,
page: (va.raw() / PAGE_SIZE) as u32,
perms,
})
);
}
}
#[kani::proof]
#[kani::unwind(6)]
fn alloc_contiguous_only_claims_a_free_run() {
let mut t = any_table();
let before = t.frames;
let count = kani::any::<usize>() % (N + 2);
let region = RegionId::from_raw(1);
match t.alloc_contiguous(count, |_| FrameUse::Region { region }) {
Some(pa) => {
let start = (pa.raw() - BASE) / PAGE_SIZE;
assert!(count > 0 && start + count <= N);
for i in 0..N {
if i >= start && i < start + count {
assert!(before[i].is_none());
assert!(t.frames[i] == Some(FrameUse::Region { region }));
} else {
assert!(t.frames[i] == before[i]);
}
}
}
None => assert!(t.frames == before),
}
assert!(well_formed(&t));
}

View File

@@ -0,0 +1,188 @@
use super::*;
use types::address::{PhysicalAddress, VirtualAddress};
use types::id::{ProcessId, RegionId};
use types::mapping::PagePerms;
const BASE: usize = 0x8400_0000;
fn table() -> FrameTable<8> {
FrameTable::new(PhysicalAddress::from_raw(BASE), 8)
}
fn pid(n: u16) -> ProcessId {
ProcessId::from_raw(n)
}
fn va(addr: usize) -> VirtualAddress {
VirtualAddress::from_raw(addr)
}
fn user(owner: u16, addr: usize) -> FrameUse {
FrameUse::user(pid(owner), va(addr), PagePerms::ReadWrite).unwrap()
}
#[test]
fn new_table_has_every_frame_free() {
let t = table();
assert_eq!(t.free_count(), 8);
assert_eq!(t.capacity(), 8);
}
#[test]
fn total_is_clamped_to_capacity() {
let t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 100);
assert_eq!(t.free_count(), 4);
}
#[test]
fn user_use_requires_page_aligned_va() {
assert!(FrameUse::user(pid(0), va(0x1234), PagePerms::ReadOnly).is_none());
assert!(FrameUse::user(pid(0), va(0x1000), PagePerms::ReadOnly).is_some());
}
#[test]
fn alloc_hands_out_distinct_frames_inside_the_pool() {
let mut t = table();
let a = t.alloc(user(0, 0x1000)).unwrap();
let b = t.alloc(user(0, 0x2000)).unwrap();
assert_ne!(a, b);
for pa in [a, b] {
assert!(pa.raw() >= BASE && pa.raw() < BASE + 8 * 4096);
assert_eq!(pa.raw() % 4096, 0);
}
assert_eq!(t.free_count(), 6);
}
#[test]
fn alloc_fails_when_exhausted() {
let mut t = table();
for i in 0..8 {
t.alloc(user(0, (i + 1) * 0x1000)).unwrap();
}
assert!(t.alloc(user(0, 0x9000)).is_none());
}
#[test]
fn free_returns_the_frame_and_its_use() {
let mut t = table();
let pa = t.alloc(user(3, 0x5000)).unwrap();
assert_eq!(t.free(pa), Some(user(3, 0x5000)));
assert_eq!(t.free_count(), 8);
}
#[test]
fn freeing_a_free_frame_is_rejected() {
let mut t = table();
let pa = t.alloc(user(0, 0x1000)).unwrap();
t.free(pa).unwrap();
assert_eq!(t.free(pa), None);
assert_eq!(t.free_count(), 8);
}
#[test]
fn freeing_outside_the_pool_or_unaligned_is_rejected() {
let mut t = table();
t.alloc(user(0, 0x1000)).unwrap();
assert_eq!(t.free(PhysicalAddress::from_raw(BASE - 4096)), None);
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 8 * 4096)), None);
assert_eq!(t.free(PhysicalAddress::from_raw(BASE + 1)), None);
assert_eq!(t.free_count(), 7);
}
#[test]
fn find_user_matches_owner_and_va() {
let mut t = table();
let pa = t.alloc(user(1, 0x3000)).unwrap();
assert_eq!(
t.find_user(pid(1), va(0x3000)),
Some((pa, PagePerms::ReadWrite))
);
assert_eq!(t.find_user(pid(2), va(0x3000)), None);
assert_eq!(t.find_user(pid(1), va(0x4000)), None);
}
#[test]
fn retag_moves_a_frame_to_a_new_use() {
let mut t = table();
let pa = t.alloc(user(1, 0x3000)).unwrap();
assert!(t.retag(pa, user(2, 0x7000)));
assert_eq!(t.find_user(pid(1), va(0x3000)), None);
assert_eq!(t.find_user(pid(2), va(0x7000)).map(|(p, _)| p), Some(pa));
}
#[test]
fn retag_of_a_free_frame_is_rejected() {
let mut t = table();
let pa = t.alloc(user(1, 0x3000)).unwrap();
t.free(pa);
assert!(!t.retag(pa, user(2, 0x7000)));
assert_eq!(t.free_count(), 8);
}
#[test]
fn alloc_contiguous_returns_adjacent_frames() {
let mut t = table();
let base = t
.alloc_contiguous(3, |_| FrameUse::Region {
region: RegionId::from_raw(0),
})
.unwrap();
assert_eq!(t.free_count(), 5);
for i in 0..3 {
assert_eq!(
t.get(base.offset(i)),
Some(&FrameUse::Region {
region: RegionId::from_raw(0)
})
);
}
}
#[test]
fn alloc_contiguous_fails_without_a_long_enough_run() {
let mut t: FrameTable<4> = FrameTable::new(PhysicalAddress::from_raw(BASE), 4);
let a = t.alloc(user(0, 0x1000)).unwrap();
let _b = t.alloc(user(0, 0x2000)).unwrap();
let _c = t.alloc(user(0, 0x3000)).unwrap();
t.free(a);
// free frames: 0 and 3 — no run of 2
assert!(
t.alloc_contiguous(2, |_| FrameUse::PageTable { owner: pid(0) })
.is_none()
);
assert_eq!(t.free_count(), 2);
}
#[test]
fn free_owned_by_releases_only_that_processes_frames() {
let mut t = table();
t.alloc(user(1, 0x1000)).unwrap();
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
let other = t.alloc(user(2, 0x1000)).unwrap();
let region = t
.alloc(FrameUse::Region {
region: RegionId::from_raw(1),
})
.unwrap();
assert_eq!(t.free_owned_by(pid(1)), 2);
assert_eq!(t.free_count(), 6);
assert!(t.get(other).is_some());
assert!(t.get(region).is_some());
}
#[test]
fn user_frames_of_lists_mappings_of_one_process() {
let mut t = table();
t.alloc(user(1, 0x1000)).unwrap();
t.alloc(user(2, 0x2000)).unwrap();
t.alloc(user(1, 0x3000)).unwrap();
t.alloc(FrameUse::PageTable { owner: pid(1) }).unwrap();
let mut vas = [0usize; 4];
let mut n = 0;
for (_, v) in t.user_frames_of(pid(1)) {
vas[n] = v.raw();
n += 1;
}
assert_eq!(&vas[..n], &[0x1000, 0x3000]);
}