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

@@ -1,9 +1,14 @@
use crate::hw::uart::Uart;
use crate::hw::virtio_blk::VirtioBlk;
use crate::{boot_fs, mmu};
use types::address::VirtualAddress;
use types::id::{ProcessId, ThreadId};
use types::mapping::PagePerms;
const PAGE_SIZE: usize = mmu::PAGE_SIZE;
const USER_VA_BASE: usize = 0x10000;
/// The stack sits just below the image and stops short of the null page.
const USER_STACK_PAGES: usize = USER_VA_BASE / PAGE_SIZE - 1;
const MAX_FILE_SIZE: usize = 64 * 1024;
pub struct LoadedProcess {
@@ -15,11 +20,23 @@ fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
pub fn load_init(
uart: &Uart,
blk: &mut VirtioBlk,
kernel_pt: &mmu::PageTable,
) -> (LoadedProcess, ThreadId) {
/// Scratch space for reading ELF files, allocated once from the boot heap.
fn elf_buffer() -> &'static mut [u8] {
static mut BUF: usize = 0;
// SAFETY: boot is single threaded; the buffer is kernel-only memory
// from the boot allocator and is never handed to a process.
unsafe {
if BUF == 0 {
BUF = mmu::allocator::alloc_zeroed_page();
for _ in 1..MAX_FILE_SIZE / PAGE_SIZE {
mmu::allocator::alloc_zeroed_page();
}
}
core::slice::from_raw_parts_mut(BUF as *mut u8, MAX_FILE_SIZE)
}
}
pub fn load_init(uart: &Uart, blk: &mut VirtioBlk) -> (LoadedProcess, ThreadId) {
let (pid, tid) = kern()
.boot(
types::bounded::Priority::DEFAULT,
@@ -27,10 +44,7 @@ pub fn load_init(
)
.unwrap_or_else(|_| crate::halt(uart, "boot failed"));
let entry = load_elf(blk, kernel_pt, pid, b"init.elf");
if entry == 0 {
crate::halt(uart, "init.elf not found");
}
let entry = load_elf(uart, blk, pid, b"init.elf");
setup_thread(tid, entry);
uart.write_str("k-os: init loaded\n");
@@ -40,17 +54,13 @@ pub fn load_init(
pub fn load_server(
uart: &Uart,
blk: &mut VirtioBlk,
kernel_pt: &mmu::PageTable,
filename: &[u8],
label: &str,
) -> LoadedProcess {
let pid = kern()
.create_process_internal()
.unwrap_or_else(|| crate::halt(uart, "process create failed"));
let entry = load_elf(blk, kernel_pt, pid, filename);
if entry == 0 {
crate::halt(uart, "elf not found");
}
let entry = load_elf(uart, blk, pid, filename);
let tid = kern()
.spawn_thread_internal(pid)
.unwrap_or_else(|| crate::halt(uart, "thread spawn failed"));
@@ -98,100 +108,89 @@ fn setup_thread(tid: ThreadId, entry: usize) {
}
}
fn load_elf(
blk: &mut VirtioBlk,
kernel_pt: &mmu::PageTable,
pid: ProcessId,
filename: &[u8],
) -> usize {
let buf_base = mmu::allocator::alloc_zeroed_page();
let file_pages = MAX_FILE_SIZE / 4096;
for _ in 1..file_pages {
mmu::allocator::alloc_zeroed_page();
/// Copies `data` into `pid`'s already mapped pages starting at `va`.
fn copy_to_user(pid: ProcessId, va: usize, data: &[u8]) -> bool {
let mut done = 0;
while done < data.len() {
let addr = va + done;
let page = addr & !(PAGE_SIZE - 1);
let offset = addr - page;
let Some(pa) = kern().user_page_frame(pid, VirtualAddress::from_raw(page)) else {
return false;
};
let len = (PAGE_SIZE - offset).min(data.len() - done);
// SAFETY: `pa` is a pool frame the kernel just mapped for `pid`;
// pool frames are identity mapped kernel RAM.
unsafe {
core::ptr::copy_nonoverlapping(
data.as_ptr().add(done),
(pa.raw() as *mut u8).add(offset),
len,
);
}
done += len;
}
let buf = unsafe { core::slice::from_raw_parts_mut(buf_base as *mut u8, MAX_FILE_SIZE) };
true
}
fn segment_perms(flags: elf::SegmentFlags) -> Option<PagePerms> {
match (flags.write, flags.execute) {
(true, true) => None,
(false, true) => Some(PagePerms::ReadExecute),
(true, false) => Some(PagePerms::ReadWrite),
(false, false) => Some(PagePerms::ReadOnly),
}
}
/// Loads `filename` into `pid` through the kernel's own mapping API, so
/// every page is tracked, charged and checked like any other. Halts on any
/// malformed or overlapping image: boot servers are trusted to be sane.
fn load_elf(uart: &Uart, blk: &mut VirtioBlk, pid: ProcessId, filename: &[u8]) -> usize {
let buf = elf_buffer();
let file_size = boot_fs::read_file_from_disk(blk, filename, buf).unwrap_or(0);
if file_size == 0 {
return 0;
crate::halt(uart, "ELF not found");
}
let elf_info = match elf::parse(&buf[..file_size]) {
Ok(info) => info,
Err(_) => return 0,
};
let user_pt = mmu::create_user_page_table(
kernel_pt,
types::address::PhysicalAddress::from_raw(0),
0,
types::address::VirtualAddress::from_raw(USER_VA_BASE),
);
let file = &buf[..file_size];
let elf_info = elf::parse(file).unwrap_or_else(|_| crate::halt(uart, "bad ELF"));
for segment in elf_info.segments() {
let page_start = segment.vaddr & !0xFFF;
let offset_in_page = segment.vaddr & 0xFFF;
let total_size = offset_in_page + segment.mem_size;
let seg_pages = (total_size + 4095) / 4096;
let perms =
segment_perms(segment.flags).unwrap_or_else(|| crate::halt(uart, "W+X segment"));
let page_start = segment.vaddr & !(PAGE_SIZE - 1);
let offset_in_page = segment.vaddr - page_start;
let pages = offset_in_page
.checked_add(segment.mem_size)
.map(|total| total.div_ceil(PAGE_SIZE))
.unwrap_or_else(|| crate::halt(uart, "segment too large"));
if pages == 0 {
continue;
}
for p in 0..seg_pages {
let page = mmu::allocator::alloc_zeroed_page();
let page_va = page_start + p * 4096;
let seg_file_start = if p == 0 { 0 } else { p * 4096 - offset_in_page };
let dest_off = if p == 0 { offset_in_page } else { 0 };
let copy_len = if seg_file_start < segment.file_size {
(segment.file_size - seg_file_start).min(4096 - dest_off)
} else {
0
};
kern()
.map_fresh_range(pid, VirtualAddress::from_raw(page_start), pages, perms)
.unwrap_or_else(|_| crate::halt(uart, "cannot map segment"));
if copy_len > 0 {
let src = segment.file_offset + seg_file_start;
if src + copy_len <= file_size {
unsafe {
core::ptr::copy_nonoverlapping(
buf.as_ptr().add(src),
(page as *mut u8).add(dest_off),
copy_len,
);
}
}
}
let flags = if segment.flags.execute {
mmu::Sv39Flags::user_rx()
} else if segment.flags.write {
mmu::Sv39Flags::user_rw()
} else {
mmu::Sv39Flags::user_rx()
};
mmu::map(
user_pt,
mmu::Mapping {
va: types::address::VirtualAddress::from_raw(page_va),
pa: types::address::PhysicalAddress::from_raw(page),
pages: 1,
flags,
},
);
let copy_len = segment.file_size.min(segment.mem_size);
let data = segment
.file_offset
.checked_add(copy_len)
.and_then(|end| file.get(segment.file_offset..end))
.unwrap_or_else(|| crate::halt(uart, "segment outside file"));
if !copy_to_user(pid, segment.vaddr, data) {
crate::halt(uart, "segment copy failed");
}
}
for i in 0..16usize {
let sp = mmu::allocator::alloc_zeroed_page();
mmu::map(
user_pt,
mmu::Mapping {
va: types::address::VirtualAddress::from_raw(USER_VA_BASE - (16 - i) * 4096),
pa: types::address::PhysicalAddress::from_raw(sp),
pages: 1,
flags: mmu::Sv39Flags::user_rw(),
},
);
}
let stack_bottom = USER_VA_BASE - USER_STACK_PAGES * PAGE_SIZE;
kern()
.platform_mut()
.set_page_table(pid, user_pt as *const mmu::PageTable as usize);
.map_fresh_range(
pid,
VirtualAddress::from_raw(stack_bottom),
USER_STACK_PAGES,
PagePerms::ReadWrite,
)
.unwrap_or_else(|_| crate::halt(uart, "cannot map stack"));
elf_info.entry_point
}