ADR — Findings Runtime Privacy Filter

Formerly docs/architecture/ADR-FINDINGS-RUNTIME-PRIVACY-FILTER.md.

FieldValue
StatusAccepted (signed off 2026-04-29 — Architecture lead via user approval)
Date2026-04-28
AuthorsFindings Ledger Runtime sub-team
Modulesrc/Findings/__Libraries/StellaOps.Findings.Ledger.Runtime/
RelatedSPRINT_20260429_012_FindingsLedger_runtime_privacy_filter, SPRINT_20260429_015_FindingsLedger_runtime_documentation

In one line: before any runtime trace is persisted or aggregated, the Stella Ops Findings Ledger strips the per-process ASLR address token (@0x…) from each frame’s symbol and file — and only that token — so the same vulnerable symbol aggregates correctly across processes while line numbers, paths, and evidence flags survive intact.

Audience: Findings Ledger engineers maintaining the runtime ingest path, and reviewers assessing the privacy posture of stored trace data. Determines what the ADR-014 tables actually hold.

Context

Runtime probes (eBPF, syscall tracing, APM agents) produce stack frames where each symbol is decorated with a per-process address token of the form @0x[hex]+ (e.g. SSL_write@0x7FFEDEADBEEF). The address suffix is:

If the address tokens reach the aggregator, the bucket key includes the address — which means SSL_write@0xAA… and SSL_write@0xBB… from two processes of the same binary land in different buckets, defeating aggregation entirely. This is the operative correctness requirement captured in the integration test RuntimeTraces_PrivacyFilter_RedactsAddresses_AndAggregatesHotSymbols.

We also need to preserve evidence value — frame metadata that isn’t an address token (line number, file path without the @0x... tail, entry-point flag, vuln-function flag, confidence) is real evidence and must survive redaction.

Decision

RuntimeSymbolRedactor strips a single trailing address token, and only that token, from each frame’s Symbol and File fields. The redaction is anchored to the end of the string and is purely a string transformation — no other field is modified.

Pattern

@0x[0-9A-Fa-f]+(\+0x[0-9A-Fa-f]+)*$

End-anchored. Matches @0xDEADBEEF and @0xAA+0xBB+0xCC (the chained address form some agents emit). Mid-string 0x... literals are not matched.

Per-field rules

FieldRedacted?Rationale
RuntimeFrame.SymbolYESStrip address tail; preserve qualified name
RuntimeFrame.FileYESSame pattern — module paths can carry the same suffix
RuntimeFrame.LineNONumeric, not an identifier
RuntimeFrame.IsEntryPointNOBoolean evidence flag
RuntimeFrame.IsVulnerableFunctionNOBoolean evidence flag
RuntimeFrame.ConfidenceNONumeric, not an identifier
RuntimeFrame.OrderNOSynthetic ordering, not from probe
RuntimeTrace.ContainerIdNOContainer identifiers are tenant-scoped, used for containerCount
RuntimeTrace.ContainerNameNOAccepted on the wire, not persisted in v1 durable tables
RuntimeTrace.ArtifactDigestNOBuild artifact digest is a public, immutable identifier
RuntimeTrace.ComponentPurlNOSBOM component identifier; required for correlation
request metadataNOAccepted on the wire, not persisted in v1 durable tables

What is persisted vs dropped

StageStorageAddress tokens?
Wire (POST body)(in flight only)yes — agents send them
Privacy filter(in memory)stripped here
Aggregator(in memory)already gone
runtime_tracesPostgres JSONBredacted frames only; container_id is persisted, containerName and request metadata are dropped
runtime_trace_aggregatesPostgres rowsredacted symbol is bucket key
runtime_scoresPostgres rowsno symbols stored
runtime_timeline_eventsPostgres JSONBredacted-only payloads

The wire input is the only place address tokens exist. Everything downstream sees redacted-only data. This is asserted by the RuntimeSymbolRedactorTests suite (idempotency: Redact(Redact(x)) == Redact(x)).

Properties

Architecture-lead sign-off

Rationale

Why end-anchored? Mid-string hex literals (foo_0xdeadbeef) are legitimate symbol fragments in some runtimes (Go method names with hash suffixes, JIT-emitted symbols). End-anchoring keeps the redaction conservative.

Why strip before persistence and before aggregation? Two correctness requirements:

  1. The aggregator’s bucket key is the redacted vulnerable function symbol. Without redaction, two processes with different ASLR layouts bucket separately. This is the integration-test contract.
  2. Persisting addresses would leak per-process layout into long-term storage, which we do not want.

Doing it at any later stage is too late.

Why not strip more aggressively? Any of the preserved fields could in principle leak info, but each carries operational evidence value that operators rely on:

Stripping any of these would erode evidence value without meaningful privacy gain.

Why no full hash? A common alternative is to hash the full symbol including the address. We rejected this because:

  1. Hashes are still unique per address, so two ASLR-shifted processes still bucket separately. Doesn’t fix the aggregation problem.
  2. Operators reading the aggregate UI need to recognize the symbol name. Hashing destroys readability.

Alternatives considered

Consequences

Test coverage

References

Sign-off Window

Accepted on 2026-04-29 by Architecture lead. The 14-day default-accept window from sprint 024 (deadline 2026-05-13) closed early via explicit approval rather than the timeout path. Original window text retained below for history.

Following the project’s RFC default-accept process: if this ADR remains Proposed with no objection logged in this document by 2026-05-13 (14 days from sprint 024 commit), it is automatically promoted to Accepted. Architecture-lead may flip status earlier on review. Objections should be added under “Open Issues” below with name + date.

Open Issues

No objections logged during the sign-off window.