Skip to content

Fix secret nesting amplification for write-only properties on refresh - #3113

Closed
yukimotochern wants to merge 1 commit into
pulumi:masterfrom
yukimotochern:fix-write-only-secret-nesting
Closed

Fix secret nesting amplification for write-only properties on refresh#3113
yukimotochern wants to merge 1 commit into
pulumi:masterfrom
yukimotochern:fix-write-only-secret-nesting

Conversation

@yukimotochern

Copy link
Copy Markdown

What

Refreshing a resource with a secret value under a write-only path (e.g. AWS::Synthetics::Canary runConfig/environmentVariables) doubles the value's secret wrapper depth on every refresh: 1 → 2 → 4 → 8 → 16 → … Each wrapper roughly doubles the JSON-escaped payload in the encrypted state, so a 36-character secret grew to ~438 KiB per occurrence after four refreshes on a production stack, and eventually pulumi refresh was OOM-killed.

Two defects combine:

  1. preserveSecretWrapper re-wraps values that are already secret. Write-only values restored from old inputs by AddWriteOnlyOutputFallbacks are already secret, so each refresh stacks the old wrapper depth on top.
  2. addWriteOnlyFallbacks aliases the old-input subtree into the new output state instead of copying, so the re-wrap also writes through into the parsed old inputs and flows into the new __inputs checkpoint — compounding across refreshes.

Fix

  • preserveSecretWrapper never wraps an already-secret value.
  • addWriteOnlyFallbacks clones restored values instead of aliasing old inputs.
  • CollapseConsecutiveSecrets collapses Secret(Secret(x))Secret(x) at the checkpoint read/write boundary (ParseCheckpointObject / CheckpointPropertyMap), so states already poisoned by earlier versions heal on their next refresh.

Tests

  • Refresh-loop regression test asserting wrapper depth stays exactly 1 in outputs and __inputs over 20 simulated refreshes (fails with depth 2 on master at iteration 0).
  • Healing test: a state with a 16-deep nested secret collapses to depth 1 after one refresh.
  • Unit tests for the collapse at the checkpoint boundary, the no-nest behavior of PreserveSecretWrappers, and the no-alias behavior of AddWriteOnlyOutputFallbacks.

🤖 Generated with Claude Code

Refreshing a resource that has a secret value under a write-only path
(e.g. AWS::Synthetics::Canary runConfig/environmentVariables) doubled the
value's secret wrapper depth on every refresh: 1, 2, 4, 8, 16, ... Each
extra wrapper roughly doubles the JSON-escaped payload in the encrypted
state, so a 36-character secret grew to ~438 KiB per occurrence after
four refreshes and kept doubling, eventually OOM-killing refresh.

Two defects combined to cause this:

1. preserveSecretWrapper re-wrapped values that were already secret.
   Write-only values restored from old inputs by
   AddWriteOnlyOutputFallbacks are already secret, so each refresh added
   the old input's wrapper depth on top of the existing wrappers.
2. addWriteOnlyFallbacks aliased the old-input subtree into the new
   output state instead of copying it, so the re-wrap in (1) also wrote
   through into the parsed old inputs, which then flowed into the new
   __inputs checkpoint - making the growth compound across refreshes.

Fix preserveSecretWrapper to never wrap an already-secret value, clone
write-only fallback values, and collapse consecutive secret wrappers at
the checkpoint read/write boundary (CollapseConsecutiveSecrets) so that
states already poisoned by earlier versions heal on their next refresh
instead of doubling further.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

PR is now waiting for a maintainer to run the acceptance tests.
Note for the maintainer: To run the acceptance tests, please comment /run-acceptance-tests on the PR

corymhall added a commit that referenced this pull request Aug 3, 2026
## Problem

Refreshing a resource with a secret under a write-only path could double
its secret-wrapper depth on every refresh: `1 → 2 → 4 → 8`. The refresh
path reapplied secret markers to values that were already secret, and
restored subtrees aliased the old inputs, allowing nested values to flow
into the next `__inputs` checkpoint.

## Solution

- Restore write-only values into raw CloudControl state before
reapplying secret markers, treating secret-wrapped checkpoint ancestors
as transparent for path lookup.
- Make secret preservation idempotent and clone restored write-only
subtrees.
- Collapse consecutive wrappers only within write-only fallbacks,
healing affected state from `4 → 1` without rewriting unrelated
checkpoint values.

Based on the investigation and initial fix in #3113 by @yukimotochern.
This version moves healing from the global checkpoint boundary to the
write-only fallback path.

## Validation

- Added repeated-refresh and targeted-healing tests based on Synthetics
Canary environment variables.
- Added direct provider `Read` coverage for a secret parent containing a
nested write-only value, plus array fallback coverage.
- Ran `mise exec -- make lint` and `mise exec -- make
test_provider_fast`.

---------

Co-authored-by: cyan <cyan.chen@nearme.jp>
@corymhall

Copy link
Copy Markdown
Member

@yukimotochern thanks for the PR! I modified your solutions slightly and merged it in #3115. I also kicked of a release so a new version should be out soon.

@corymhall corymhall closed this Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants