Authority Has a Lifetime.
Stronger request isolation is our proposed response to PHP lifecycle failures. ZR-41 would bind memory and authority to the request that owns them, aiming to reduce compromise paths in the sites people browse. This page sets out that design and the trust boundaries it cannot eliminate.
Request-local memory
Every request executes inside a request cell: a region of mutable memory, a capability set, a resource budget, and a list of acquired resources. Values a request creates are allocated in its region. Values that must be shared across requests, such as compiled code, configuration, and interned constants, are created during boot and frozen before the first request starts.
This separation is the operational form of the first clause of the Sealed Execution Contract. It does not stop requests sharing data; it stops them sharing mutable data except through a service that mediates access.
// Rust-style sketch of a request cell. Illustrative; not compiled engine code.
pub struct RequestCell {
region: Region, // request-owned mutable memory
caps: CapabilitySet, // what this request may touch
budget: Budget, // remaining instructions, bytes, wall time
open: Vec<ResourceToken>, // acquired files, sockets, statements
}
impl RequestCell {
pub fn open(grant: Grant, budget: Budget) -> Self {
RequestCell { region: Region::new(), caps: grant.into_caps(), budget, open: Vec::new() }
}
// The cell is consumed: once run returns, no code can reach its region again.
pub fn run(mut self, entry: &ZirModule) -> Outcome {
let result = entry.execute(&mut self.region, &self.caps, &mut self.budget, &mut self.open);
Outcome::from(result) // only owned, copied output leaves the cell
} // self is dropped here
}
impl Drop for RequestCell {
fn drop(&mut self) {
// Reverse acquisition order: statements before connections, files before dirs.
while let Some(token) = self.open.pop() {
token.release_within(RELEASE_DEADLINE);
}
self.caps.revoke_all(); // outstanding handles now fail their generation check
// region memory is reclaimed as a unit when its fields drop
}
}The sketch leans on Rust ownership for one specific property: run takes the cell by value, so after it returns no reference into the region can exist in safe code, and Drop is the single place where release happens. It does not lean on Rust for PHP semantics; those live in ZIR and the engine.
Typed and generation-checked handles
PHP code never sees raw pointers, but the engine still hands out identifiers: object handles, resource handles, reference slots. In ZR-41 each handle carries a type, the region it belongs to, and a generation number. Releasing a slot increments its generation, so a stale handle presented later fails a cheap equality check instead of reading reused memory.
Handles are also tagged with their owning cell. A handle from request A that somehow reached request B (through a bug in a shared service, for instance) fails ownership validation before dereference. This is a runtime guard, not a static proof, and the design states that plainly: the guard turns one class of bug into a defined error, and the error is logged as an engine fault. A full example is on the Execution Notes page.
Capability-mediated effects
Ambient authority is the default in most runtimes: any code in the process may open any file the process can open. ZR-41 replaces ambient authority with capabilities granted when the cell is created.
- Files: a capability names a directory root and a mode. Paths are resolved inside that root; traversal outside it fails.
- Databases: a capability references a named connection pool and, optionally, a statement allow-list. The request never holds the credentials.
- Network: egress capabilities name destinations. A request without one cannot open sockets at all.
- Clocks and randomness: reading time is an effect too. Making it a capability lets tests run with a fixed clock and makes nondeterminism visible.
A missing capability is not a crash. The effect operation raises a catchable error inside the program at the point of the call, the effect does not occur, and the attempt is recorded. That failure path is as much part of the design as the success path; the model page defines it formally.
Shared data and shared services
Real applications share things: compiled templates, configuration, connection pools, caches. ZR-41 distinguishes two kinds.
Shared immutable data is frozen at boot and readable by every cell. Because nothing can mutate it, reading it cannot create a path between requests. Lazily populated caches do not qualify.
Controlled shared services are mutable but live outside every cell, behind an interface that accepts and returns copied values. A cache service, a session store and a connection pool are all services. The isolation property does not cover their internals: it covers the fact that a request can only reach them through a capability and only exchange copies with them. The correctness of each service is a separate obligation.
Cancellation and teardown
Requests end in four ways: normal completion, an uncaught error, cancellation (the client disconnected or an operator stopped it), or budget exhaustion. All four converge on the same teardown sequence.
Bounding teardown matters. A destructor that loops forever, or a socket close that blocks on the network, must not hold the worker hostage. Finalizers therefore run under their own small budget; overruns are abandoned and logged, and release continues. This is a deliberate compatibility trade-off: a destructor that would have run to completion in the reference engine may be cut short here.
Native extension trust boundary
Native extensions are outside the boundary. Code in C can read or write any memory in the process, so no property of the request cell holds against a faulty or hostile extension running in the same address space. ZR-41 makes that explicit rather than implicit:
- Extensions are reached through a shim that copies arguments out of the region and results back in. No region pointer is ever passed to native code.
- Each extension is listed with the capabilities its shim may exercise on a request's behalf.
- Where practical, an extension can run out of process behind the same shim interface, trading latency for a real boundary.
- Reimplementing a widely used extension inside the engine is an option evaluated case by case, never assumed.
Limits and assumptions
The isolation properties hold only under assumptions, and they should be read with them:
- The engine, the ZIR checker and the shared services are correct. Bugs there can break any property.
- Native extensions in the same process behave as their interfaces state.
- The host language's safety guarantees hold for the engine code that relies on them, with any unsafe blocks reviewed separately.
- The operating system and hardware enforce process memory protection.
- Timing, cache and other side channels are out of scope.
The NULL TERMINAL search tool on this site is unrelated to these mechanisms. It is a source-discovery tool and does not isolate, sandbox or vet the pages it links to.