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

@@ -2,6 +2,7 @@ use crate::hw::uart::Uart;
use crate::hw::virtio_blk::VirtioBlk;
use crate::{boot_fs, mmu};
use types::address::VirtualAddress;
use types::budget::Limits;
use types::id::{ProcessId, ThreadId};
use types::mapping::PagePerms;
@@ -52,14 +53,38 @@ pub fn load_init(uart: &Uart, blk: &mut VirtioBlk) -> LoadedProcess {
LoadedProcess { pid, tid }
}
/// The servers boot loads, their budgets carved from init's.
pub const STORAGE: Limits = Limits {
pages: 256,
endpoints: 2,
notifications: 2,
// A buffer per client (8) and the disk's DMA.
regions: 12,
};
/// The framebuffer alone is ~1000 pages at 1280x800.
pub const DISPLAY: Limits = Limits {
pages: 1280,
endpoints: 2,
notifications: 2,
regions: 4,
};
pub const INPUT: Limits = Limits {
pages: 64,
endpoints: 2,
notifications: 2,
regions: 4,
};
pub fn load_server(
uart: &Uart,
blk: &mut VirtioBlk,
init: ProcessId,
filename: &[u8],
label: &str,
limits: Limits,
) -> LoadedProcess {
let pid = kern()
.create_process_internal()
.create_process_internal(init, limits)
.unwrap_or_else(|| crate::halt(uart, "process create failed"));
let entry = load_elf(uart, blk, pid, filename);
let tid = kern()

View File

@@ -1,4 +1,4 @@
// Boot-time userspace setup: the display's page quota and init's BootInfo page.
// Boot-time userspace setup: init's BootInfo page.
use crate::hw::uart::Uart;
use types::address::VirtualAddress;
@@ -6,20 +6,10 @@ use types::bootinfo::{BOOT_INFO_VA, BootDevice, BootInfo, MAGIC, MAX_BOOT_DEVICE
use types::id::ProcessId;
use types::mapping::PagePerms;
/// The framebuffer alone is ~1000 pages at 1280x800.
const DISPLAY_PAGE_LIMIT: usize = 2048;
fn kern() -> &'static mut kernel::Kernel<crate::platform::RiscVPlatform> {
unsafe { &mut *crate::kernel_ptr() }
}
/// Lets the display server hold a framebuffer.
pub(crate) fn display_quota(uart: &Uart, display: ProcessId) {
kern()
.set_page_limit(display, DISPLAY_PAGE_LIMIT)
.unwrap_or_else(|_| crate::halt(uart, "display quota"));
}
/// Tells init, in a read-only page at BOOT_INFO_VA, which of its slots hold
/// each board device's capabilities.
pub(crate) fn boot_info(

View File

@@ -40,15 +40,29 @@ extern "C" fn kmain(hart: usize) -> ! {
boot::hardware::probe_block_device(&uart).unwrap_or_else(|| halt(&uart, "no block device"));
let init = boot::loader::load_init(&uart, &mut blk);
let storage = boot::loader::load_server(&uart, &mut blk, b"storage.elf", "storage");
let display = boot::loader::load_server(&uart, &mut blk, b"display.elf", "display");
let input = boot::loader::load_server(&uart, &mut blk, b"input.elf", "input");
use boot::loader::{DISPLAY, INPUT, STORAGE, load_server};
let storage = load_server(
&uart,
&mut blk,
init.pid,
b"storage.elf",
"storage",
STORAGE,
);
let display = load_server(
&uart,
&mut blk,
init.pid,
b"display.elf",
"display",
DISPLAY,
);
let input = load_server(&uart, &mut blk, init.pid, b"input.elf", "input", INPUT);
let board = board::devices().unwrap_or_else(|| halt(&uart, "bad board table"));
kern_mut()
.register_board(init.pid, &board)
.unwrap_or_else(|_| halt(&uart, "board registration failed"));
devices::display_quota(&uart, display.pid);
boot::caps::grant_all(&uart, init.pid, storage.pid, display.pid, input.pid);
// Last, so the servers' fixed slots stay where they expect them.
boot::caps::grant_console(&uart, &[storage, display, input]);

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,

View File

@@ -7,6 +7,7 @@ mod selftest;
use types::address::{EntryPoint, StackPointer, VirtualAddress};
use types::bounded::{CapSlot, PageCount};
use types::budget::Limits;
use types::permission::{Permission, PermissionSet};
use libkos::fs::Storage;
use libkos::syscall;
@@ -42,7 +43,12 @@ pub(crate) struct LoadedProcess {
pub(crate) entry_point: EntryPoint,
}
pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &[u8]) -> Option<LoadedProcess> {
pub(crate) fn load_binary(
storage: &mut Storage,
scratch: &mut [u8],
filename: &[u8],
limits: Limits,
) -> Option<LoadedProcess> {
let file_size = read_file(storage, scratch, filename)?;
let elf_data = &scratch[..file_size];
let elf_info = match elf::parse(elf_data) {
@@ -50,7 +56,7 @@ pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &
Err(_) => return None,
};
let process_cap = syscall::create_process(CREATE_CAP)?;
let process_cap = syscall::create_process(CREATE_CAP, limits)?;
for segment in elf_info.segments() {
let page_start = segment.vaddr & !0xFFF;
@@ -102,14 +108,25 @@ pub(crate) fn load_binary(storage: &mut Storage, scratch: &mut [u8], filename: &
struct ServerInfo {
filename: &'static [u8],
name: &'static str,
limits: Limits,
}
/// Enough for a small program: its image, stack and a little heap.
pub(crate) const SMALL: Limits = Limits {
pages: 64,
endpoints: 2,
notifications: 2,
regions: 2,
};
const SERVERS: &[ServerInfo] = &[
ServerInfo { filename: b"log.elf", name: "log" },
ServerInfo { filename: b"permis.elf", name: "permis" },
ServerInfo { filename: b"session.elf", name: "session" },
ServerInfo { filename: b"log.elf", name: "log", limits: SMALL },
ServerInfo { filename: b"permis.elf", name: "permis", limits: SMALL },
ServerInfo { filename: b"session.elf", name: "session", limits: SMALL },
];
const SNAKE: Limits = Limits { pages: 128, ..SMALL };
pub fn run() -> ! {
libkos::start::set_console(devices::console());
libkos::println!("init: started (pid={:#x})", syscall::get_process_id().raw());
@@ -142,7 +159,7 @@ pub fn run() -> ! {
for server in SERVERS {
libkos::print!("init: loading {}... ", server.name);
let loaded = match load_binary(&mut storage, scratch, server.filename) {
let loaded = match load_binary(&mut storage, scratch, server.filename, server.limits) {
Some(l) => l,
None => {
libkos::println!("SKIP");
@@ -166,7 +183,7 @@ pub fn run() -> ! {
}
libkos::print!("init: loading snake... ");
if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf") {
if let Some(snake) = load_binary(&mut storage, scratch, b"snake.elf", SNAKE) {
// A client only calls: Execute, never Read, so it cannot pose as
// the server and take other clients' requests.
let client = PermissionSet::empty().with(Permission::Execute);

View File

@@ -40,7 +40,7 @@ fn start_spinner(storage: &mut Storage, scratch: &mut [u8], page: CapSlot) -> Re
for i in 0..=THREADS {
word(i).store(0, Ordering::SeqCst);
}
let spinner = crate::load_binary(storage, scratch, b"smptest.elf").ok_or("cannot load smptest.elf")?;
let spinner = crate::load_binary(storage, scratch, b"smptest.elf", crate::SMALL).ok_or("cannot load smptest.elf")?;
let rw = PermissionSet::empty()
.with(Permission::Read)
.with(Permission::Write)

View File

@@ -94,6 +94,12 @@ pub fn decode(nr: usize, a: &[usize; 6]) -> Option<Syscall> {
SYS_CREATE_PROCESS => Syscall::CreateProcess {
create_cap: slot(a[0])?,
image: DataRef::empty(),
limits: domain::budget::Limits {
pages: a[1],
endpoints: a[2],
notifications: a[3],
regions: a[4],
},
},
SYS_KILL => Syscall::Kill {
process: slot(a[0])?,

100
crates/kernel/src/budget.rs Normal file
View File

@@ -0,0 +1,100 @@
//! Per-process budgets (see `domain::budget`): carved from the creator,
//! charged as a process takes frames and kernel objects, returned on death.
use domain::{
budget::{Budget, Limits},
capability::CapabilityTable,
error::KernelError,
memory::quota::Quota,
};
use platform::Platform;
use types::id::ProcessId;
use crate::Kernel;
/// Picks the part of a budget an object counts against.
pub(crate) type Part = fn(&mut Budget) -> &mut Quota;
pub(crate) const ENDPOINTS: Part = |b| &mut b.endpoints;
pub(crate) const NOTIFICATIONS: Part = |b| &mut b.notifications;
pub(crate) const REGIONS: Part = |b| &mut b.regions;
impl<P: Platform> Kernel<P> {
pub(crate) fn budget_mut(&mut self, pid: ProcessId) -> Result<&mut Budget, KernelError> {
self.budgets
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))
}
pub fn limits_of(&self, pid: ProcessId) -> Option<Limits> {
self.processes.get(pid).ok()?;
self.budgets.get(pid.raw() as usize).map(Budget::limits)
}
/// A new process holding `limits`, carved from `parent`'s unused budget;
/// only boot makes one without a parent.
pub(crate) fn new_process(
&mut self,
parent: Option<ProcessId>,
limits: Limits,
) -> Result<ProcessId, KernelError> {
if let Some(p) = parent {
self.processes.get(p)?;
if !self.budget_mut(p)?.carve(limits) {
return Err(KernelError::OutOfMemory);
}
}
let pid = match self.processes.create(CapabilityTable::new(), None) {
Ok(pid) => pid,
Err(e) => {
if let Some(p) = parent {
self.budget_mut(p)?.refund(limits);
}
return Err(e);
}
};
*self.budget_mut(pid)? = Budget::new(limits);
if let Some(slot) = self.parents.get_mut(pid.raw() as usize) {
*slot = parent;
}
if let Err(e) = self.create_address_space(pid) {
self.release_budget(pid);
let _ = self.processes.destroy(pid);
return Err(e);
}
Ok(pid)
}
/// Gives a dead process's limits back to its parent; its children are
/// carved from what it held, so they pass to that parent too.
pub(crate) fn release_budget(&mut self, pid: ProcessId) {
let i = pid.raw() as usize;
let parent = self.parents.get_mut(i).and_then(Option::take);
let limits = self
.budgets
.get_mut(i)
.map(|b| core::mem::replace(b, Budget::EMPTY).limits());
if let (Some(p), Some(limits)) = (parent, limits)
&& let Ok(b) = self.budget_mut(p)
{
b.refund(limits);
}
for slot in self.parents.iter_mut().filter(|s| **s == Some(pid)) {
*slot = parent;
}
}
pub(crate) fn charge_object(&mut self, pid: ProcessId, part: Part) -> Result<(), KernelError> {
if part(self.budget_mut(pid)?).charge(1) {
Ok(())
} else {
Err(KernelError::NoFreeSlot)
}
}
pub(crate) fn uncharge_object(&mut self, pid: ProcessId, part: Part) {
if let Ok(b) = self.budget_mut(pid) {
part(b).uncharge(1);
}
}
}

View File

@@ -16,6 +16,7 @@
pub mod abi;
mod board;
mod budget;
mod captree;
pub mod interrupt;
mod memory;
@@ -33,11 +34,12 @@ pub use syscall::{SyscallOutcome, SyscallResult};
mod tests;
use domain::{
capability::{Capability, CapabilityTable},
budget::{Budget, Limits},
capability::Capability,
endpoint::EndpointTable,
error::KernelError,
irq::IrqRoutingTable,
memory::{RegionTable, frame::FrameTable, quota::Quota},
memory::{RegionTable, frame::FrameTable},
notification::NotificationTable,
process::{MAX_PROCESSES, ProcessTable},
resource::Resource,
@@ -61,7 +63,9 @@ pub struct Kernel<P: Platform> {
endpoints: EndpointTable,
scheduler: MultiCoreScheduler,
frames: FrameTable,
quotas: [Quota; MAX_PROCESSES],
budgets: [Budget; MAX_PROCESSES],
/// Whose budget each process was carved from (see budget.rs).
parents: [Option<ProcessId>; MAX_PROCESSES],
regions: RegionTable,
irqs: IrqRoutingTable,
notifications: NotificationTable,
@@ -85,7 +89,8 @@ impl<P: Platform> Kernel<P> {
endpoints: EndpointTable::new(),
scheduler: MultiCoreScheduler::new(num_cores),
frames: FrameTable::new(pool_base, pool_pages),
quotas: [Quota::default(); MAX_PROCESSES],
budgets: [Budget::EMPTY; MAX_PROCESSES],
parents: [None; MAX_PROCESSES],
regions: RegionTable::new(),
irqs: IrqRoutingTable::new(),
notifications: NotificationTable::new(),
@@ -118,7 +123,8 @@ impl<P: Platform> Kernel<P> {
addr_of_mut!((*slot).endpoints).write(EndpointTable::new());
addr_of_mut!((*slot).scheduler).write(MultiCoreScheduler::new(num_cores));
addr_of_mut!((*slot).frames).write(FrameTable::new(pool_base, pool_pages));
addr_of_mut!((*slot).quotas).write([Quota::default(); MAX_PROCESSES]);
addr_of_mut!((*slot).budgets).write([Budget::EMPTY; MAX_PROCESSES]);
addr_of_mut!((*slot).parents).write([None; MAX_PROCESSES]);
addr_of_mut!((*slot).regions).write(RegionTable::new());
addr_of_mut!((*slot).irqs).write(IrqRoutingTable::new());
addr_of_mut!((*slot).notifications).write(NotificationTable::new());
@@ -135,11 +141,14 @@ impl<P: Platform> Kernel<P> {
priority: Priority,
time_slice: TimeSlice,
) -> Result<(ProcessId, ThreadId), KernelError> {
let pid = self.processes.create(CapabilityTable::new(), None)?;
if let Err(e) = self.create_address_space(pid) {
let _ = self.processes.destroy(pid);
return Err(e);
}
// Init holds every frame and every kernel object to start with.
let everything = Limits {
pages: self.frames.free_count(),
endpoints: domain::endpoint::MAX_ENDPOINTS,
notifications: domain::notification::MAX_NOTIFICATIONS,
regions: domain::memory::MAX_REGIONS,
};
let pid = self.new_process(None, everything)?;
let tid = self.threads.spawn(pid, priority, time_slice, None)?;
self.processes.get_mut(pid)?.add_thread(tid)?;
@@ -211,13 +220,13 @@ impl<P: Platform> Kernel<P> {
self.scheduler.next(core)
}
pub fn create_process_internal(&mut self) -> Option<ProcessId> {
let pid = self.processes.create(CapabilityTable::new(), None).ok()?;
if self.create_address_space(pid).is_err() {
let _ = self.processes.destroy(pid);
return None;
}
Some(pid)
/// A process for boot code to load, its `limits` carved from `parent`.
pub fn create_process_internal(
&mut self,
parent: ProcessId,
limits: Limits,
) -> Option<ProcessId> {
self.new_process(Some(parent), limits).ok()
}
pub fn spawn_thread_internal(&mut self, pid: ProcessId) -> Option<ThreadId> {
@@ -237,7 +246,11 @@ impl<P: Platform> Kernel<P> {
owner: ProcessId,
) -> Result<types::id::EndpointId, KernelError> {
self.processes.get(owner)?;
self.endpoints.create(owner).ok_or(KernelError::NoFreeSlot)
self.charge_object(owner, budget::ENDPOINTS)?;
self.endpoints
.create(owner)
.ok_or(KernelError::NoFreeSlot)
.inspect_err(|_| self.uncharge_object(owner, budget::ENDPOINTS))
}
pub fn grant_capability(

View File

@@ -91,22 +91,13 @@ impl<P: Platform> Kernel<P> {
}
fn quota_mut(&mut self, pid: ProcessId) -> Result<&mut Quota, KernelError> {
self.quotas
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))
self.budget_mut(pid).map(|b| &mut b.pages)
}
pub fn page_usage(&self, pid: ProcessId) -> usize {
self.quotas.get(pid.raw() as usize).map_or(0, Quota::used)
}
pub fn set_page_limit(&mut self, pid: ProcessId, limit: usize) -> Result<(), KernelError> {
self.processes.get(pid)?;
if self.quota_mut(pid)?.set_limit(limit) {
Ok(())
} else {
Err(KernelError::OutOfMemory)
}
self.budgets
.get(pid.raw() as usize)
.map_or(0, |b| b.pages.used())
}
pub fn free_page_count(&self) -> usize {
@@ -121,10 +112,11 @@ impl<P: Platform> Kernel<P> {
pa: PhysicalAddress,
perms: PagePerms,
) -> Result<(), KernelError> {
let quota = self
.quotas
let quota = &mut self
.budgets
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))?;
.ok_or(KernelError::InvalidProcess(pid))?
.pages;
let mut source = ChargedFrames {
frames: &mut self.frames,
quota,
@@ -136,11 +128,11 @@ impl<P: Platform> Kernel<P> {
}
pub(crate) fn create_address_space(&mut self, pid: ProcessId) -> Result<(), KernelError> {
let quota = self
.quotas
let quota = &mut self
.budgets
.get_mut(pid.raw() as usize)
.ok_or(KernelError::InvalidProcess(pid))?;
*quota = Quota::default();
.ok_or(KernelError::InvalidProcess(pid))?
.pages;
let mut source = ChargedFrames {
frames: &mut self.frames,
quota,
@@ -152,9 +144,6 @@ impl<P: Platform> Kernel<P> {
.map_err(map_error);
if created.is_err() {
self.frames.free_owned_by(pid);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
*q = Quota::default();
}
}
created
}
@@ -188,8 +177,8 @@ impl<P: Platform> Kernel<P> {
if let Some((pa, _)) = self.user_frame(pid, va) {
self.platform.unmap_page(pid, va);
self.frames.free(pa);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
q.uncharge(1);
if let Some(b) = self.budgets.get_mut(pid.raw() as usize) {
b.pages.uncharge(1);
}
}
}
@@ -200,9 +189,6 @@ impl<P: Platform> Kernel<P> {
self.teardown_regions(pid);
self.platform.destroy_address_space(pid);
self.frames.free_owned_by(pid);
if let Some(q) = self.quotas.get_mut(pid.raw() as usize) {
*q = Quota::default();
}
if let Some(c) = self.heap_cursors.get_mut(pid.raw() as usize) {
*c = 0;
}

View File

@@ -28,7 +28,9 @@ impl<P: Platform> Kernel<P> {
self.processes.get(owner)?;
let id = self.regions.next_free_id().ok_or(KernelError::NoFreeSlot)?;
let count = pages.value() as usize;
self.charge_object(owner, crate::budget::REGIONS)?;
if !self.quota_mut(owner)?.charge(count) {
self.uncharge_object(owner, crate::budget::REGIONS);
return Err(KernelError::OutOfMemory);
}
let Some(base) = self
@@ -36,6 +38,7 @@ impl<P: Platform> Kernel<P> {
.alloc_contiguous(count, |_| FrameUse::Region { region: id })
else {
self.quota_mut(owner)?.uncharge(count);
self.uncharge_object(owner, crate::budget::REGIONS);
return Err(KernelError::OutOfMemory);
};
for i in 0..count {
@@ -76,9 +79,11 @@ impl<P: Platform> Kernel<P> {
if overlaps {
return Err(KernelError::AddressInUse);
}
self.charge_object(owner, crate::budget::REGIONS)?;
self.regions
.insert(Region::new(owner, RegionBacking::Device, base, pages))
.ok_or(KernelError::NoFreeSlot)
.inspect_err(|_| self.uncharge_object(owner, crate::budget::REGIONS))
}
pub fn region_base(&self, id: RegionId) -> Option<PhysicalAddress> {

View File

@@ -70,7 +70,13 @@ fn any_slot() -> CapSlot {
fn small_world() -> Kernel<KaniPlatform> {
let mut k = Kernel::new(KaniPlatform, PhysicalAddress::from_raw(0x8400_0000), 6);
let (init, _) = k.boot(Priority::DEFAULT, TimeSlice::DEFAULT).unwrap();
let b = k.create_process_internal().unwrap();
let small = domain::budget::Limits {
pages: 2,
endpoints: 1,
notifications: 1,
regions: 1,
};
let b = k.create_process_internal(init, small).unwrap();
k.spawn_thread_internal(b).unwrap();
let ep = k.create_endpoint(init).unwrap();
let n = k.create_notification(b).unwrap();

View File

@@ -301,6 +301,7 @@ impl<P: Platform> Kernel<P> {
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
Err(e) => {
self.endpoints.destroy(id);
self.uncharge_object(pid, crate::budget::ENDPOINTS);
SyscallOutcome::Complete(SyscallResult::Error(e))
}
}

View File

@@ -38,9 +38,11 @@ impl<P: Platform> Kernel<P> {
pub fn create_notification(&mut self, owner: ProcessId) -> Result<NotificationId, KernelError> {
self.processes.get(owner)?;
self.charge_object(owner, crate::budget::NOTIFICATIONS)?;
self.notifications
.create(owner)
.ok_or(KernelError::NoFreeSlot)
.inspect_err(|_| self.uncharge_object(owner, crate::budget::NOTIFICATIONS))
}
pub(crate) fn create_notification_cap(
@@ -51,6 +53,7 @@ impl<P: Platform> Kernel<P> {
self.grant_cap(pid, Resource::Notification(id), rw_delegate())
.inspect_err(|_| {
self.notifications.destroy(id);
self.uncharge_object(pid, crate::budget::NOTIFICATIONS);
})
}

View File

@@ -41,6 +41,7 @@ impl<P: Platform> Kernel<P> {
caller_pid: types::id::ProcessId,
create_cap: types::bounded::CapSlot,
_image: types::address::DataRef,
limits: domain::budget::Limits,
) -> SyscallOutcome {
if let Err(e) = self.check_cap(
caller_pid,
@@ -50,19 +51,11 @@ impl<P: Platform> Kernel<P> {
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
let new_pid = match self
.processes
.create(domain::capability::CapabilityTable::new(), None)
{
let new_pid = match self.new_process(Some(caller_pid), limits) {
Ok(p) => p,
Err(e) => return SyscallOutcome::Complete(SyscallResult::Error(e)),
};
if let Err(e) = self.create_address_space(new_pid) {
let _ = self.processes.destroy(new_pid);
return SyscallOutcome::Complete(SyscallResult::Error(e));
}
match self.grant_cap(
caller_pid,
domain::resource::Resource::Process(new_pid),
@@ -75,9 +68,7 @@ impl<P: Platform> Kernel<P> {
) {
Ok(slot) => SyscallOutcome::Complete(SyscallResult::Slot(slot)),
Err(e) => {
// Rollback: destroy orphaned process
self.teardown_memory(new_pid);
let _ = self.processes.destroy(new_pid);
self.destroy_process(new_pid);
SyscallOutcome::Complete(SyscallResult::Error(e))
}
}

View File

@@ -83,9 +83,11 @@ impl<P: Platform> Kernel<P> {
Syscall::SpawnThread { entry, stack, arg } => {
self.handle_spawn_thread(pid, entry, stack, arg)
}
Syscall::CreateProcess { create_cap, image } => {
self.handle_create_process(pid, create_cap, image)
}
Syscall::CreateProcess {
create_cap,
image,
limits,
} => self.handle_create_process(pid, create_cap, image, limits),
Syscall::Kill { process } => self.handle_kill(pid, process),
Syscall::Revoke { slot } => self.handle_revoke(pid, slot),
Syscall::DropCap { slot } => self.handle_drop_cap(pid, slot),

View File

@@ -28,6 +28,7 @@ impl<P: Platform> Kernel<P> {
self.destroy_notifications_owned_by(pid);
self.irqs.clear_owned_by(pid);
self.teardown_memory(pid);
self.release_budget(pid);
let _ = self.processes.destroy(pid);
}

View File

@@ -42,7 +42,7 @@ fn crowded(
let parent = k.cap_ref(init, root);
let mut pids = std::vec::Vec::new();
for _ in 0..n {
let Some(pid) = k.create_process_internal() else {
let Some(pid) = k.create_process_for_test() else {
break;
};
while k
@@ -173,7 +173,7 @@ fn bench_create_spawn_kill() {
let rounds = 2_000;
let start = Instant::now();
for _ in 0..rounds {
let pid = k.create_process_internal().unwrap();
let pid = k.create_process_for_test().unwrap();
k.spawn_thread_internal(pid).unwrap();
k.destroy_process(pid);
}

View File

@@ -109,7 +109,7 @@ fn halt_needs_execute_on_the_cap() {
#[should_panic(expected = "halt")]
fn any_holder_of_the_cap_may_halt_not_just_init() {
let (mut k, init) = booted();
let other = k.create_process_internal().unwrap();
let other = k.create_process_for_test().unwrap();
let tid = k.spawn_thread_for_test(other, Priority::DEFAULT, TimeSlice::DEFAULT);
k.grant_cap_for_test(
other,
@@ -207,7 +207,7 @@ fn granting_needs_a_registered_board() {
#[test]
fn granting_needs_the_regions_to_be_inits() {
let (mut k, init) = kernel_with_process();
let other = k.create_process_internal().unwrap();
let other = k.create_process_for_test().unwrap();
k.register_board(other, &board()).unwrap();
assert!(matches!(
k.grant_boot_authority(init, &board(), &mut [None; 2]),

View File

@@ -0,0 +1,152 @@
//! Budgets: carved from the creator, enforced, and returned on death.
use super::helpers::CHILD_LIMITS;
use super::*;
use crate::{SyscallOutcome, SyscallResult};
use domain::budget::Limits;
use domain::error::KernelError;
use domain::syscall::Syscall;
use types::address::DataRef;
use types::bounded::{CapSlot, PageCount};
fn create(k: &mut Kernel<MockPlatform>, limits: Limits) -> SyscallResult {
match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits,
}) {
SyscallOutcome::Complete(r) => r,
_ => panic!("create process blocked"),
}
}
fn minus(a: Limits, b: Limits) -> Limits {
Limits {
pages: a.pages - b.pages,
endpoints: a.endpoints - b.endpoints,
notifications: a.notifications - b.notifications,
regions: a.regions - b.regions,
}
}
#[test]
fn init_starts_with_the_whole_pool() {
let (k, init) = kernel_with_process();
let l = k.limits_of(init).unwrap();
assert_eq!(l.pages, k.free_page_count() + k.page_usage(init));
assert_eq!(l.endpoints, domain::endpoint::MAX_ENDPOINTS);
}
#[test]
fn a_child_is_carved_from_its_creator() {
let (mut k, init) = kernel_with_process();
let before = k.limits_of(init).unwrap();
let SyscallResult::Slot(_) = create(&mut k, CHILD_LIMITS) else {
panic!("create failed");
};
assert_eq!(k.limits_of(init).unwrap(), minus(before, CHILD_LIMITS));
}
#[test]
fn a_child_gets_no_more_than_its_creator_has_spare() {
let (mut k, init) = kernel_with_process();
let before = k.limits_of(init).unwrap();
// The creator's own pages are in use, so its whole limit is not spare.
let greedy = Limits {
pages: before.pages,
..CHILD_LIMITS
};
assert!(matches!(
create(&mut k, greedy),
SyscallResult::Error(KernelError::OutOfMemory)
));
let greedy = Limits {
endpoints: before.endpoints + 1,
..CHILD_LIMITS
};
assert!(matches!(
create(&mut k, greedy),
SyscallResult::Error(KernelError::OutOfMemory)
));
assert_eq!(k.limits_of(init).unwrap(), before);
}
#[test]
fn kernel_objects_count_against_the_owner() {
let (mut k, init) = kernel_with_process();
let one = Limits {
pages: 8,
endpoints: 1,
notifications: 1,
regions: 1,
};
let child = k.create_process_internal(init, one).unwrap();
assert!(k.create_endpoint(child).is_ok());
assert!(matches!(
k.create_endpoint(child),
Err(KernelError::NoFreeSlot)
));
assert!(k.create_notification(child).is_ok());
assert!(matches!(
k.create_notification(child),
Err(KernelError::NoFreeSlot)
));
let page = PageCount::new(1).unwrap();
assert!(k.create_pool_region(child, page).is_ok());
assert!(matches!(
k.create_pool_region(child, page),
Err(KernelError::NoFreeSlot)
));
}
#[test]
fn a_failed_region_leaves_no_charge_behind() {
let (mut k, init) = kernel_with_process();
let one = Limits {
pages: 2,
endpoints: 0,
notifications: 0,
regions: 1,
};
let child = k.create_process_internal(init, one).unwrap();
// More pages than the child may hold: the region slot stays free.
assert!(matches!(
k.create_pool_region(child, PageCount::new(8).unwrap()),
Err(KernelError::OutOfMemory)
));
assert!(
k.create_pool_region(child, PageCount::new(1).unwrap())
.is_ok()
);
}
#[test]
fn death_returns_the_budget_to_the_parent() {
let (mut k, init) = kernel_with_process();
let before = k.limits_of(init).unwrap();
let child = k.create_process_internal(init, CHILD_LIMITS).unwrap();
k.create_endpoint(child).unwrap();
k.destroy_process(child);
assert_eq!(k.limits_of(init).unwrap(), before);
}
#[test]
fn an_orphan_returns_its_budget_to_the_grandparent() {
let (mut k, init) = kernel_with_process();
let before = k.limits_of(init).unwrap();
let a = k.create_process_internal(init, CHILD_LIMITS).unwrap();
let small = Limits {
pages: 4,
endpoints: 1,
notifications: 1,
regions: 1,
};
let b = k.create_process_internal(a, small).unwrap();
k.destroy_process(a);
assert_eq!(k.limits_of(init).unwrap(), minus(before, small));
// a's slot may be reused; b's budget must still find its way home.
let c = k.create_process_internal(init, CHILD_LIMITS).unwrap();
k.destroy_process(b);
k.destroy_process(c);
assert_eq!(k.limits_of(init).unwrap(), before);
}

View File

@@ -28,6 +28,7 @@ fn delegate_with_valid_cap_succeeds() {
let target_slot = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("expected slot"),

View File

@@ -163,6 +163,7 @@ impl World {
Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
},
);
self.pids[i] = self.k.process_of_cap_for_test(cap);

View File

@@ -24,7 +24,32 @@ pub fn kernel_with_process() -> (Kernel<MockPlatform>, ProcessId) {
(kernel, pid)
}
/// What a test's child processes get: small, so several fit in the pool.
pub const CHILD_LIMITS: domain::budget::Limits = domain::budget::Limits {
pages: 32,
endpoints: 4,
notifications: 4,
regions: 2,
};
impl Kernel<MockPlatform> {
/// Shrinks `pid`'s page limit to `limit` (never below its usage).
pub fn cap_pages_for_test(&mut self, pid: ProcessId, limit: usize) {
let pages = &mut self.budgets[pid.raw() as usize].pages;
assert!(pages.carve(pages.limit() - limit));
}
/// A process outside any budget tree, as tests used to get by default.
pub fn create_process_for_test(&mut self) -> Option<ProcessId> {
let limits = domain::budget::Limits {
pages: 1024,
endpoints: domain::endpoint::MAX_ENDPOINTS,
notifications: domain::notification::MAX_NOTIFICATIONS,
regions: domain::memory::MAX_REGIONS,
};
self.new_process(None, limits).ok()
}
pub fn spawn_thread_for_test(
&mut self,
pid: ProcessId,

View File

@@ -66,6 +66,7 @@ impl World {
caps[i] = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
@@ -164,6 +165,7 @@ impl World {
let fresh = match self.k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("re-create process"),

View File

@@ -42,6 +42,7 @@ impl Ipc {
match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(_)) => {}
_ => panic!("create process"),
@@ -487,7 +488,7 @@ fn a_reply_never_reaches_a_thread_that_reused_the_callers_id() {
let mut t = Ipc::new();
let (ta, tb, b, b_recv) = (t.ta, t.tb, t.b, t.b_recv);
// A second client process C calls, then dies while B holds the reply.
let c = t.k.create_process_internal().unwrap();
let c = t.k.create_process_for_test().unwrap();
let tc =
t.k.spawn_thread_for_test(c, Priority::DEFAULT, TimeSlice::DEFAULT);
let c_send =

View File

@@ -64,8 +64,8 @@ impl World {
fn new() -> Self {
let (mut k, p0) = kernel_with_process();
let t0 = k.current_thread().unwrap();
let p1 = k.create_process_internal().unwrap();
let p2 = k.create_process_internal().unwrap();
let p1 = k.create_process_for_test().unwrap();
let p2 = k.create_process_for_test().unwrap();
// Endpoints and the notification belong to init, which never dies.
let eps = [
k.create_endpoint(p0).unwrap(),
@@ -168,7 +168,7 @@ impl World {
/// Kills process `pi` and brings it back with fresh threads and caps.
fn reincarnate(&mut self, pi: usize) {
self.k.destroy_process(self.procs[pi].pid);
self.procs[pi].pid = self.k.create_process_internal().unwrap();
self.procs[pi].pid = self.k.create_process_for_test().unwrap();
let t0 = self.procs[0].threads[0];
Self::equip(
&mut self.k,

View File

@@ -61,6 +61,7 @@ fn two_processes() -> (
let cap_b = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
@@ -219,7 +220,7 @@ fn page_tables_count_against_the_process_quota() {
let (mut k, pid) = kernel_with_process();
let used = k.page_usage(pid);
// Room for exactly one data page plus the leaf table it needs.
k.set_page_limit(pid, used + 2).unwrap();
k.cap_pages_for_test(pid, used + 2);
assert!(request(&mut k, 1).is_some());
assert!(request(&mut k, 1).is_none(), "quota should be exhausted");
}
@@ -231,6 +232,7 @@ fn killing_a_process_returns_its_page_tables_too() {
let cap_b = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),
@@ -260,7 +262,7 @@ fn map_page_fails_cleanly_when_target_quota_is_full() {
let (mut k, pid_a, _, pid_b, _, cap_b) = two_processes();
let src = request(&mut k, 1).unwrap();
let used = k.page_usage(pid_b);
k.set_page_limit(pid_b, used).unwrap();
k.cap_pages_for_test(pid_b, used);
assert!(!map_page(&mut k, cap_b, src, 0x10000));
// the page stays with the caller
assert!(k.platform_mut().mapping(pid_a, src).is_some());

View File

@@ -47,6 +47,7 @@ fn create_process_with_cap_succeeds() {
let outcome = k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
});
assert!(matches!(
outcome,
@@ -61,6 +62,7 @@ fn create_process_without_cap_fails() {
let outcome = k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(5).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
});
assert!(matches!(
outcome,
@@ -75,6 +77,7 @@ fn kill_removes_process() {
let slot = match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("expected slot"),

View File

@@ -16,6 +16,7 @@ fn create_process(k: &mut Kernel<MockPlatform>) -> CapSlot {
match k.handle_syscall(Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
}) {
SyscallOutcome::Complete(SyscallResult::Slot(s)) => s,
_ => panic!("create process"),

View File

@@ -70,7 +70,7 @@ fn a_lying_page_table_cannot_hand_out_another_process_frame() {
// The kernel finds a page through the platform's page table but only
// believes the answer if its own frame table agrees.
let (mut k, a) = kernel_with_process();
let b = k.create_process_internal().unwrap();
let b = k.create_process_for_test().unwrap();
k.map_fresh_range(
b,
VirtualAddress::from_raw(0x50000),

View File

@@ -4,6 +4,7 @@ mod mock_platform;
mod abi;
mod bench;
mod board;
mod budget;
mod capability;
mod captree_model;
mod exit;

View File

@@ -59,6 +59,7 @@ impl Tree {
let create = || Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
};
let a_to_b = slot_of(k.handle_syscall(create()));
let a_to_c = slot_of(k.handle_syscall(create()));

View File

@@ -96,7 +96,7 @@ fn a_busy_core_is_not_kicked_for_an_equal_priority_thread() {
#[test]
fn killing_a_thread_running_on_another_core_stops_that_core() {
let (mut k, init) = smp(2);
let child = k.create_process_internal().unwrap();
let child = k.create_process_for_test().unwrap();
let t = k.spawn_thread_internal(child).unwrap();
on(&mut k, core(1));
k.platform_mut().take_ipi(core(1));

View File

@@ -319,7 +319,7 @@ fn new_round(k: &mut Kernel<MockPlatform>, init: ProcessId, init_thread: ThreadI
let _ = k.delete_cap(init, slot);
}
}
let Some(hub) = k.create_process_internal() else {
let Some(hub) = k.create_process_for_test() else {
if std::env::var("STORM_DEBUG").is_ok() {
std::eprintln!("no hub: free pages {}", k.free_page_count());
}
@@ -346,6 +346,7 @@ fn new_round(k: &mut Kernel<MockPlatform>, init: ProcessId, init_thread: ThreadI
k.handle_syscall(domain::syscall::Syscall::CreateProcess {
create_cap: CapSlot::new(0).unwrap(),
image: DataRef::empty(),
limits: super::helpers::CHILD_LIMITS,
})
{
let pid = k.process_of_cap_for_test(cap);

View File

@@ -3,6 +3,7 @@
use super::{call0, raw, slot_of};
use types::address::{EntryPoint, StackPointer};
use types::bounded::CapSlot;
use types::budget::Limits;
use types::id::ProcessId;
use types::syscall_abi::*;
use types::time::Ticks;
@@ -38,10 +39,18 @@ pub fn get_process_id() -> ProcessId {
ProcessId::from_raw(call0(SYS_GET_PROCESS_ID).map_or(0, |r| r[1]) as u16)
}
pub fn create_process(create_cap: CapSlot) -> Option<CapSlot> {
/// The new process's `limits` come out of this process's own budget.
pub fn create_process(create_cap: CapSlot, limits: Limits) -> Option<CapSlot> {
slot_of(raw(
SYS_CREATE_PROCESS,
[create_cap.value() as usize, 0, 0, 0, 0, 0],
[
create_cap.value() as usize,
limits.pages,
limits.endpoints,
limits.notifications,
limits.regions,
0,
],
))
.ok()
}

View File

@@ -1,10 +1,19 @@
use server_framework::Server;
use types::bounded::CapSlot;
use types::budget::Limits;
use crate::protocol::{AppId, SessionRequest, SessionResponse};
const MAX_APPS: usize = 16;
/// What each launched app may hold, out of the session server's budget.
const APP_LIMITS: Limits = Limits {
pages: 256,
endpoints: 2,
notifications: 2,
regions: 2,
};
#[derive(Clone, Copy)]
pub struct AppEntry {
pub name: &'static str,
@@ -77,7 +86,10 @@ impl SessionServer {
if self.running_mut(app).is_none_or(|r| r.is_some()) {
return SessionResponse::Error;
}
let Some(process) = self.create_cap.and_then(libkos::syscall::create_process) else {
let Some(process) = self
.create_cap
.and_then(|c| libkos::syscall::create_process(c, APP_LIMITS))
else {
return SessionResponse::Error;
};
if let Some(r) = self.running_mut(app) {

View File

@@ -0,0 +1,10 @@
//! How much a process may hold, as a creator asks for it in `CreateProcess`
//! (`a1..a4`). The kernel carves it from the creator's own budget.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Limits {
pub pages: usize,
pub endpoints: usize,
pub notifications: usize,
pub regions: usize,
}

View File

@@ -4,6 +4,7 @@ pub mod address;
pub mod badge;
pub mod bootinfo;
pub mod bounded;
pub mod budget;
pub mod display;
pub mod generation;
pub mod id;