Security
This document maps the zero-trust enforcement points in the CambiOS microkernel — what is enforced, where in the code, and why. It is the security-specific reference; project-wide implementation status (what’s built vs. designed vs. planned, test counts, phase markers) lives in STATUS.md.
Required reading by topic:
- Foundational security decision (why capabilities, why zero-trust): ADR-000
- Enforcement pipeline decision (why three layers, why this ordering): ADR-002
- Content-addressed storage and identity: ADR-003
- Cryptographic integrity (signed binaries, signed objects): ADR-004
- IPC primitives — control path and bulk-data channels: ADR-005
- Policy service — externalized
on_syscalldecisions: ADR-006 - Capability revocation and audit telemetry: ADR-007
Enforcement Status Summary
This table maps every enforcement point to its code location and what it does. For the project-wide “is X built yet” view, see STATUS.md § Subsystem status.
| Enforcement Point | Status | Blocks on Failure | Where |
|---|---|---|---|
| ELF entry point validation | Enforced | Binary not loaded | loader/mod.rs |
| ELF kernel space rejection | Enforced | Binary not loaded | loader/mod.rs |
| ELF W^X enforcement | Enforced | Binary not loaded | loader/mod.rs |
| ELF segment overlap detection | Enforced | Binary not loaded | loader/mod.rs |
| ELF memory limit | Enforced | Binary not loaded | loader/mod.rs |
| ELF Ed25519 signature verification | Enforced | Binary not loaded | loader/mod.rs (SignedBinaryVerifier) |
| Capability check (IPC send) | Enforced | PermissionDenied | ipc/mod.rs |
| Capability check (IPC recv) | Enforced | PermissionDenied | ipc/mod.rs |
| Capability delegation validation | Enforced | AccessDenied | ipc/capability.rs |
| Interceptor: syscall pre-dispatch | Scaffolding (designed in ADR-006) | Always allows; policy externalization pending | syscalls/dispatcher.rs |
| Interceptor: IPC send policy | Enforced | PermissionDenied | ipc/mod.rs, ipc/interceptor.rs |
| Interceptor: IPC recv policy | Enforced | PermissionDenied | ipc/mod.rs, ipc/interceptor.rs |
| Interceptor: delegation policy | Enforced | AccessDenied | ipc/capability.rs |
| IPC sender_principal stamping | Enforced | N/A (kernel stamps unconditionally) | ipc/mod.rs |
| BindPrincipal restricted to bootstrap | Enforced | PermissionDenied | syscalls/dispatcher.rs |
| ObjPut Ed25519 signature verification | Enforced | PermissionDenied | syscalls/dispatcher.rs (ObjPutSigned) |
| ObjDelete ownership enforcement | Enforced | PermissionDenied | syscalls/dispatcher.rs |
| FS service principal-based access | Enforced | Error response to caller | user/fs-service/src/main.rs |
| Bulk-data channel capability checks | Designed in ADR-005, not implemented | — | — |
Capability revocation (SYS_REVOKE_CAPABILITY) | Designed in ADR-007, not implemented | — | — |
| Audit telemetry channel | Designed in ADR-007, not implemented | — | — |
| Externalized policy service | Designed in ADR-006, not implemented | — | — |
| Per-process syscall allowlists | Designed in ADR-006, not implemented | — | — |
| Runtime behavioral AI (advisory only) | Designed in ADR-007, not implemented | — | — |
| Cryptographic capabilities (cross-node) | Planned (post-v1) | — | — |
Enforced = real check that returns an error and blocks the operation on failure. Scaffolding = hook is wired into the call path but the default policy is permissive (real policy lives in the linked ADR). Designed = ADR drafted, code not yet written.
Code line numbers were intentionally removed from this table — they drift on every change. Use grep or your editor’s symbol search to find the specific call site within the listed file.
The Three-Layer Enforcement Pipeline
Every IPC operation passes through three independent enforcement layers. Bypassing one does not bypass the others.
Process makes SYS_WRITE or SYS_READ syscall
|
v
+-----------------------------------------------+
| Layer 1: Interceptor pre-dispatch |
| IpcInterceptor::on_syscall() |
| - Per-process syscall allowlist |
| - Status: SCAFFOLDING (always allows) |
| - File: syscalls/dispatcher.rs:182 |
+-----------------------------------------------+
|
v
+-----------------------------------------------+
| Layer 2: Capability verification |
| CapabilityManager::verify_access() |
| - Process must hold correct rights for |
| the target endpoint (SEND or RECV) |
| - Status: ENFORCED |
| - File: ipc/capability.rs:114-128 |
+-----------------------------------------------+
|
v
+-----------------------------------------------+
| Layer 3: Interceptor post-capability |
| IpcInterceptor::on_send() / on_recv() |
| - Endpoint bounds check |
| - Payload size limit (256 bytes) |
| - No self-send |
| - Status: ENFORCED |
| - File: ipc/interceptor.rs:145-212 |
+-----------------------------------------------+
|
v
IPC operation proceeds
Why Three Layers Instead of One
A single capability check would be sufficient if capabilities were the only thing that could go wrong. They aren’t:
Layer 1 catches a compromised process that tries to invoke syscalls outside its profile. A serial driver that only needs
WriteandWaitIrqshould never callAllocate. Even if it holds capabilities, it shouldn’t be making that syscall at all.Layer 2 is the core access control. Capabilities are unforgeable kernel-managed tokens. If you don’t hold the right token, the operation fails. This is the load-bearing wall.
Layer 3 catches structural violations that capabilities don’t address: oversized payloads (buffer overflow prevention), out-of-bounds endpoints (kernel memory safety), self-send (deadlock prevention). These are invariants about the message, not the authority.
Each layer is independently useful. Together, they make exploitation require three independent bypasses.
ELF Verification Gate
The verifier runs before the loader allocates any resources. A binary that fails verification causes zero side effects — no frames allocated, no pages mapped, no process created.
Raw ELF binary bytes
|
v
Parse ELF header + collect LOAD segments
|
v
+-----------------------------------------------+
| BinaryVerifier::verify() |
| 1. Entry point falls within a LOAD segment |
| 2. All segments in user space (< canonical) |
| 3. No segment is both writable AND executable |
| 4. No overlapping segment virtual addresses |
| 5. Total memory footprint <= 256 MB |
+-----------------------------------------------+
| |
| Allow | Deny(reason)
v v
Allocate frames, Return error immediately.
create page table, No resources consumed.
map segments,
create process.
What the Verifier Prevents
| Attack | Check | Result |
|---|---|---|
| Jump to kernel code | Entry point must be in a LOAD segment | EntryPointOutOfRange |
| Map pages into kernel space | All segments < 0x0000_8000_0000_0000 | SegmentInKernelSpace |
| Self-modifying shellcode | No page is W+X simultaneously | WritableAndExecutable |
| Aliased memory confused deputy | Segments must not overlap | OverlappingSegments |
| OOM denial of service | Total memory <= 256 MB | ExcessiveMemory |
Can a Binary Bypass the Verifier?
No. load_elf_process() takes verifier: &dyn BinaryVerifier as a required parameter. The verify call is unconditional — there is no code path that skips it. The only way to load a binary without verification is to write a new loader that doesn’t call the verifier, which requires modifying kernel code.
Capability System
What a Capability Is
A capability is a kernel-managed (endpoint, rights) pair. User-space cannot see, touch, or fabricate capabilities. They exist only inside the kernel’s CapabilityManager.
Capability {
endpoint: EndpointId, // Which IPC endpoint
rights: CapabilityRights, // What operations are allowed
}
CapabilityRights {
send: bool, // Can send messages to this endpoint
receive: bool, // Can receive messages from this endpoint
delegate: bool, // Can grant this capability to another process
}
How Capabilities Are Created
There are exactly two paths:
SYS_REGISTER_ENDPOINT — A process registers a new IPC endpoint. The kernel grants the registering process full rights (send + recv + delegate) on that endpoint. This is the only way to create a new capability from nothing.
Delegation — A process that holds a capability with
delegate = truecan grant a subset of its rights to another process. You cannot delegate more rights than you hold. You cannot delegate without the delegate right.
What Prevents Forgery
ProcessCapabilitiesis a struct incapability.rswith all internal fields private. No public constructor — onlyCapabilityManagermethods can create or mutate instances.- Capabilities are stored in a kernel-managed table indexed by process ID. User-space has no pointer to this table.
- The only mutations are through
CapabilityManagermethods, which enforce all invariants. - There is no syscall that says “give me a capability for endpoint X.” The only paths are register (you create the endpoint) or delegate (someone who has it gives it to you).
Delegation Flow
Process A holds: Capability { endpoint: 5, rights: send + delegate }
Process A delegates to Process B: rights = send (no delegate)
Checks:
1. Interceptor: on_delegate(A, B, endpoint=5, rights=send) → Allow?
2. A has delegate right on endpoint 5? → Yes
3. A holds at least the rights being delegated (send)? → Yes
4. Grant to B: Capability { endpoint: 5, rights: send }
Result: B can send to endpoint 5. B cannot delegate further.
What Delegation Cannot Do
- Escalate rights. A process with send-only cannot delegate recv. A process without delegate cannot delegate at all.
- Self-delegate. The interceptor rejects source == target.
- Exceed 32 capabilities per process. The per-process table has a hard limit.
Identity Enforcement (Phase 0)
Cryptographic identity is woven into the kernel’s IPC and storage layers. There are no passwords. Every process either has a bound Principal (an Ed25519 public key) or has no identity at all.
Principal Binding
A Principal is a 32-byte Ed25519 public key bound to a process by the kernel. Binding is restricted:
- Only a process whose own Principal matches the bootstrap Principal can call
SYS_BIND_PRINCIPAL(syscall 11). All other callers getPermissionDenied. - A process can be bound at most once. Double-bind attempts are rejected.
- Kernel processes 0–2 are bound to the bootstrap Principal at boot.
The bootstrap Principal’s public key is compiled into the kernel from bootstrap_pubkey.bin, extracted from the signing YubiKey. The private key lives exclusively on the hardware YubiKey — it never enters kernel memory. Boot modules are signed at build time by the YubiKey via the sign-elf tool’s OpenPGP smart card interface.
IPC Sender Identity (Unforgeable)
Every IPC message carries a sender_principal field. This field is set by the kernel in send_message_with_capability() at ipc/mod.rs:586, not by user code. User-space cannot write to this field — any value it sets is overwritten before the message is enqueued.
Process A calls SYS_WRITE → kernel IPC path:
1. Capability check (does A have SEND on this endpoint?)
2. Kernel reads A's bound Principal from CapabilityManager
3. Kernel stamps msg.sender_principal = A's Principal (or None if unbound)
4. Interceptor check
5. Message enqueued with unforgeable identity
The receiving process (via SYS_RECV_MSG, syscall 13) gets the 32-byte sender_principal prepended to the payload. It can verify who sent the message without trusting the sender’s self-identification.
What Identity Prevents
| Attack | Enforcement | Result |
|---|---|---|
| Process claims to be another identity | Kernel stamps real Principal | Forgery impossible |
| Unauthorized process binds identities | Only bootstrap Principal can call BindPrincipal | PermissionDenied |
| Process reads another’s identity | GetPrincipal returns caller’s own | No cross-process read |
ObjectStore Enforcement (Phase 0)
CambiOS storage is content-addressed signed objects, not files-at-paths. The ObjectStore is the kernel’s storage primitive; the FS service is a user-space gateway to it.
Ownership Model
Every CambiObject has an immutable author (who created it) and a transferable owner (who controls it). The kernel enforces ownership on destructive operations:
- ObjPut (syscall 14): The caller’s Principal becomes the author and owner. Content is hashed (Blake3) and stored. Returns the 32-byte content hash.
- ObjGet (syscall 15): Any process can read by hash. No ownership check on read (content-addressed data is inherently shareable).
- ObjDelete (syscall 16): The kernel verifies the caller’s Principal matches the object’s owner at dispatcher.rs:939. Non-owners get
PermissionDenied. - ObjList (syscall 17): Lists all hashes. No access restriction (hashes are not secrets — the content they reference may be).
FS Service (User-Space Enforcement Layer)
The FS service (user/fs-service/) runs as a user-space process on IPC endpoint 16. It adds an additional enforcement layer on top of the kernel’s ObjectStore syscalls:
- Receives messages via
SYS_RECV_MSG, which includes the kernel-stampedsender_principal - DELETE commands check that
sender_principalis non-zero (anonymous callers rejected) at fs-service/src/main.rs:218 - Delegates to kernel ObjDelete, which does the real ownership check
This is defense-in-depth: the FS service rejects obviously unauthorized requests before they reach the kernel, and the kernel enforces the authoritative ownership check.
Interceptor Details
The IpcInterceptor trait defines four hooks. The DefaultInterceptor provides baseline policy. Custom interceptors can be substituted for stricter enforcement.
Hook: on_syscall (Layer 1)
Current status: Scaffolding. Always returns Allow.
This hook fires before the syscall dispatcher routes to a handler. It receives the caller’s process ID and the syscall number. The intended use is per-process syscall allowlists:
Serial driver profile: [Write, WaitIrq, Yield, GetPid]
Filesystem driver profile: [Read, Write, Allocate, Free, RegisterEndpoint, Yield]
A process that attempts a syscall outside its profile gets PermissionDenied before any work happens. The hook is wired at dispatcher.rs:182 — only the policy logic is missing.
Hook: on_send (Layer 3)
Current status: Enforced. Three checks:
- Endpoint ID < MAX_ENDPOINTS (32) — prevents out-of-bounds access
- Payload length <= 256 bytes — prevents buffer overflow
- Sender process ID != endpoint ID — prevents self-send deadlock
Hook: on_recv (Layer 3)
Current status: Enforced. One check:
- Endpoint ID < MAX_ENDPOINTS (32) — prevents out-of-bounds access
Hook: on_delegate (Layer 3)
Current status: Enforced. Two checks:
- Endpoint ID < MAX_ENDPOINTS (32) — prevents out-of-bounds access
- Source process ID != target process ID — prevents self-delegation
Substituting a Custom Interceptor
The interceptor is a trait object (Box<dyn IpcInterceptor>). At boot, main.rs installs the DefaultInterceptor. A production deployment could install a stricter interceptor that:
- Reads per-process syscall profiles from a policy table
- Logs all capability exercises to an audit buffer
- Connects to the AI security engine for behavioral analysis
- Enforces rate limits on IPC send frequency
The swap is a single line: ipc.set_interceptor(Box::new(MyInterceptor::new())).
Bulk-Data Channels (Phase 3 — Designed)
The 256-byte fixed control IPC is not the data plane. Bulk data (video frames, file blocks, LLM token streams) needs a separate primitive: shared-memory channels, designed in ADR-005. Channels are a security-relevant addition because they introduce the first IPC path the kernel does not read byte-for-byte. The security model must hold without that read.
How the model still holds:
- Channel creation is a control-IPC operation.
SYS_CHANNEL_CREATEis mediated by the kernel and gated by capabilities. A process cannot conjure a channel into existence — it must hold the right to ask the kernel to create one between two consenting endpoints. - Capabilities, not pointers. A
ChannelCapabilityis a kernel-managed token. The mapped pages are only accessible to processes that hold a capability for that channel. There is no “anyone with the address can read it” semantic. - Notification still goes through control IPC. The producer writes data into the shared ring, then sends a small “data ready” message through the existing 256-byte IPC path. The kernel still sees the notification, still stamps
sender_principalon it, still runs the interceptor. The bulk data is the payload of the transaction, but the transaction itself is still kernel-mediated. - Channels are revocable.
SYS_CHANNEL_REVOKE(paired with the capability revocation primitive in ADR-007) tears down the shared mapping and any in-flight references. A compromised process loses its data plane the same way it loses its control plane. - The kernel can still observe. Audit telemetry (ADR-007) records channel create/attach/close/revoke events. The AI never sees the bytes flowing through a channel; it sees that two services have a channel, how often it’s used, and whether the topology is anomalous. The AI watches; it does not decide.
What channels do not change: the verification stance for control-path messages (256 bytes, fixed, kernel reads every byte), the capability model, the three-layer enforcement pipeline, or sender_principal stamping. Channels are an additional primitive, not a replacement.
Externalized Policy and Audit Telemetry (Phase 3 — Designed)
Two architectural moves currently in design phase shift how security policy is decided and observed without changing what the kernel enforces:
Policy externalization (ADR-006). The IpcInterceptor::on_syscall hook is currently scaffolding inside the kernel. The plan is to move the decision into a user-space policy-service while keeping the enforcement in the kernel. The kernel asks “is process P allowed to call syscall N with these args?” via a fast per-CPU cache; the policy service answers and the kernel acts. Cache invalidation, fail-open semantics on policy-service crash, and a bootstrap order that allows the policy service to load before it gates other services are all spelled out in the ADR. Critically: the AI is never the policy service. The policy service is deterministic Rust code; the AI is an advisor that can recommend changes to policy-service rules through its own audit channel. The AI watches, the policy service decides, the kernel enforces.
Audit telemetry (ADR-007). Every capability grant, revocation, IPC denial, channel create/attach/close, identity bind, and ObjectStore mutation produces an audit event. Events flow through a kernel→userspace channel (using the channel primitive from ADR-005) to a user-space audit consumer. Per-CPU lock-free staging buffers keep the kernel hot path off the audit path. Best-effort delivery: dropped events are counted, never block the producer. The audit channel is the only way the AI security service learns what’s happening on the system — there is no kernel hook the AI can call directly, no capability the AI holds to suspend a process, no out-of-band introspection. The AI is a user-space process that reads an event stream and emits recommendations into the policy service’s input queue.
Capability revocation (ADR-007). Currently a capability granted in error cannot be taken back. The revocation primitive adds SYS_REVOKE_CAPABILITY and atomic teardown of all references (in-flight messages, blocked receivers, channel mappings). Three authorities can revoke: the original grantor, any process holding the new revoke right on the capability, and the bootstrap Principal (which the policy service can be granted). This is the prerequisite for everything else in Phase 3 — without revocation, the AI’s recommendations have no teeth and the policy service can only deny new operations, never undo prior grants.
Gap Analysis
The gaps below are organized by whether they have a design (ADR drafted) or are still open questions. Implementation status of each item lives in STATUS.md.
Designed, Implementation Pending (Phase 3)
| Gap | Impact | Where It’s Designed |
|---|---|---|
| Capability revocation | A capability granted in error cannot be taken back; driver updates can’t reclaim the old driver’s authority | ADR-007 |
| Audit logging / telemetry channel | No forensic trail; the AI security service has no input stream | ADR-007 |
| Per-process syscall allowlists | A compromised process can invoke any syscall it has arguments for | ADR-006 — on_syscall hook routed through policy service |
| Externalized policy decisions | All policy lives inside the kernel; cannot be updated without a kernel rebuild | ADR-006 |
| Bulk-data IPC path | 256-byte control IPC blocks workloads like video, file I/O, LLM streaming; today’s drivers paper over with chunking | ADR-005 |
| Runtime behavioral AI (advisory) | No detection of anomalous capability usage patterns | ADR-007 — AI consumes the audit channel, recommends to policy service, never decides directly |
Open / Not Yet Designed
| Gap | Impact | Notes |
|---|---|---|
| ObjGet access control | Any process can read any object by hash | Hashes are not secrets, so this is not strictly a defect — but per-object ACLs may be wanted for sensitive content. ObjectStore already has the ObjectCapSet field; needs an enforcement story. |
| Capability expiry / TTL | Granted capabilities last forever unless revoked | Add TTL field to Capability, check in verify_access(). Useful for short-lived delegations (e.g., “let this one binary read this one file”). |
| IPC rate limiting | No defense against IPC flooding DoS | on_send hook — track send count per process per interval. Becomes more important once channels exist (ADR-005), since channels open a path the kernel doesn’t per-byte inspect. |
| Cryptographic capabilities | Capabilities don’t work across networked CambiOS nodes | Replace kernel tables with signed tokens. Only matters once Yggdrasil mesh networking lands; not a v1 concern. |
Done (Historical)
Bootstrap Principal hardening— Hardware-backed YubiKey root of trust. Secret key never enters kernel memory. Boot modules signed at build time, verified at load time.ELF signature verification—SignedBinaryVerifierenforces Ed25519 signatures (ARCSIG trailer) on all boot modules.IPC sender_principal stamping— Kernel stamps unforgeable identity on every control-IPC message.
Priority Order for Phase 3
The Phase 3 items have a forced ordering, captured in STATUS.md § Phase markers:
- Capability revocation — every other Phase 3 item depends on this. Channels need it (to tear down a compromised attach), policy service needs it (so its decisions can have effect on existing capabilities), audit telemetry doesn’t strictly need it but is useless without it.
- Audit telemetry channel — precedes the AI work because the AI consumes this stream.
- Channels — bulk-data path. Unblocks DHCP, real video/audio, LLM serving.
- Policy service — externalizes
on_syscallonce revocation exists to give policy decisions their teeth. - Behavioral AI — sits on top of all four. Watches the audit channel, emits recommendations into the policy service’s input.
For test coverage of the currently-enforced security points, see STATUS.md § Test coverage.
Architectural Invariants
These properties must hold after every change to security-related code:
No binary runs without verification. Every path from raw bytes to executing code passes through
BinaryVerifier::verify().No IPC without capability. Every
send_messageandrecv_messagepasses throughverify_access(). There is no “internal” send that skips the check.No delegation without authorization.
can_delegate()enforces that the source holds the delegate right and is not escalating beyond its own rights.Interceptor is not optional. The interceptor is set at boot and cannot be removed at runtime. Every IPC operation passes through it. The interceptor and capability check are independent — bypassing one does not bypass the other.
Verification before allocation. The ELF verifier runs before any frame allocation, page mapping, or process creation. A denied binary consumes zero resources.
Deny by default. A new process holds zero capabilities. It cannot do anything until explicitly granted access.
Sender identity is unforgeable. The kernel stamps
msg.sender_principalin the IPC send path. User-space values are overwritten. A receiving process can trustsender_principalas kernel-attested.Only bootstrap can bind identity.
SYS_BIND_PRINCIPALchecks the caller’s Principal againstBOOTSTRAP_PRINCIPAL. No other process can assign identities, even if it holds all IPC capabilities.Only owners can delete objects.
SYS_OBJ_DELETEverifies the caller’s Principal matches the object’sownerfield. This is enforced in the kernel, not just in user-space services.