capability_attr/lib.rs
1//! `capability-attr` — Layer 2 (side-effect / capability safety) typed
2//! capability declarations for Rust.
3//!
4//! # The problem this solves
5//!
6//! Rust's safety model is binary: a function is `safe` or `unsafe`. Real
7//! systems code spans a spectrum `unsafe` collapses into one signal —
8//! reading a bounded buffer and writing an arbitrary raw pointer are both
9//! just "unsafe" to the compiler, even though their risk profiles are
10//! wildly different. `#[capability(...)]` gives that spectrum a
11//! machine-readable, compiler-enforced structure: a function declares the
12//! allocation/I/O/raw-pointer scope it needs, and this crate verifies the
13//! function body doesn't exceed it.
14//!
15//! ```compile_fail
16//! # use capability_attr::capability;
17//! // COMPILE ERROR: body allocates on the heap, but only `alloc(none)` was
18//! // declared.
19//! #[capability(alloc(none), io(none), ptr(none))]
20//! fn quiet_fn() {
21//! let _buf: Vec<u8> = Vec::new();
22//! }
23//! # fn main() {}
24//! ```
25//!
26//! ```
27//! # use capability_attr::capability;
28//! // Compiles clean — every operation in the body is within what was
29//! // declared.
30//! #[capability(alloc(heap), io(display), ptr(none))]
31//! fn log_message(msg: &str) {
32//! let buf: Vec<u8> = msg.bytes().collect();
33//! println!("{}", buf.len());
34//! }
35//! # fn main() { log_message("hi"); }
36//! ```
37//!
38//! This is orthogonal to `unsafe`, not a replacement for it: `unsafe`
39//! remains the programmer's memory-safety promise (Layer 1, unchanged);
40//! `#[capability(...)]` is the compiler's side-effect-scope promise
41//! (Layer 2, this crate). See the companion [`sensitive-ifc`] crate for
42//! Layer 3 (semantic/policy safety — does this function leak a credential,
43//! not just "does it do I/O").
44//!
45//! # Phased scope (this crate implements Phase 1 only, function-level only)
46//!
47//! This is a direct, workspace-local implementation of Phase 1 from
48//! `docs/aisecurity/capability-rfc-updated.md`, requiring no `rustc`
49//! changes, no nightly compiler — a `syn`/`quote`-based proc-macro attribute
50//! that (1) parses declared capabilities from the attribute's arguments,
51//! (2) walks the annotated function's body with a [`syn::visit::Visit`]
52//! walker ([`capability_core::inspector::BodyInspector`]) to detect actual
53//! capability usage, and (3) emits a real `compile_error!(...)` when
54//! detected capabilities exceed declared ones
55//! ([`capability_core::check_subset`] / [`error::emit_violation`]).
56//!
57//! **Scoped to function items only, this pass.** The RFC also describes
58//! module/trait/impl/crate-level declarations with hierarchical narrowing
59//! (a module's declaration bounds every function inside it, a trait's
60//! declaration bounds every `impl`). That requires tracking capability
61//! state *across* multiple macro-expansion sites, which a single
62//! `#[proc_macro_attribute]` invocation cannot see by itself — it is real,
63//! valuable, and explicitly deferred (see this workspace's
64//! `spec/SPEC-00045-*.md`), not attempted here.
65//!
66//! - **Phase 1 (this crate, stable Rust today):** function-level
67//! declaration + body-inspection + subset-check, described above.
68//! - **Phase 2 (deferred, not built this pass):** custom Clippy lints
69//! (`declare_lint!`) for cross-function capability-flow checking. The
70//! RFC frames this as "Phase 2" but it needs Clippy's internal lint
71//! infrastructure — effectively nightly-adjacent in practice, not as
72//! "stable" as this phase despite the RFC's own phase numbering.
73//! - **Phase 3 (deferred, not built this pass):** MIR-level analysis via
74//! `rustc_private` — nightly-only, out of scope for this crate entirely.
75//!
76//! # Vocabulary — see `capability-core`'s `vocabulary` module docs
77//!
78//! The capability vocabulary implemented here (`alloc`/`io`/`ptr`) is
79//! deliberately reduced from the RFC's five categories and reshaped for
80//! this project's real target (`git.git`, a userspace CLI tool, not
81//! embedded firmware) — see [`capability_core::vocabulary`]'s
82//! module-level doc comment for the full reasoning, including why
83//! `register(...)`/`interrupt(...)` are dropped entirely rather than
84//! stubbed, and why `io(process)` exists (with no RFC equivalent) and
85//! outranks `io(network)` in this crate's risk ordering. That vocabulary
86//! lives in `capability-core` now, shared with `taint-generate` — see
87//! `docs/adr/ADR-0005-generate-and-refactor.md`.
88//!
89//! # Honest scope statement
90//!
91//! **What this crate catches:** a function declaring `alloc(none)` that
92//! calls `Vec::new`/`Box::new`/etc.; a function declaring `io(none)` that
93//! calls `println!`/touches `std::fs`/`std::net`/spawns a `Command`; a
94//! function declaring `ptr(none)` that dereferences a raw pointer for a
95//! read or write. All are real `compile_error!(...)`s produced by this
96//! crate today — see `tests/ui/fail/*.rs` and their checked-in `.stderr`
97//! snapshots for real compiler output, not a description.
98//!
99//! **What this crate does NOT catch:** capability usage inside a function
100//! called *by* the annotated function (cross-function flow — Phase 2/3);
101//! usage hidden behind a macro that itself expands to an allocating/IO
102//! call (AST-level detection only sees the macro invocation, not its
103//! expansion, unless the macro name itself is recognized — see
104//! [`capability_core::inspector`]); a raw pointer write's actual address
105//! range (Phase 1 has no PAC-style address verification, so every
106//! detected write is conservatively classified `ptr(write, any)`, never
107//! `ptr(write, bounded)` — see [`capability_core::PtrBound`]).
108
109#![warn(missing_docs)]
110// `cargo_common_metadata` inspects every workspace member's `Cargo.toml`
111// reachable from this crate's own dependency graph (confirmed live under
112// packages/offline-ops, SPEC-00034 T6: fires on all sibling crates, not
113// just this one's), so it's carved out here rather than silently left
114// un-denied or "fixed" by editing unrelated crates' manifests out of scope.
115#![allow(
116 clippy::cargo_common_metadata,
117 reason = "workspace-wide dependency-graph check, not something a single-crate pass can fix or meaningfully scope — see SPEC-00034 T6 / SPEC-00052 T0b"
118)]
119
120use proc_macro::TokenStream;
121use syn::ItemFn;
122
123mod error;
124mod parser;
125
126/// Declare a function's allocation/I/O/raw-pointer capability scope, and
127/// verify at compile time that the function body does not exceed it.
128///
129/// # Syntax
130///
131/// ```text
132/// #[capability(alloc(<none|heap|any>), io(<none|display|filesystem|network|process|any>), ptr(<none|read|any|write, bounded|write, any>))]
133/// ```
134///
135/// Any category may be omitted; an omitted category defaults to its most
136/// restrictive level (`none`) — see [`parser::CapabilitySet::alloc_or_none`]
137/// and friends. See the crate-level docs for the full worked
138/// compile-passing and compile-failing examples.
139#[proc_macro_attribute]
140pub fn capability(args: TokenStream, item: TokenStream) -> TokenStream {
141 let declared = match parser::parse_capability_args(args.into()) {
142 Ok(caps) => caps,
143 Err(e) => return e.into_compile_error().into(),
144 };
145
146 let func = match syn::parse::<ItemFn>(item) {
147 Ok(f) => f,
148 Err(e) => {
149 return syn::Error::new(
150 e.span(),
151 "#[capability] can only be applied to a function item in this crate's current \
152 (function-level-only, Phase 1) scope — see this crate's module docs for the \
153 deferred module/trait/impl-level support",
154 )
155 .into_compile_error()
156 .into();
157 }
158 };
159
160 let detected = capability_core::inspector::inspect_body(&func.block);
161
162 if let Some(violation) = capability_core::check_subset(&detected, &declared) {
163 return error::emit_violation(&func.sig.ident, &violation).into();
164 }
165
166 quote::quote! { #func }.into()
167}