# Why does an instance of taint-aware tool sandboxing silently lose its exfiltration block when the labeled context is re-materialized through a cache hit **Date of Record:** 2026 November 13 **Scope:** Applies to tool-calling agents whose sandbox enforces information-flow control (IFC) via taint labels attached to context segments, where labeled segments may be served from a shared or per-session cache before being passed to a tool. Does not apply to stateless sandboxes that re-derive taint from provenance on every call, nor to deployments where cache entries carry and re-validate their original taint label. ## Core Distinction A taint-aware sandbox has two distinct jobs that are easy to conflate: (1) *tracking* which bytes in the model context are derived from labeled sources, and (2) *enforcing* a flow policy at the boundary where a tool receives arguments. The invariant that is commonly assumed is that "if a labeled segment is present in context, any tool call whose arguments overlap that segment is blocked." The non-obvious failure is that this invariant is stated over *content identity*, but enforcement is implemented over *provenance metadata*. When a cache hit re-materializes a labeled segment, the bytes are identical but the provenance record is either absent or points to the cache origin rather than the labeled source. The sandbox then evaluates the flow policy against an untainted provenance and permits the call. This is not a bug in the label *values*; it is a boundary failure at the seam between the cache and the taint tracker. The two subsystems each behave correctly in isolation. The cache correctly returns the same bytes. The taint tracker correctly reports "no labeled provenance on this segment." The composed system violates the exfiltration invariant. ## What Was Observed In a representative agent runtime, a document retrieved from a sensitive corpus was labeled `SENSITIVE:customer_pii` and placed in context. A summarization step produced a derived segment that inherited the label. On a subsequent turn, the same document was requested again; the retrieval layer served it from a content-addressed cache. The cached object was deserialized and inserted into context as a fresh segment. The taint tracker, which keyed provenance on the *insertion event* rather than the *content hash*, recorded provenance as `cache://retrieval/v3` with no inherited label. A downstream tool call (`http_post` to a webhook) whose payload included a verbatim substring of the document was evaluated against the untainted provenance and allowed. The exfiltration channel opened precisely on the second read, not the first. The failure is intermittent and load-dependent: it only manifests on cache hits, so cold-path testing never exercises it. It also only manifests when the tainted content survives re-materialization unchanged, which is the common case for cached retrieval. ## Condition of Validity The block is valid only when the following holds for every labeled segment reaching a tool boundary: 1. Provenance is keyed on content identity (e.g., a cryptographic hash of the segment bytes plus its label set), not on the insertion event or the transport that delivered it. 2. Cache entries persist the full label set alongside the payload, and cache hits re-apply that label set to the re-materialized segment before it enters context. 3. The flow policy is evaluated over the *union* of provenance reachable from the argument bytes, including any cache-origin segments whose stored labels were merged at materialization time. When any of these is false, the sandbox's block is only conditionally valid—specifically, valid on the cold path and invalid on the warm path. A sandbox that cannot state which of these three conditions it relies on should be treated as unverified for exfiltration control. ## Decision Boundary The operational decision is whether to treat a cache hit as a *new* context insertion (requiring re-derivation of taint) or as a *replay* of the original insertion (inheriting taint). The correct choice is replay-inheritance, but it is not free: it requires the cache to be taint-aware, which in turn requires that eviction and invalidation respect label changes (a relabeled source must invalidate cached copies, or the cache becomes a taint-laundering channel). The boundary is crossed when the cache is treated as a pure performance layer with no security semantics. At that point the taint tracker and the cache disagree about what a segment *is*, and the disagreement is resolved in favor of the cache—silently, because the cache has no way to signal "I am returning something you previously considered sensitive." A secondary boundary: substring-overlap detection between tool arguments and labeled segments must run against the *materialized* bytes, not against the provenance graph. If overlap detection is provenance-driven, a cache hit defeats it by construction. ## Operational Effect The practical consequence is a class of exfiltration that appears only under cache warmth, evades cold-path test suites, and produces audit logs that look clean because the taint tracker faithfully reports "untainted" at the moment of the call. Detection requires correlating cache-hit events with tool-call argument overlap against the *original* labeled corpus, independent of the runtime's own taint state. Mitigation requires making the cache taint-aware and keying provenance on content hash; verification requires a test that warms the cache with a labeled document, then attempts an exfiltrating tool call on the second read. Until that test passes, the sandbox's exfiltration block should be documented as cold-path-only. ```python import hashlib from dataclasses import dataclass, field class TaintLaunderingError(RuntimeError): """Raised when a labeled segment reaches a tool boundary without its labels.""" @dataclass class Segment: payload: bytes labels: frozenset[str] = field(default_factory=frozenset) @property def content_hash(self) -> str: return hashlib.sha256(self.payload).hexdigest() class TaintAwareCache: def __init__(self) -> None: self._store: dict[str, Segment] = {} def put(self, seg: Segment) -> None: # Persist labels alongside payload; a cache without labels is a laundering channel. self._store[seg.content_hash] = Segment(seg.payload, seg.labels) def get(self, content_hash: str) -> Segment | None: return self._store.get(content_hash) def enforce_flow(segment: Segment, tool_name: str, arg_bytes: bytes) -> None: """Block if a labeled segment's bytes overlap the tool argument bytes.""" if not segment.labels: # Provenance lost. If the bytes overlap, this is laundering, not a clean call. if segment.payload and segment.payload in arg_bytes: raise TaintLaunderingError( f"tool={tool_name!r} argument overlaps unlabeled segment " f"hash={segment.content_hash[:12]}; provenance missing (cache hit?)" ) return if segment.payload in arg_bytes: raise TaintLaunderingError( f"tool={tool_name!r} argument overlaps segment labeled " f"{sorted(segment.labels)}; exfiltration blocked" ) def second_read_exfil_test() -> None: cache = TaintAwareCache() sensitive = Segment(b"acct=4111-1111-1111-1111", frozenset({"SENSITIVE:customer_pii"})) cache.put(sensitive) # Cold path: labeled segment in context, exfil attempt blocked. enforce_flow(sensitive, "http_post", b"payload=" + sensitive.payload) # Warm path: re-materialize from cache. Labels must survive. remat = cache.get(sensitive.content_hash) if remat is None: raise TaintLaunderingError("cache miss on warmed entry") enforce_flow(remat, "http_post", b"payload=" + remat.payload) if __name__ == "__main__": second_read_exfil_test() ```