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_syscall decisions: 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 PointStatusBlocks on FailureWhere
ELF entry point validationEnforcedBinary not loadedloader/mod.rs
ELF kernel space rejectionEnforcedBinary not loadedloader/mod.rs
ELF W^X enforcementEnforcedBinary not loadedloader/mod.rs
ELF segment overlap detectionEnforcedBinary not loadedloader/mod.rs
ELF memory limitEnforcedBinary not loadedloader/mod.rs
ELF Ed25519 signature verificationEnforcedBinary not loadedloader/mod.rs (SignedBinaryVerifier)
Capability check (IPC send)EnforcedPermissionDeniedipc/mod.rs
Capability check (IPC recv)EnforcedPermissionDeniedipc/mod.rs
Capability delegation validationEnforcedAccessDeniedipc/capability.rs
Interceptor: syscall pre-dispatchScaffolding (designed in ADR-006)Always allows; policy externalization pendingsyscalls/dispatcher.rs
Interceptor: IPC send policyEnforcedPermissionDeniedipc/mod.rs, ipc/interceptor.rs
Interceptor: IPC recv policyEnforcedPermissionDeniedipc/mod.rs, ipc/interceptor.rs
Interceptor: delegation policyEnforcedAccessDeniedipc/capability.rs
IPC sender_principal stampingEnforcedN/A (kernel stamps unconditionally)ipc/mod.rs
BindPrincipal restricted to bootstrapEnforcedPermissionDeniedsyscalls/dispatcher.rs
ObjPut Ed25519 signature verificationEnforcedPermissionDeniedsyscalls/dispatcher.rs (ObjPutSigned)
ObjDelete ownership enforcementEnforcedPermissionDeniedsyscalls/dispatcher.rs
FS service principal-based accessEnforcedError response to calleruser/fs-service/src/main.rs
Bulk-data channel capability checksDesigned in ADR-005, not implemented
Capability revocation (SYS_REVOKE_CAPABILITY)Designed in ADR-007, not implemented
Audit telemetry channelDesigned in ADR-007, not implemented
Externalized policy serviceDesigned in ADR-006, not implemented
Per-process syscall allowlistsDesigned 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 Write and WaitIrq should never call Allocate. 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

AttackCheckResult
Jump to kernel codeEntry point must be in a LOAD segmentEntryPointOutOfRange
Map pages into kernel spaceAll segments < 0x0000_8000_0000_0000SegmentInKernelSpace
Self-modifying shellcodeNo page is W+X simultaneouslyWritableAndExecutable
Aliased memory confused deputySegments must not overlapOverlappingSegments
OOM denial of serviceTotal memory <= 256 MBExcessiveMemory

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:

  1. 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.

  2. Delegation — A process that holds a capability with delegate = true can 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

  • ProcessCapabilities is a struct in capability.rs with all internal fields private. No public constructor — only CapabilityManager methods 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 CapabilityManager methods, 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 get PermissionDenied.
  • 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

AttackEnforcementResult
Process claims to be another identityKernel stamps real PrincipalForgery impossible
Unauthorized process binds identitiesOnly bootstrap Principal can call BindPrincipalPermissionDenied
Process reads another’s identityGetPrincipal returns caller’s ownNo 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-stamped sender_principal
  • DELETE commands check that sender_principal is 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:

  1. Endpoint ID < MAX_ENDPOINTS (32) — prevents out-of-bounds access
  2. Payload length <= 256 bytes — prevents buffer overflow
  3. Sender process ID != endpoint ID — prevents self-send deadlock

Hook: on_recv (Layer 3)

Current status: Enforced. One check:

  1. Endpoint ID < MAX_ENDPOINTS (32) — prevents out-of-bounds access

Hook: on_delegate (Layer 3)

Current status: Enforced. Two checks:

  1. Endpoint ID < MAX_ENDPOINTS (32) — prevents out-of-bounds access
  2. 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_CREATE is 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 ChannelCapability is 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_principal on 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)

GapImpactWhere It’s Designed
Capability revocationA capability granted in error cannot be taken back; driver updates can’t reclaim the old driver’s authorityADR-007
Audit logging / telemetry channelNo forensic trail; the AI security service has no input streamADR-007
Per-process syscall allowlistsA compromised process can invoke any syscall it has arguments forADR-006on_syscall hook routed through policy service
Externalized policy decisionsAll policy lives inside the kernel; cannot be updated without a kernel rebuildADR-006
Bulk-data IPC path256-byte control IPC blocks workloads like video, file I/O, LLM streaming; today’s drivers paper over with chunkingADR-005
Runtime behavioral AI (advisory)No detection of anomalous capability usage patternsADR-007 — AI consumes the audit channel, recommends to policy service, never decides directly

Open / Not Yet Designed

GapImpactNotes
ObjGet access controlAny process can read any object by hashHashes 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 / TTLGranted capabilities last forever unless revokedAdd 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 limitingNo defense against IPC flooding DoSon_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 capabilitiesCapabilities don’t work across networked CambiOS nodesReplace 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 verificationSignedBinaryVerifier enforces 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:

  1. 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.
  2. Audit telemetry channel — precedes the AI work because the AI consumes this stream.
  3. Channels — bulk-data path. Unblocks DHCP, real video/audio, LLM serving.
  4. Policy service — externalizes on_syscall once revocation exists to give policy decisions their teeth.
  5. 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:

  1. No binary runs without verification. Every path from raw bytes to executing code passes through BinaryVerifier::verify().

  2. No IPC without capability. Every send_message and recv_message passes through verify_access(). There is no “internal” send that skips the check.

  3. No delegation without authorization. can_delegate() enforces that the source holds the delegate right and is not escalating beyond its own rights.

  4. 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.

  5. Verification before allocation. The ELF verifier runs before any frame allocation, page mapping, or process creation. A denied binary consumes zero resources.

  6. Deny by default. A new process holds zero capabilities. It cannot do anything until explicitly granted access.

  7. Sender identity is unforgeable. The kernel stamps msg.sender_principal in the IPC send path. User-space values are overwritten. A receiving process can trust sender_principal as kernel-attested.

  8. Only bootstrap can bind identity. SYS_BIND_PRINCIPAL checks the caller’s Principal against BOOTSTRAP_PRINCIPAL. No other process can assign identities, even if it holds all IPC capabilities.

  9. Only owners can delete objects. SYS_OBJ_DELETE verifies the caller’s Principal matches the object’s owner field. This is enforced in the kernel, not just in user-space services.