Language Runtimes · Runtime Security

Capability References and the Absence of Ambient Authority

Why naming a resource by path or file descriptor grants more than the caller needs, and how unforgeable references make authority a property of what a program holds rather than who it claims to be.

An access-control decision made at the moment of use requires the system to answer a question about identity: who is asking. A decision made at the moment of grant requires the system to answer a question about the object: what was handed over. The second style of question is easier to answer correctly, and the difference between them is the difference between ambient authority and capabilities.

The confused deputy in a single function call

Ambient authority means the program’s permissions are properties of the process, not of the operation. A Unix process running as a user may open any file that user may open, whether or not the code path in question has any business touching it. Nothing in the open call distinguishes a request from the module that owns the file from a request from a module that was tricked into making it.

The standard illustration is a privileged program that accepts a filename from an untrusted caller. The caller names a file the caller cannot read but the privileged program can, and the program, being helpful, opens it and returns the contents. The deputy was not compromised; it did exactly what it was asked, using authority it held ambiently. The defect is that the caller could name an object it had no reference to.

Capability systems remove the naming step. A caller that holds no reference to a file cannot name it, because names are not globally meaningful. The deputy receives references, and can only act on the references it received.

What makes a reference a capability

Three properties distinguish a capability from a handle in the ordinary sense.

It is unforgeable. The program cannot manufacture a reference to an object it was not given. In a language runtime this is usually achieved by making references ordinary pointers whose validity is enforced by the memory model: a forged pointer that does not point at a live object cannot be dereferenced safely, and the runtime’s type checks reject it before use. In a kernel it is achieved by keeping the capability table outside the reachable address space of the subject.

It designates a specific object, not a pattern. A file descriptor designates an open file description, which is more specific than a path. A capability designates a single object with a single set of rights.

It carries its rights with it. The right to read and the right to write may be separate capabilities to the same object, so a program given a read-only reference has no mechanism to write, not merely a policy that forbids writing.

The second and third properties are what make revocation and attenuation expressible, which attenuation and revocation develops. The first property is the one the runtime must provide, and providing it is not free.

Implementing unforgeability in a managed heap

A language with a safe type system and no unsafe escape already provides a weak form of unforgeability: a value of type File can only have been produced by code that constructs one, and code outside the module cannot call the constructor if it is not exported. Authority then becomes a matter of which constructors a module can reach.

use std::fs::File;
use std::io::{self, Read};
use std::path::Path;

pub struct ReadOnlyFile {
    // Private field: the only way to build one is through the constructor below.
    file: File,
}

impl ReadOnlyFile {
    pub fn open(path: &Path) -> io::Result<Self> {
        Ok(ReadOnlyFile { file: File::open(path)? })
    }
    pub fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
        // &File implements Read, so a read takes a shared borrow.
        (&self.file).read(buf)
    }
}

The type has no write method and its field is private, so a holder of ReadOnlyFile cannot write through it. The guarantee is not absolute: the descriptor namespace of the process is global, so code that can call the platform’s write function on an arbitrary descriptor number bypasses the wrapper entirely. Capability discipline in a language without a sealed authority model is a convention enforced by module privacy, not by the type system. Rust’s ownership rules help, but any code with unsafe and the right imports can address any descriptor number.

A runtime that wants a real guarantee must make the global namespace unreachable. This is the design of capability-based microkernels, where a thread’s capability table is the only route to any object, and there is no system-wide descriptor number space. In a language runtime the analogous design puts every external effect behind a single interface object that the host constructs, so the program’s entire authority is the set of methods reachable from that object.

Object-capability discipline in ordinary code

The object-capability model applies the same reasoning inside a single program. Authority is the set of objects a piece of code can reach by following references, starting from its parameters and its lexical environment. Two rules make the model hold:

No global mutable state reachable by untrusted code. A global registry of services, a singleton logger, or a static map from names to objects is an ambient authority channel, because any code can reach it without being handed anything. Module-level static variables are the usual violation.

No broad primitives on the ambient objects. If every object inherits from a base type that exposes reflection or a way to reach the module loader, then holding any object grants everything. Reflection in particular is a capability amplifier: an object whose methods allow enumerating fields and invoking them at runtime converts a reference to one object into a reference to anything reachable from it.

A practical middle ground is the membrane: a wrapper placed between two subsystems that intercepts every reference crossing it and wraps each object it returns in a proxy that applies a policy. The proxy exposes a restricted interface and, when the guest calls a method, the membrane decides whether the call is permitted and whether the result needs wrapping. This is how a runtime can host untrusted plugin code in the same process while presenting it with a narrowed view of the host.

Why ambient authority survives anyway

Despite the model’s cleanliness, most runtimes expose ambient authority, for reasons that are worth naming rather than dismissing.

Interoperability requires names. Files, sockets, and environment variables are the interfaces the rest of the system speaks. A capability-only runtime that cannot open a path cannot run a program that reads a configuration file the user selected.

Ambient authority is convenient for programmers. Passing a reference through six layers of code to reach the layer that needs it is tedious, so languages grow globals, thread-locals, and dependency injection containers, each of which is a mechanism for making a reference reachable without threading it.

Some authority is genuinely process-wide. A process’s working directory, its locale, its limit on open descriptors, and its signal dispositions affect every thread. There is no object to hand over.

The result in mainstream runtimes is a hybrid: a few ambient channels, carefully enumerated, plus capability-style references for the operations where the distinction matters. A server framework that passes a request object rather than letting handlers reach a global connection pool is a capability design at that seam. A plugin host that passes a HostApi object instead of exposing the host’s modules is the same design.

Delegation and the direction of the flow

Capabilities make delegation a data flow rather than a policy decision. To let a subordinate read a file, the holder passes a read-only reference. The subordinate’s authority grows by exactly that reference. No administrator is consulted, and no identity is checked, because the act of passing is the act of granting.

This property is what makes capabilities composable. A program can build a processing chain in which each stage receives only the references it needs, and the composition’s authority is the union of the stages’ references. The union is computable by inspection, which is the point: the security review of the chain is a review of which references cross which boundaries.

The corresponding weakness is that passing is not always visible. In a language with closures, a reference captured by a closure escapes with the closure, and the code that receives the closure receives the captured authority. Closures are therefore capability transfers, and a plugin API that accepts callbacks is granting the callback’s author whatever the callback closed over. Languages with linear types or ownership annotations make the transfer explicit; the subject appears in linearity and aliasing as confinement.

The boundary the model needs

Capabilities describe how authority is distributed; they do not by themselves describe what the authority can affect. A read-only file reference is only read-only if the underlying object’s implementation actually restricts reads. A capability to a database handle is only a capability to that database if the handle’s operations cannot be used to reach the connection string that would open another.

The model therefore depends on a layer beneath it that is the reference monitor in the sense of the first article in this cluster: something must enforce that a reference designates one object and that its rights are as advertised. Capabilities are the distribution mechanism; mediation is the enforcement. Neither is sufficient alone, and most real systems that are secure are secure because the two layers are both present.

References

  1. Capability Myths Demolished — Mark S. Miller, Ka-Ping Yee, Jonathan Shapiro
  2. The Protection of Information in Computer Systems — Jerome H. Saltzer and Michael D. Schroeder, Proceedings of the IEEE, 1975
  3. EROS: A Principle-Driven Operating System from the Ground Up — Jonathan S. Shapiro and Norman Hardy, IEEE Software, 2002