feat(kernel): per-process budgets (pages, endpoints, notifs, regions) carved from creator, returned on death; init owns pool
This commit is contained in:
80
crates/domain/src/budget.rs
Normal file
80
crates/domain/src/budget.rs
Normal 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]
|
||||
}
|
||||
35
crates/domain/src/budget_tests.rs
Normal file
35
crates/domain/src/budget_tests.rs
Normal 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);
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
#[cfg(kani)]
|
||||
pub mod proofs;
|
||||
|
||||
pub mod budget;
|
||||
pub mod capability;
|
||||
pub mod endpoint;
|
||||
pub mod error;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
33
crates/domain/src/proofs/budget.rs
Normal file
33
crates/domain/src/proofs/budget.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
mod budget;
|
||||
mod capability;
|
||||
mod delegation;
|
||||
mod thread;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user