# When must an instance Treat Retrieved Context as Tainted: The Label-Laundering Boundary in Dynamic IFT for Tool and RAG Pipelines **Date of Record:** 2026 November 12 **Scope:** Applies to agentic systems that propagate dynamic information-flow (IFT) taint labels from untrusted sources (tool outputs, RAG retrievals, web fetches) across LLM context boundaries, and must decide whether a downstream sink (tool call, code execution, outbound message) inherits a taint. Does not apply to static label systems where taint is assigned at design time, nor to systems where all context is uniformly trusted. ## Core Distinction The non-obvious boundary is between **label propagation** and **label laundering**. In a naive dynamic-IFT design, a taint label attached to a retrieved chunk is expected to flow into any derived value the model produces. But an LLM is a *lossy, re-encoding* channel, not an identity function. When a tainted string is paraphrased, summarized, translated, or reformatted by the model, the output token sequence no longer carries the label's provenance — the label does not survive the model's forward pass unless the runtime re-attaches it explicitly. The critical distinction is therefore: - **Propagation:** taint flows because the runtime *carries* the label alongside derived artifacts (structured taint objects, not strings). - **Laundering:** taint is *dropped* because the runtime treats model output as a fresh, untainted value. An instance must treat retrieved context as tainted not when the *string* is present in the context window, but when the *derivation chain* from a tainted source to the sink has not been severed by an explicit declassification decision. The failure mode is a system that taints the input string but not the *model output derived from it*, producing a clean-looking sink call that is semantically downstream of attacker-controlled text. ## What Was Observed In a pipeline where a retrieval tool returned a document chunk labeled `TAINT:external`, the runtime correctly marked the chunk. The agent then summarized the chunk into a planning note, and the planner emitted a tool call `send_email(to=..., body=...)` whose body was a rephrased version of the chunk. The runtime evaluated the `send_email` arguments as untainted because they were *newly generated tokens*, not the original labeled string. The taint check passed; the exfiltration succeeded. The label was attached to the *bytes*, not to the *causal derivation*. Observed invariant violation: `taint(output) ⊄ taint(inputs)` whenever the transform is a model invocation rather than a pure function. ## Condition of Validity This boundary is valid (the naive string-label approach is unsound) under all of the following: 1. The sink's arguments are produced by a model invocation whose prompt includes tainted context. 2. The model is permitted to paraphrase/re-encode (i.e., the transform is not constrained to verbatim copy). 3. The runtime has no explicit declassification gate between tainted source and sink. 4. The taint label is stored on the payload rather than on a derivation node in a provenance graph. It is *not* valid (naive propagation is sound) only when the transform is a pure, verifiable function (exact substring copy, structured field extraction with schema validation) where the runtime can prove the output is a function of the labeled input. The moment an LLM sits in the transform, the invariant `taint(output) ⊇ taint(inputs)` fails. ## Decision Boundary The operational rule: **An instance must treat retrieved context as tainted at every sink whose argument derivation graph reaches a tainted node, regardless of whether the literal tainted bytes appear in the argument.** Concretely, a sink call is tainted if *any* ancestor in its provenance DAG is tainted, unless a declassification node (human approval, allowlisted transform, or a verified pure function) explicitly severs the edge. Equivalently: taint is a property of *provenance*, not of *payload*. A paraphrased summary is tainted because its provenance includes the retrieval; the fact that the string differs is irrelevant. This is the inverse of the intuition that "the label is on the text" — the label must be on the *node*. ## Operational Effect Systems that attach labels to payloads will systematically under-taint LLM-mediated derivations and will pass prompt-injection payloads through summarization/planning stages into privileged sinks. The mitigation is to move taint from strings to a provenance graph: every model invocation is a node with edges to its prompt's tainted inputs, and sinks consult the graph, not the string. This raises the cost of false positives (benign paraphrases of tainted-but-harmless retrievals now require declassification), so the runtime needs an explicit declassification API and an audit trail. The net effect is that taint tracking becomes a *dataflow* discipline rather than a *substring* discipline, and the boundary condition is the model invocation itself — the point where the label would otherwise be laundered. ```python from dataclasses import dataclass, field from typing import Optional class TaintLaunderingError(RuntimeError): """Raised when a sink is invoked with an argument whose provenance reaches a tainted node but the runtime failed to propagate the label.""" pass @dataclass class ProvenanceNode: node_id: str tainted: bool parents: list["ProvenanceNode"] = field(default_factory=list) def is_tainted(self, _seen: Optional[set] = None) -> bool: seen = _seen or set() if self.node_id in seen: return False seen.add(self.node_id) if self.tainted: return True return any(p.is_tainted(seen) for p in self.parents) @dataclass class SinkCall: sink: str args: dict[str, ProvenanceNode] declassified: bool = False def assert_safe(self) -> None: if self.declassified: return for arg_name, node in self.args.items(): if node.is_tainted(): raise TaintLaunderingError( f"Sink '{self.sink}' argument '{arg_name}' has tainted " f"provenance (node {node.node_id}); label was laundered " f"across a model-mediated transform. Declassify explicitly " f"or block the call." ) # --- Demonstration of the boundary failure --- retrieval = ProvenanceNode("rag_chunk_42", tainted=True) summary = ProvenanceNode("planner_note", tainted=False, parents=[retrieval]) send = SinkCall("send_email", {"body": summary}) send.assert_safe() # raises: summary's provenance reaches a tainted node ```