capsec enforces I/O permissions at compile time via a zero-cost capability type system. This document describes what it protects against, what it doesn't, and where the boundaries are.
The only way to construct a Cap<P> in safe Rust is through CapRoot::grant(). The constructor Cap::new() is pub(crate) — external crates cannot call it.
// This is the entire security gate:
pub(crate) fn new() -> Self { ... }Every capability in a capsec program traces back to a CapRoot::grant() call. CapRoot is a singleton — root() panics if called twice, ensuring a single point of authority.
The Has<P> trait is open for external implementation. This is by design — the #[capsec::context] macro generates impl Has<P> on user-defined structs to enable capability threading through call stacks.
This is safe because Has<P> requires returning a Cap<P>:
pub trait Has<P: Permission> {
fn cap_ref(&self) -> Cap<P>;
}Any implementation must produce a Cap<P>. Since Cap::new() is pub(crate), the only way to satisfy this in safe Rust is to hold a real Cap<P> obtained from CapRoot::grant().
All capsec-std and capsec-tokio wrapper functions extract a typed proof before executing I/O:
pub fn read(path: impl AsRef<Path>, cap: &impl Has<FsRead>) -> Result<Vec<u8>, CapSecError> {
let _proof: Cap<FsRead> = cap.cap_ref(); // proof extracted here
Ok(std::fs::read(path)?) // I/O happens here
}The concrete type annotation Cap<FsRead> is not cosmetic. It forces the compiler to resolve the return type of cap_ref() and prevents dead-code elimination. If a Has<P> implementation diverges (panics, loops, calls process::exit), the divergence fires at the _proof line — the I/O on the next line never executes.
The adversarial test suite (capsec-tests/tests/type_system.rs, section L) proves this with a PanicForge impl that panics in cap_ref() — the test confirms the panic fires before std::fs::read runs.
transmute, MaybeUninit, and ptr::read can forge a Cap<P> because Cap is a zero-sized type. All three attacks are documented and tested in capsec-tests/tests/type_system.rs (section C). This is expected — capsec's threat model is safe Rust only.
A function can always call std::fs::read() or tokio::fs::read() directly without a capability token. The Rust compiler won't stop it — capsec wrappers are opt-in replacements, not mandatory.
This is where cargo capsec audit comes in. The audit tool statically scans source code for ambient authority calls and reports them. Functions annotated with #[capsec::deny(all)] that contain I/O calls are promoted to critical risk.
extern blocks and inline asm! interact with the OS directly. The audit tool flags extern blocks but cannot analyze what foreign code does.
Code generated by procedural macros is invisible to the static scanner. This is inherent to syntax-level tooling.
cargo capsec audit is an AST-level heuristic scanner, not a data flow analyzer. It:
- Detects qualified calls (
std::fs::read,TcpStream::connect,Command::new) - Expands import aliases (
use std::fs::read as load; load(...)) - Handles glob imports (
use std::fs::*; read(...)) - Uses contextual method matching (
.output()only flags whenCommand::newis in the same function) - Flags
externblocks as FFI findings
Known blind spots (documented in capsec-tests/tests/audit_evasion.rs):
- Function pointers and closures that hide the call target
include!()directives that pull in code from other files- Module re-exports across crate boundaries
- Dependency re-exports (a crate wrapping
std::fsunder a different name) libc/nixcrate calls (not in the pattern registry)cfg-conditional code that may or may not compile
If you discover a security issue in capsec, please email the maintainers directly rather than opening a public issue. Contact: open an issue with the security label.
For issues that are not security-sensitive (e.g., audit tool false negatives, documentation gaps), please open a regular GitHub issue.