feat(kernel): per-process budgets (pages, endpoints, notifs, regions) carved from creator, returned on death; init owns pool

This commit is contained in:
2026-10-27 15:08:58 +01:00
parent 7415bab7f5
commit 4049ba5007
46 changed files with 672 additions and 119 deletions

View File

@@ -0,0 +1,80 @@
//! What a process may hold: frames and kernel objects. A child's limits
//! are carved from its creator's unused budget and go back when it dies,
//! so the limits of all live processes never add up to more than init got.
use crate::memory::quota::Quota;
pub use types::budget::Limits;
#[cfg(test)]
#[path = "budget_tests.rs"]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Budget {
pub pages: Quota,
pub endpoints: Quota,
pub notifications: Quota,
pub regions: Quota,
}
impl Budget {
pub const EMPTY: Self = Self::new(Limits {
pages: 0,
endpoints: 0,
notifications: 0,
regions: 0,
});
pub const fn new(limits: Limits) -> Self {
Self {
pages: Quota::new(limits.pages),
endpoints: Quota::new(limits.endpoints),
notifications: Quota::new(limits.notifications),
regions: Quota::new(limits.regions),
}
}
pub fn limits(&self) -> Limits {
Limits {
pages: self.pages.limit(),
endpoints: self.endpoints.limit(),
notifications: self.notifications.limit(),
regions: self.regions.limit(),
}
}
fn parts(&mut self) -> [&mut Quota; 4] {
[
&mut self.pages,
&mut self.endpoints,
&mut self.notifications,
&mut self.regions,
]
}
/// Carves `child` out of the unused budget: all of it or nothing.
pub fn carve(&mut self, child: Limits) -> bool {
let amounts = amounts(child);
let fits = self.parts().iter().zip(amounts).all(|(q, n)| {
q.limit()
.checked_sub(n)
.is_some_and(|rest| rest >= q.used())
});
if fits {
for (q, n) in self.parts().into_iter().zip(amounts) {
q.carve(n);
}
}
fits
}
pub fn refund(&mut self, child: Limits) {
for (q, n) in self.parts().into_iter().zip(amounts(child)) {
q.refund(n);
}
}
}
fn amounts(l: Limits) -> [usize; 4] {
[l.pages, l.endpoints, l.notifications, l.regions]
}

View File

@@ -0,0 +1,35 @@
use super::{Budget, Limits};
const L: Limits = Limits {
pages: 10,
endpoints: 2,
notifications: 2,
regions: 1,
};
#[test]
fn a_carve_takes_all_parts_or_none() {
let mut b = Budget::new(L);
b.endpoints.charge(2);
let child = Limits {
pages: 1,
endpoints: 1,
..Limits::default()
};
assert!(!b.carve(child));
assert_eq!(b.limits(), L);
assert!(b.carve(Limits {
endpoints: 0,
..child
}));
assert_eq!(b.limits().pages, 9);
}
#[test]
fn a_refund_restores_what_was_carved() {
let mut b = Budget::new(L);
assert!(b.carve(L));
assert_eq!(b.limits(), Limits::default());
b.refund(L);
assert_eq!(b.limits(), L);
}

View File

@@ -17,6 +17,7 @@
#[cfg(kani)]
pub mod proofs;
pub mod budget;
pub mod capability;
pub mod endpoint;
pub mod error;

View File

@@ -1,5 +1,6 @@
/// Per-process page budget. Every frame charged to a process (user pages
/// and its page tables) counts against it.
/// A count against a limit: one of a process's budgets (see `budget`).
/// For pages, every frame charged to a process (user pages and its page
/// tables) counts against it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Quota {
used: usize,
@@ -7,8 +8,6 @@ pub struct Quota {
}
impl Quota {
pub const DEFAULT_LIMIT: usize = 1024;
pub const fn new(limit: usize) -> Self {
Self { used: 0, limit }
}
@@ -41,17 +40,19 @@ impl Quota {
}
}
pub fn set_limit(&mut self, limit: usize) -> bool {
if limit < self.used {
return false;
/// Gives up `amount` of the unused limit, for a child to hold.
pub fn carve(&mut self, amount: usize) -> bool {
match self.limit.checked_sub(amount) {
Some(rest) if rest >= self.used => {
self.limit = rest;
true
}
_ => false,
}
self.limit = limit;
true
}
}
impl Default for Quota {
fn default() -> Self {
Self::new(Self::DEFAULT_LIMIT)
/// Takes back a limit carved out earlier.
pub fn refund(&mut self, amount: usize) {
self.limit = self.limit.saturating_add(amount);
}
}

View File

@@ -28,10 +28,13 @@ fn uncharge_cannot_underflow() {
}
#[test]
fn limit_can_be_raised_but_not_below_usage() {
fn only_the_unused_limit_can_be_carved() {
let mut q = Quota::new(4);
q.charge(3);
assert!(!q.set_limit(2));
assert!(q.set_limit(10));
assert!(q.charge(7));
assert!(!q.carve(2));
assert_eq!(q.limit(), 4);
assert!(q.carve(1));
assert!(!q.charge(1));
q.refund(1);
assert!(q.charge(1));
}

View File

@@ -0,0 +1,33 @@
use crate::budget::{Budget, Limits};
fn any_limits() -> Limits {
let l = Limits {
pages: kani::any(),
endpoints: kani::any(),
notifications: kani::any(),
regions: kani::any(),
};
kani::assume(l.pages < 1 << 20 && l.endpoints < 1 << 20);
kani::assume(l.notifications < 1 << 20 && l.regions < 1 << 20);
l
}
/// A carve never lets parent and child together exceed what the parent
/// had, never strands the parent's usage, and a refund undoes it.
#[kani::proof]
fn carving_conserves_the_budget() {
let (have, child) = (any_limits(), any_limits());
let used: usize = kani::any();
let mut b = Budget::new(have);
b.pages.charge(used);
let before = b;
if b.carve(child) {
assert_eq!(b.limits().pages + child.pages, have.pages);
assert_eq!(b.limits().regions + child.regions, have.regions);
assert!(b.pages.used() <= b.pages.limit());
b.refund(child);
assert_eq!(b, before);
} else {
assert_eq!(b, before);
}
}

View File

@@ -1,3 +1,4 @@
mod budget;
mod capability;
mod delegation;
mod thread;

View File

@@ -62,9 +62,11 @@ pub enum Syscall {
AcknowledgeInterrupt {
irq: CapSlot,
},
/// The new process's `limits` are carved from the caller's budget.
CreateProcess {
create_cap: CapSlot,
image: DataRef,
limits: crate::budget::Limits,
},
Kill {
process: CapSlot,