# In what scenarios must an instance treat capability confinement as unsound when a sandboxed tool holds a live, writable reference to the egress policy it is subject to **Date of Record:** 2026 November 11 **Scope:** Applies to tool sandboxes where capabilities (filesystem handles, sockets, subprocess rights) are confined at grant time and egress is filtered by a policy object the tool process can reach through its own address space or an inherited descriptor. Does not apply to sandboxes where the policy is enforced in a separate privilege domain (hypervisor, kernel LSM, or a broker process the tool cannot address), nor to purely static, immutable allowlists compiled into the tool image. ## Core Distinction The distinction that matters is between *capability confinement* and *policy confinement*. Capability confinement asserts: a tool can only exercise rights it was explicitly granted, and those rights are unforgeable. Policy confinement asserts: the rules governing how granted rights may be used (where bytes may go, which hosts are reachable) cannot be modified by the holder of those rights. These are independent invariants, and most sandbox designs conflate them. A tool may be perfectly capability-confined—it genuinely cannot open a socket it was not granted—while being entirely policy-unconfined, because the egress filter it is subject to lives in memory the tool can write. The failure is non-obvious because capability audits pass: every right exercised is a granted right. The tool never forges a capability; it merely *redefines the boundary that capability is checked against.* The invariant that is silently violated: **the egress policy must be in a strictly lower privilege domain than any process whose granted capabilities include write access to memory or descriptors reachable from that policy.** If the tool can write the policy, the policy is not a constraint on the tool—it is a suggestion the tool may revise. ## What Was Observed A sandboxed tool was granted a single outbound socket capability, restricted by an egress policy object held in the supervisor's address space but passed to the tool as a read-only memory-mapped region for "transparency"—the tool was told which destinations it was permitted to reach so it could fail fast rather than attempt blocked connections. The mapping was created `PROT_READ`. Two things went wrong in sequence. First, the policy object contained a pointer to a mutable allowlist array that lived in a shared page; the read-only mapping protected the struct but not the array it referenced, and the array was in a page the tool also held a writable mapping to (it was the tool's own scratch buffer, reused by the allocator). Second, the tool's connection-attempt path read the allowlist through the struct pointer, so writing the scratch page silently rewrote the effective egress policy. The tool then opened a connection to an address that had never been in the allowlist, and the supervisor's enforcement—which re-read the same array at connect time—permitted it. The capability audit showed exactly one socket, to a destination the (now-mutated) policy allowed. Nothing was forged. The confinement held at the capability layer and failed completely at the policy layer. The exfiltration channel was live for the duration between the scratch-buffer write and the next policy reload, which in the observed configuration was never, because the policy was loaded once at sandbox construction. A second variant was observed with inherited descriptors: the tool received a pipe descriptor for logging. The egress policy was keyed by a file descriptor table index that the tool could `dup2` over, remapping the "logging" descriptor slot onto the socket slot so that policy lookups by index resolved to the wrong object. The tool did not need write access to policy memory at all; it needed only the ability to renumber its own descriptor table. ## Condition of Validity Capability confinement is sound as an egress control **only if all three of the following hold simultaneously**: 1. **No aliasing.** No memory reachable for writing by the tool aliases any memory read by the enforcement path. This must be checked transitively: a read-only mapping of a struct containing a pointer to writable memory does not satisfy this. The correct test is reachability of the *enforcement read*, not protection bits on the mapping the tool was handed. 2. **No descriptor renumbering.** Policy decisions are not keyed by anything the tool can mutate, including its own descriptor table, its pid, its cwd, or environment strings. Decisions must be keyed by the unforgeable capability identity itself (a kernel object reference or a broker-issued token), not by a name the tool controls. 3. **Reload is atomic and out-of-band.** If the policy can change during the sandbox's lifetime, the change must be applied by a party the tool cannot influence, and applied atomically with respect to in-flight connection attempts. A policy read at connect time and a policy write by the tool are a race the tool wins by construction, because it controls when it connects. If any of the three fails, the correct classification is not "capability-confined sandbox with a policy bug" but "no egress confinement at all," regardless of what the capability audit reports. ## Decision Boundary The decision an operator must make is whether to trust a capability audit as evidence of egress confinement. The boundary is sharp: - **Trust the audit** when the enforcement path reads only from a privilege domain the tool cannot address—kernel-side policy, a broker process in a separate address space, or a hardware-isolated filter. Here the tool's capabilities and the policy are in different domains and the audit is meaningful. - **Do not trust the audit** whenever the tool and the enforcement path share an address space, a descriptor table, or any mutable object graph. In this regime the tool's write capabilities are, transitively, policy-modification capabilities, and the audit's "only granted rights were exercised" is vacuously true and evidentially worthless. The trap is the middle case: a read-only mapping that *looks* like separation. Read-only protection on the struct is not separation when the struct points into writable memory. The boundary is reachability, not protection bits. An operator who reasons "the tool cannot write the policy, the mapping is `PROT_READ`" has made the exact error this record documents. When the boundary is crossed, the only sound remediation is relocation of enforcement, not hardening of the shared object. You cannot patch aliasing out of a shared address space reliably; you can move the read to a domain the write cannot reach. ## Operational Effect The operational consequence is that egress policy must be treated as a *capability in its own right*, held by the enforcement domain and never shared by reference into the confined domain. Concretely: policy decisions must be made by reading capability identity and destination from a source the tool cannot rewrite, at the moment of the syscall or broker request, in a domain the tool cannot address. The tool may be *told* the policy for fail-fast ergonomics, but that telling must be a copy into tool-owned memory that the enforcement path never reads back. The moment the enforcement path reads anything the tool can write, the confinement is nominal. A secondary effect: policy reload cadence becomes a security parameter. A policy loaded once at construction and never reloaded is not safer—it is a frozen target. The observed exfiltration succeeded precisely because the mutated array was never re-validated. Reload must be driven out-of-band, and in-flight connections must be re-checked against the reloaded policy or dropped. The record below encodes the check as an assertion an agent can run before trusting any egress decision that shares a domain with the confined tool. ```python class PolicyDomainViolation(RuntimeError): """Raised when an egress decision is made in a domain the confined tool can write.""" def assert_egress_enforcement_isolated( tool_writable_regions: list[tuple[int, int]], enforcement_read_regions: list[tuple[int, int]], policy_keyed_by_tool_mutable_name: bool, ) -> None: """ tool_writable_regions: (base, length) of every region the tool can write, computed transitively through pointers reachable from any mapping it holds. enforcement_read_regions: (base, length) of every region the enforcement path reads when deciding an egress action. policy_keyed_by_tool_mutable_name: True if the decision is keyed by a descriptor index, pid, cwd, or env string the tool controls rather than by an unforgeable capability identity. """ for e_base, e_len in enforcement_read_regions: e_end = e_base + e_len for t_base, t_len in tool_writable_regions: t_end = t_base + t_len if e_base < t_end and t_base < e_end: raise PolicyDomainViolation( f"egress enforcement reads [{e_base:#x},{e_end:#x}) which " f"aliases tool-writable [{t_base:#x},{t_end:#x}); " f"capability confinement does not imply policy confinement" ) if policy_keyed_by_tool_mutable_name: raise PolicyDomainViolation( "egress decision keyed by a tool-mutable name (fd index, pid, cwd, env); " "the tool can renumber or rewrite the key and redirect the decision" ) ```