refactor(proofs): split into per-topic modules with shared helpers across types, domain, and kernel

This commit is contained in:
2026-10-18 01:08:05 +02:00
parent e5fd6372b3
commit 71518a93a4
16 changed files with 554 additions and 607 deletions

View File

@@ -1,340 +0,0 @@
#[cfg(kani)]
mod kani_proofs {
use domain::capability::{Capability, CapabilityTable};
use domain::message::{Message, MAX_MESSAGE_WORDS};
use domain::resource::Resource;
use types::bounded::CapSlot;
use types::generation::Generation;
use types::id::{EndpointId, ProcessId};
use types::permission::{Permission, PermissionSet};
#[kani::proof]
fn kernel_delegate_path_never_escalates() {
let source_bits: u8 = kani::any();
let restrict_bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let source_perms = PermissionSet::from_bits(source_bits);
kani::assume(source_perms.allows(Permission::Delegate));
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
// Step 1: source process has a capability (simulates check_cap)
let mut source_table = CapabilityTable::new();
let source_cap = Capability::new(resource.clone(), source_perms, Generation::initial());
let slot = source_table.insert(source_cap);
kani::assume(slot.is_ok());
let slot = slot.unwrap();
// Step 2: kernel reads source cap and restricts (simulates handle_delegate)
let cap = source_table.get(slot).unwrap();
let original_perms = *cap.permissions();
let restricted = original_perms.restrict_to(&PermissionSet::from_bits(restrict_bits));
// Step 3: kernel inserts into target (simulates grant_cap)
let mut target_table = CapabilityTable::new();
let delegated = Capability::new(cap.resource().clone(), restricted, Generation::initial());
let target_slot = target_table.insert(delegated);
kani::assume(target_slot.is_ok());
// PROOF: the capability in the target table has permissions
// that are a strict subset of what the source held
let result = target_table.get(target_slot.unwrap()).unwrap();
assert!(result.permissions().is_subset_of(&original_perms));
// PROOF: no individual permission was gained
if !original_perms.allows(Permission::Read) {
assert!(!result.permissions().allows(Permission::Read));
}
if !original_perms.allows(Permission::Write) {
assert!(!result.permissions().allows(Permission::Write));
}
if !original_perms.allows(Permission::Execute) {
assert!(!result.permissions().allows(Permission::Execute));
}
if !original_perms.allows(Permission::Delegate) {
assert!(!result.permissions().allows(Permission::Delegate));
}
if !original_perms.allows(Permission::Revoke) {
assert!(!result.permissions().allows(Permission::Revoke));
}
}
#[kani::proof]
fn kernel_chained_delegate_never_escalates() {
let original_bits: u8 = kani::any();
let restrict1_bits: u8 = kani::any();
let restrict2_bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let original_perms = PermissionSet::from_bits(original_bits);
kani::assume(original_perms.allows(Permission::Delegate));
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
// Process A has the original cap
let mut table_a = CapabilityTable::new();
let cap_a = Capability::new(resource.clone(), original_perms, Generation::initial());
let slot_a = table_a.insert(cap_a);
kani::assume(slot_a.is_ok());
let slot_a = slot_a.unwrap();
// A delegates to B
let cap = table_a.get(slot_a).unwrap();
let perms_for_b = cap.permissions().restrict_to(&PermissionSet::from_bits(restrict1_bits));
let mut table_b = CapabilityTable::new();
let cap_b = Capability::new(resource.clone(), perms_for_b, Generation::initial());
let slot_b = table_b.insert(cap_b);
kani::assume(slot_b.is_ok());
let slot_b = slot_b.unwrap();
// B delegates to C (if B has Delegate permission)
let cap_in_b = table_b.get(slot_b).unwrap();
if cap_in_b.permissions().allows(Permission::Delegate) {
let perms_for_c = cap_in_b.permissions().restrict_to(&PermissionSet::from_bits(restrict2_bits));
let mut table_c = CapabilityTable::new();
let cap_c = Capability::new(resource, perms_for_c, Generation::initial());
let slot_c = table_c.insert(cap_c);
kani::assume(slot_c.is_ok());
let result = table_c.get(slot_c.unwrap()).unwrap();
// C's permissions are subset of B's
assert!(result.permissions().is_subset_of(cap_in_b.permissions()));
// C's permissions are subset of A's original
assert!(result.permissions().is_subset_of(&original_perms));
}
}
#[kani::proof]
fn kernel_delegate_rejects_without_delegate_permission() {
let source_bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let source_perms = PermissionSet::from_bits(source_bits);
kani::assume(!source_perms.allows(Permission::Delegate));
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
let cap = Capability::new(resource, source_perms, Generation::initial());
let mut table = CapabilityTable::new();
let slot = table.insert(cap);
kani::assume(slot.is_ok());
let slot = slot.unwrap();
// This is what check_cap does: look up the cap and check permission
let retrieved = table.get(slot).unwrap();
let has_delegate = retrieved.permissions().allows(Permission::Delegate);
// The guard MUST reject: a cap inserted without Delegate
// permission is never retrievable with Delegate permission
assert!(!has_delegate);
}
#[kani::proof]
fn kernel_check_cap_faithfully_reflects_stored_permissions() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
let cap = Capability::new(resource, perms, Generation::initial());
let mut table = CapabilityTable::new();
let slot = table.insert(cap);
kani::assume(slot.is_ok());
let slot = slot.unwrap();
let retrieved = table.get(slot).unwrap();
// check_cap checks allows(needed). Prove the table doesn't
// corrupt permissions: every permission check on the retrieved
// cap matches what was stored.
assert!(retrieved.permissions().allows(Permission::Read) == perms.allows(Permission::Read));
assert!(retrieved.permissions().allows(Permission::Write) == perms.allows(Permission::Write));
assert!(retrieved.permissions().allows(Permission::Execute) == perms.allows(Permission::Execute));
assert!(retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate));
assert!(retrieved.permissions().allows(Permission::Revoke) == perms.allows(Permission::Revoke));
}
#[kani::proof]
fn grant_cap_preserves_resource_identity() {
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let bits: u8 = kani::any();
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
let perms = PermissionSet::from_bits(bits);
let cap = Capability::new(resource.clone(), perms, Generation::initial());
let mut table = CapabilityTable::new();
let slot = table.insert(cap);
kani::assume(slot.is_ok());
let retrieved = table.get(slot.unwrap()).unwrap();
match retrieved.resource() {
Resource::Endpoint(id) => assert!(id.raw() == ep_id),
_ => panic!("resource type changed"),
}
}
// --- Complete mediation: every resource-accessing syscall checks caps ---
#[kani::proof]
fn handle_send_rejects_without_execute_permission() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
kani::assume(!perms.allows(Permission::Execute));
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
let cap = Capability::new(resource, perms, Generation::initial());
let mut table = CapabilityTable::new();
let slot = table.insert(cap);
kani::assume(slot.is_ok());
let slot = slot.unwrap();
let retrieved = table.get(slot).unwrap();
assert!(!retrieved.permissions().allows(Permission::Execute));
}
#[kani::proof]
fn handle_receive_rejects_without_read_permission() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
kani::assume(!perms.allows(Permission::Read));
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
let cap = Capability::new(resource, perms, Generation::initial());
let mut table = CapabilityTable::new();
let slot = table.insert(cap);
kani::assume(slot.is_ok());
let slot = slot.unwrap();
let retrieved = table.get(slot).unwrap();
assert!(!retrieved.permissions().allows(Permission::Read));
}
#[kani::proof]
fn missing_cap_always_rejected() {
let slot_val: u8 = kani::any();
kani::assume(slot_val <= 63);
let slot = CapSlot::new(slot_val).unwrap();
let table = CapabilityTable::new();
assert!(table.get(slot).is_err());
}
// --- Message delivery integrity ---
#[kani::proof]
fn message_survives_pending_storage_roundtrip() {
let w0: usize = kani::any();
let w1: usize = kani::any();
let w2: usize = kani::any();
let w3: usize = kani::any();
let original = Message::from_words([w0, w1, w2, w3]);
let mut pending: [Option<Message>; 4] = [const { None }; 4];
let idx: usize = kani::any();
kani::assume(idx < 4);
pending[idx] = Some(original);
let retrieved = pending[idx].take();
assert!(retrieved.is_some());
let msg = retrieved.unwrap();
assert!(msg.word(0) == w0);
assert!(msg.word(1) == w1);
assert!(msg.word(2) == w2);
assert!(msg.word(3) == w3);
}
#[kani::proof]
fn message_storage_does_not_cross_slots() {
let w_a: usize = kani::any();
let w_b: usize = kani::any();
kani::assume(w_a != w_b);
let msg_a = Message::from_words([w_a, 0, 0, 0]);
let msg_b = Message::from_words([w_b, 0, 0, 0]);
let mut pending: [Option<Message>; 4] = [const { None }; 4];
pending[0] = Some(msg_a);
pending[1] = Some(msg_b);
let retrieved_a = pending[0].take().unwrap();
let retrieved_b = pending[1].take().unwrap();
assert!(retrieved_a.word(0) == w_a);
assert!(retrieved_b.word(0) == w_b);
}
// --- Revocation completeness ---
#[kani::proof]
fn revoke_invalidates_cap_slot() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
let cap = Capability::new(
Resource::Endpoint(EndpointId::from_raw(ep_id)),
perms,
Generation::initial(),
);
let mut table = CapabilityTable::new();
let slot = table.insert(cap);
kani::assume(slot.is_ok());
let slot = slot.unwrap();
assert!(table.get(slot).is_ok());
table.remove(slot).unwrap();
assert!(table.get(slot).is_err());
}
#[kani::proof]
fn revoke_does_not_affect_other_slots() {
let bits_a: u8 = kani::any();
let bits_b: u8 = kani::any();
let cap_a = Capability::new(
Resource::ProcessCreate,
PermissionSet::from_bits(bits_a),
Generation::initial(),
);
let cap_b = Capability::new(
Resource::Endpoint(EndpointId::from_raw(0)),
PermissionSet::from_bits(bits_b),
Generation::initial(),
);
let mut table = CapabilityTable::new();
let slot_a = table.insert(cap_a);
let slot_b = table.insert(cap_b);
kani::assume(slot_a.is_ok() && slot_b.is_ok());
let slot_a = slot_a.unwrap();
let slot_b = slot_b.unwrap();
table.remove(slot_a).unwrap();
assert!(table.get(slot_a).is_err());
assert!(table.get(slot_b).is_ok());
assert!(*table.get(slot_b).unwrap().permissions() == PermissionSet::from_bits(bits_b));
}
}

View File

@@ -0,0 +1,95 @@
use domain::capability::{Capability, CapabilityTable};
use domain::resource::Resource;
use super::{arbitrary_endpoint_cap, insert_cap};
use types::generation::Generation;
use types::id::EndpointId;
use types::permission::{Permission, PermissionSet};
#[kani::proof]
fn kernel_delegate_path_never_escalates() {
let source_bits: u8 = kani::any();
let restrict_bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let source_perms = PermissionSet::from_bits(source_bits);
kani::assume(source_perms.allows(Permission::Delegate));
let mut source_table = CapabilityTable::new();
let slot = insert_cap(&mut source_table, arbitrary_endpoint_cap(source_bits, ep_id));
let cap = source_table.get(slot).unwrap();
let original_perms = *cap.permissions();
let restricted = original_perms.restrict_to(&PermissionSet::from_bits(restrict_bits));
let delegated = Capability::new(
cap.resource().clone(),
restricted,
Generation::initial(),
);
let mut target_table = CapabilityTable::new();
let target_slot = insert_cap(&mut target_table, delegated);
let result = target_table.get(target_slot).unwrap();
assert!(result.permissions().is_subset_of(&original_perms));
if !original_perms.allows(Permission::Read) { assert!(!result.permissions().allows(Permission::Read)); }
if !original_perms.allows(Permission::Write) { assert!(!result.permissions().allows(Permission::Write)); }
if !original_perms.allows(Permission::Execute) { assert!(!result.permissions().allows(Permission::Execute)); }
if !original_perms.allows(Permission::Delegate) { assert!(!result.permissions().allows(Permission::Delegate)); }
if !original_perms.allows(Permission::Revoke) { assert!(!result.permissions().allows(Permission::Revoke)); }
}
#[kani::proof]
fn kernel_chained_delegate_never_escalates() {
let original_bits: u8 = kani::any();
let restrict1_bits: u8 = kani::any();
let restrict2_bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let original_perms = PermissionSet::from_bits(original_bits);
kani::assume(original_perms.allows(Permission::Delegate));
let resource = Resource::Endpoint(EndpointId::from_raw(ep_id));
let mut table_a = CapabilityTable::new();
let slot_a = insert_cap(&mut table_a, arbitrary_endpoint_cap(original_bits, ep_id));
let cap = table_a.get(slot_a).unwrap();
let perms_for_b = cap.permissions().restrict_to(&PermissionSet::from_bits(restrict1_bits));
let mut table_b = CapabilityTable::new();
let cap_b = Capability::new(resource.clone(), perms_for_b, Generation::initial());
let slot_b = insert_cap(&mut table_b, cap_b);
let cap_in_b = table_b.get(slot_b).unwrap();
if cap_in_b.permissions().allows(Permission::Delegate) {
let perms_for_c = cap_in_b.permissions().restrict_to(&PermissionSet::from_bits(restrict2_bits));
let mut table_c = CapabilityTable::new();
let cap_c = Capability::new(resource, perms_for_c, Generation::initial());
let slot_c = insert_cap(&mut table_c, cap_c);
let result = table_c.get(slot_c).unwrap();
assert!(result.permissions().is_subset_of(cap_in_b.permissions()));
assert!(result.permissions().is_subset_of(&original_perms));
}
}
#[kani::proof]
fn kernel_delegate_rejects_without_delegate_permission() {
let source_bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let source_perms = PermissionSet::from_bits(source_bits);
kani::assume(!source_perms.allows(Permission::Delegate));
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, arbitrary_endpoint_cap(source_bits, ep_id));
let retrieved = table.get(slot).unwrap();
assert!(!retrieved.permissions().allows(Permission::Delegate));
}

View File

@@ -0,0 +1,66 @@
use domain::capability::CapabilityTable;
use super::{arbitrary_endpoint_cap, insert_cap};
use types::bounded::CapSlot;
use types::permission::{Permission, PermissionSet};
#[kani::proof]
fn handle_send_rejects_without_execute_permission() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
kani::assume(!perms.allows(Permission::Execute));
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, arbitrary_endpoint_cap(bits, ep_id));
let retrieved = table.get(slot).unwrap();
assert!(!retrieved.permissions().allows(Permission::Execute));
}
#[kani::proof]
fn handle_receive_rejects_without_read_permission() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
kani::assume(!perms.allows(Permission::Read));
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, arbitrary_endpoint_cap(bits, ep_id));
let retrieved = table.get(slot).unwrap();
assert!(!retrieved.permissions().allows(Permission::Read));
}
#[kani::proof]
fn missing_cap_always_rejected() {
let slot_val: u8 = kani::any();
kani::assume(slot_val <= 63);
let slot = CapSlot::new(slot_val).unwrap();
let table = CapabilityTable::new();
assert!(table.get(slot).is_err());
}
#[kani::proof]
fn check_cap_faithfully_reflects_stored_permissions() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let perms = PermissionSet::from_bits(bits);
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, arbitrary_endpoint_cap(bits, ep_id));
let retrieved = table.get(slot).unwrap();
assert!(retrieved.permissions().allows(Permission::Read) == perms.allows(Permission::Read));
assert!(retrieved.permissions().allows(Permission::Write) == perms.allows(Permission::Write));
assert!(retrieved.permissions().allows(Permission::Execute) == perms.allows(Permission::Execute));
assert!(retrieved.permissions().allows(Permission::Delegate) == perms.allows(Permission::Delegate));
assert!(retrieved.permissions().allows(Permission::Revoke) == perms.allows(Permission::Revoke));
}

View File

@@ -0,0 +1,46 @@
use domain::message::Message;
#[kani::proof]
fn message_survives_pending_storage_roundtrip() {
let w0: usize = kani::any();
let w1: usize = kani::any();
let w2: usize = kani::any();
let w3: usize = kani::any();
let original = Message::from_words([w0, w1, w2, w3]);
let mut pending: [Option<Message>; 4] = [const { None }; 4];
let idx: usize = kani::any();
kani::assume(idx < 4);
pending[idx] = Some(original);
let retrieved = pending[idx].take();
assert!(retrieved.is_some());
let msg = retrieved.unwrap();
assert!(msg.word(0) == w0);
assert!(msg.word(1) == w1);
assert!(msg.word(2) == w2);
assert!(msg.word(3) == w3);
}
#[kani::proof]
fn message_storage_does_not_cross_slots() {
let w_a: usize = kani::any();
let w_b: usize = kani::any();
kani::assume(w_a != w_b);
let msg_a = Message::from_words([w_a, 0, 0, 0]);
let msg_b = Message::from_words([w_b, 0, 0, 0]);
let mut pending: [Option<Message>; 4] = [const { None }; 4];
pending[0] = Some(msg_a);
pending[1] = Some(msg_b);
let retrieved_a = pending[0].take().unwrap();
let retrieved_b = pending[1].take().unwrap();
assert!(retrieved_a.word(0) == w_a);
assert!(retrieved_b.word(0) == w_b);
}

View File

@@ -0,0 +1,25 @@
mod delegation;
mod mediation;
mod message;
mod revocation;
use domain::capability::{Capability, CapabilityTable};
use domain::resource::Resource;
use types::bounded::CapSlot;
use types::generation::Generation;
use types::id::EndpointId;
use types::permission::PermissionSet;
fn arbitrary_endpoint_cap(bits: u8, ep_id: u16) -> Capability {
Capability::new(
Resource::Endpoint(EndpointId::from_raw(ep_id)),
PermissionSet::from_bits(bits),
Generation::initial(),
)
}
fn insert_cap(table: &mut CapabilityTable, cap: Capability) -> CapSlot {
let slot = table.insert(cap);
kani::assume(slot.is_ok());
slot.unwrap()
}

View File

@@ -0,0 +1,63 @@
use domain::capability::{Capability, CapabilityTable};
use domain::resource::Resource;
use super::{arbitrary_endpoint_cap, insert_cap};
use types::generation::Generation;
use types::id::EndpointId;
use types::permission::PermissionSet;
#[kani::proof]
fn revoke_invalidates_cap_slot() {
let bits: u8 = kani::any();
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, arbitrary_endpoint_cap(bits, ep_id));
assert!(table.get(slot).is_ok());
table.remove(slot).unwrap();
assert!(table.get(slot).is_err());
}
#[kani::proof]
fn revoke_does_not_affect_other_slots() {
let bits_a: u8 = kani::any();
let bits_b: u8 = kani::any();
let cap_a = Capability::new(
Resource::ProcessCreate,
PermissionSet::from_bits(bits_a),
Generation::initial(),
);
let cap_b = Capability::new(
Resource::Endpoint(EndpointId::from_raw(0)),
PermissionSet::from_bits(bits_b),
Generation::initial(),
);
let mut table = CapabilityTable::new();
let slot_a = insert_cap(&mut table, cap_a);
let slot_b = insert_cap(&mut table, cap_b);
table.remove(slot_a).unwrap();
assert!(table.get(slot_a).is_err());
assert!(table.get(slot_b).is_ok());
assert!(*table.get(slot_b).unwrap().permissions() == PermissionSet::from_bits(bits_b));
}
#[kani::proof]
fn grant_cap_preserves_resource_identity() {
let ep_id: u16 = kani::any();
kani::assume(ep_id < 256);
let bits: u8 = kani::any();
let mut table = CapabilityTable::new();
let slot = insert_cap(&mut table, arbitrary_endpoint_cap(bits, ep_id));
let retrieved = table.get(slot).unwrap();
match retrieved.resource() {
Resource::Endpoint(id) => assert!(id.raw() == ep_id),
_ => panic!("resource type changed"),
}
}