Skip to content

fix: consult live message tree for wrapped-attribute reads when dirty - #296

Open
detail-app[bot] wants to merge 3 commits into
masterfrom
detail/bug-fix/fix-consult-live-message-tree-for-wrapped-attribut-0bf564
Open

detail-app[bot] wants to merge 3 commits into
masterfrom
detail/bug-fix/fix-consult-live-message-tree-for-wrapped-attribut-0bf564

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 16, 2026

Copy link
Copy Markdown

Detail bug report: View on Detail

Bug

For Datadog logs with a JSON-wrapped message (GCP/Cloud Run shape), wrapped-attribute reads go through DatadogLog.unwrappedAttribute, which serves a one-shot message_flat snapshot built from the original message on first read. Wrapped writes (setWrapped/deleteWrapped) mutate the live message_tree and flip message_dirty, but never refresh message_flat.

The policy engine re-reads a field via the accessor before each transform write (remove → redact → rename → add), so for a field edited by an earlier rule, a later rule reads the stale pre-transform snapshot and overwrites the prior edit. The realistic impact: a redaction policy with more than one regex redact rule on the same wrapped path silently loses every earlier rule's substitution — only the last rule's pattern is reliably scrubbed, and the PII the earlier rules targeted reappears in the forwarded record.

Introduced in PR #203 (f40cc7e), which added the edit primitives and the one-shot read cache but only wired the whole-body-replace path through clearWrappedRewrite; the log_attribute/resource_attribute/scope_attribute branches route to setWrapped/deleteWrapped and never invalidate the read side.

Fixes ENG-697

Fix

In DatadogLog.unwrappedAttribute (src/signals/datadog/log.zig), consult the live message_tree first when message_dirty is true, using the existing navigateParent + obj.getPtr primitives:

  • Present string leaf → return the live (possibly edited) value.
  • Missing leaf → return null (a deleteWrapped removal reads as absent, not the stale value — this is the primitive backing callExists, so remove→add and remove→redact now compose correctly).
  • Non-string leaf → return null (matches the flat, which stores strings only).
  • When the tree can't resolve the path (navigateParent fails, or the parent isn't an object — e.g. an array-of-objects the flattener reaches but navigateParent won't descend into, or no edit happened yet) → fall through to message_flat, which stays authoritative for never-edited and array-flattened paths.

This keeps reads and writes consistent within a single transform pass. setWrapped/deleteWrapped, ensureUnwrapped, clearWrappedRewrite, bodyForMatch, findExtraString, and the whole-body and top-level set/delete paths are unchanged. bodyForMatch is unaffected because matchers run in Phase 1, before any transform flips message_dirty.

Testing

Added 4 inline regression tests (the repo keeps tests inline in the same file):

  • log.zig: unwrappedAttribute observes a setWrapped edit on the same path; unwrappedAttribute returns null after deleteWrapped removes the leaf; unwrappedAttribute still resolves array-of-objects paths after an unrelated edit (guards against an over-eager fix regressing the array-of-objects flatten path).
  • logs.zig: processLogs - two regex redacts on the same wrapped path compose — the primary leak end-to-end through the policy engine + re-serialization.

Verified the tests are genuine regression guards by disabling the fix's if (self.message_dirty) branch: exactly the 4 new tests fail (and nothing else), with the failure detail showing the stale alice@example.com read after rule 1's edit. Restoring the fix flips them back to pass.

Routine checks all pass: zig build, zig build test --summary all (523 pass, 1 skip, 0 fail), zig fmt --check src/ build.zig, and ziglint.

Also verified three un-versioned end-to-end scenarios live via temporary inline tests (since removed): three composing regex redacts on the same path (aliceALICE_R, exampleEXAMPLE_R, \.-ALICE_R@EXAMPLE_R-com); a regex redact followed by a whole-value redact on the same path (whole-value X wins, all prior tokens gone); and remove then regex-redact on the same path (the later redact reads callExists→null and no-ops, no PII leak). The datadog-log-bench benchmark also still completes without error, including the wrapped/wrapped_rewrite path that exercises setWrapped/deleteWrapped.


Automatic Fixes PRs can be configured here.

Comment thread src/signals/datadog/log.zig
@macroscopeapp

macroscopeapp Bot commented Sep 16, 2026

Copy link
Copy Markdown

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This focused fix changes production Datadog redaction and attribute-resolution behavior, including whether previously scrubbed log data can be forwarded. Because the affected path handles potentially sensitive data, human review is warranted despite the added regression coverage.

You can add or adjust custom eligibility rules. Learn more.

…nwrappedAttribute

When the tree parent is an object and the leaf value is a non-string
(e.g. an array), the previous code returned null immediately.
flattenValue stores the first string element of such arrays under the
parent key in message_flat, so the null short-circuit caused a later
redact to see no value and leave the secret in the forwarded record.

Change else => return null to else => {} so the non-string-leaf case
falls through to message_flat, which stays authoritative for
never-edited and array-flattened paths.

Add a regression test covering an array containing a string after an
unrelated sibling edit dirties the tree.
Comment thread src/signals/datadog/log.zig Outdated
When message_dirty is set and an ancestor key was deleted from the
message_tree via deleteWrapped, unwrappedAttribute previously fell
through to the stale message_flat snapshot, surfacing data that a
policy had removed.

Replace the single navigateParent call with a manual per-segment walk:
- object ancestor with key absent → return null (deleted ancestor)
- non-object ancestor (array) → defer to message_flat (array-flattened path)

Add a regression test covering deleteWrapped on an ancestor object node
followed by a read through that ancestor.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant