Run an egress redaction filter in a worker: abort denies sharing, primary task survives
Use when an outbound privacy filter must survive hostile depth, size or matcher cost without taking down the task that produced the document. Covers worker isolation done correctly, the parent-side pitfalls that re-expose the primary process, crash artifacts as a leak channel, a warning cardinality invariant, and two scoring corrections found under adversarial review.
Run an egress redaction filter in a worker: abort denies sharing, primary task survives
When this applies
A component screens an outgoing structured document for identifying strings before an optional sharing path transmits it. Untrusted input can shape the document, so extremely deep nesting, very large payloads, or matcher-hostile text can exhaust the stack, memory, or CPU. The task that produced the document must finish normally regardless of what the filter does. This skill assumes the base design already validates decoded values, budgets the walk from one ledger, and returns canonical bytes; it covers only how to isolate that filter and what the isolation gets wrong when done naively.
The failure it prevents
Two crashes are not exceptions. A stack overflow and an out-of-memory kill terminate the whole process, so an in-process filter wrapped in a catch-all still takes the primary task down with it. Moving the filter to a worker fixes that only if the parent side is also hardened. Adversarial review of a draft that stopped at "spawn a worker" found the primary process re-exposed through its own serializer and through pipe signals, the whole document leaking through worker crash artifacts, and telemetry whose series count grew with attacker input despite a closed code enumeration.
Steps
- Hold deny in the parent before spawning. The verdict variable starts at deny. Only a worker that exits with status zero after writing a length-framed, terminator-checked result over a dedicated descriptor flips it to allow. End of stream without the terminator, a nonzero exit, a signal kill, or deadline expiry all leave deny untouched. Cap how many result bytes the parent will read, since canonical escaping expands input by a small constant factor.
- Feed the worker bytes the primary already materialized. If the sharing path receives an in-memory object, the parent must serialize it to reach the pipe, and a recursive encoder on a deep document overflows the parent stack before any cap applies. Accept only bytes, or serialize in the parent with the same non-recursive, budgeted serializer the worker uses.
- Harden the pipe on the parent side. A worker that dies before draining its input raises a broken-pipe signal in the parent, whose default action terminates the process. A worker wedged in the matcher stops reading and the parent blocks on write once the pipe buffer fills. Ignore the broken-pipe signal, use timed or non-blocking writes, and put the deadline around the write phase as well as the read phase.
- Treat worker crash artifacts as a disclosure channel. The worker holds the plaintext document. Disable core dumps and crash reporters in the worker, route its standard error to a sink that is never persisted, and give it a minimal environment. Runtime fatal-error text often includes program locations and sometimes the string being processed.
- Give the worker its own limits and a single emitter. Apply an address-space cap and a fixed stack size to the worker. Run at most one worker at a time and deny concurrent requests with a busy code. Decide that one side emits telemetry for a pass: a parent-initiated kill records the deadline code, not the crash code, so a kill race cannot produce two records for one pass.
- Keep the primary task decoupled in both directions. The primary task writes its output before the sharing job is enqueued and never reads the verdict. If the primary task is an agent that reads its own console, route the user-facing skipped-for-limits line to a channel the agent does not read, or the warning becomes an indirect coupling.
- Bound warning cardinality with a stated invariant. One detailed record per code per fixed window, then counters only, then a single flood record when a per-window cap is hit. Labels come only from the compile-time code enumeration and schema-derived field pointers. Collapse array positions to a wildcard, since instance pointers give one series per element. Report anything beneath a decoded nested document under the outer field pointer plus a fixed nested marker, never the inner keys. With those two constraints the series count is the product of two closed sets and records per window are the code count plus one.
- Score per token and per whole string. Per-token scoring alone is bypassed by spaces around separators or inside segments, which are common on some platforms. Keep whole-string signals as well, and make the tokenizer whitespace class an explicit closed list. Reject default-ignorable and bidirectional controls rather than stripping them, but note that visible lookalike separators and percent-encoded separators are neither, so decode percent forms in a bounded round and map known confusable separators before scoring.
- Make the deep-document test consistent with the caps. With a non-recursive parser and a depth cap, a synthetic deep document must yield a deterministic depth-cap deny from a normally exiting worker. Exercise the crash path with fault injection instead. Assert that the primary output is complete and unchanged, that transmit was never invoked, and that exactly one telemetry record exists for the expected code.
Limits
The deadline path is nondeterministic by nature. A legitimately allowable payload can be denied on a loaded host, and the recorded code for an abort can differ between deadline and budget depending on timing. A virtual clock cannot cover this, because the parent timer must be real to kill a wedged worker. Whether allocation failure under an address-space cap surfaces as a catchable error inside the worker or as a kernel kill seen by the parent is platform dependent; the verdict is deny either way, but the code differs. A keyed hash included for correlation is a stable pseudonym within its rotation epoch and links records from one user; keep epochs short and state whether whole fields or tokens are hashed. The scoring rules block path shapes only; a bare account or host name with no separator structure passes unless the field allowlist forbids free-form text there.
Basis
Reasoned analysis with one independent adversarial review. No tests were executed, no code was run, and no implementation was inspected. Pipe buffer behavior, fatal-error output contents, confusable mappings under normalization, and allocation-failure behavior under a memory cap are the items the review flagged as needing an executed test to confirm.