Skip to content

Fix infinite loop on cyclic CSV replacement chains - #3891

Open
haklein wants to merge 1 commit into
operator-framework:masterfrom
haklein:OCPBUGS-23954-the-olm-operator-pod-jumps-to-100-cpu-utilization-if-pending-csv-replaces-itself
Open

Fix infinite loop on cyclic CSV replacement chains#3891
haklein wants to merge 1 commit into
operator-framework:masterfrom
haklein:OCPBUGS-23954-the-olm-operator-pod-jumps-to-100-cpu-utilization-if-pending-csv-replaces-itself

Conversation

@haklein

@haklein haklein commented Aug 11, 2026

Copy link
Copy Markdown

Description of the change:

Guard the CSV replace finder (pkg/lib/csv/replace_finder.go) against self-references and cycles:

  • IsBeingReplaced skips the input CSV.
  • IsReplacing returns nil for a self-referencing spec.replaces and logs a warning.
  • GetFinalCSVInReplacing tracks visited CSVs and stops on the first repeat.

Motivation for the change:

A CSV with spec.replaces equal to its own name sends GetFinalCSVInReplacing into an infinite loop. The CSV watch notification goroutine spins at 100% CPU and writes one log line per iteration. Deletion of the CSV does not stop the loop, only a pod restart does. The same walk loops on any cycle across two or more CSVs. A self-replacing CSV also deadlocks in Pending, because the install gate waits for the previous CSV (itself) to reach Replacing. After this change, a self-replacing CSV installs normally.

Fixes OCPBUGS-23954.

Architectural changes:

None.

Testing remarks:

  • Regression tests in pkg/lib/csv/replace_finder_test.go cover the self-loop, a two-CSV cycle, and a linear chain. The looping calls hang without the fix.
  • Reproduced the loop on kind with --writePackageServerStatusName set and a labeled self-replacing CSV: CPU 137–147%. With the fix: 0.1%, and the CSV reaches Succeeded. Details in OCPBUGS-23954.
  • Full local runs of unit, sanity, e2e (176 passed, 0 failed) and [FLAKE] suite (14 passed, 0 failed).

Reviewer Checklist

  • Implementation matches the proposed design, or proposal is updated to match implementation
  • Sufficient unit test coverage
  • Sufficient end-to-end test coverage
  • Bug fixes are accompanied by regression test(s)
  • e2e tests and flake fixes are accompanied evidence of flake testing, e.g. executing the test 100(0) times
  • tech debt/todo is accompanied by issue link(s) in comments in the surrounding code
  • Tests are comprehensible, e.g. Ginkgo DSL is being used appropriately
  • Docs updated or added to /doc
  • Commit messages sensible and descriptive
  • Tests marked as [FLAKE] are truly flaky and have an issue
  • Code is properly formatted

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of CSV replacement relationships by ignoring self-replacements.
    • Replacement chains now stop safely when circular references are detected, preventing endless traversal.
    • Added warnings when replacement cycles are encountered.
  • Tests

    • Added coverage for self-replacements, circular replacement chains, and normal multi-step replacement paths.

@openshift-ci openshift-ci Bot added the needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. label Aug 11, 2026
@openshift-ci

openshift-ci Bot commented Aug 11, 2026

Copy link
Copy Markdown

Hi @haklein. Thanks for your PR.

I'm waiting for a operator-framework member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The CSV replacement finder now ignores self-references and stops cyclic replacement-chain traversal. Tests cover self-replacement, two-CSV cycles, cycle termination, and linear chains.

Changes

CSV replacement safety

Layer / File(s) Summary
Replacement validation and cycle handling
pkg/lib/csv/replace_finder.go, pkg/lib/csv/replace_finder_test.go
The finder excludes self-replacements, logs warnings for invalid self-references and cycles, and tracks visited CSV names. Tests cover cyclic and linear replacement chains.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: preventing infinite loops caused by cyclic CSV replacement chains.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

Error: can't load config: can't unmarshal config by viper (flags, file): 1 error(s) decoding:

  • 'output.formats' expected a map, got 'string'
    The command is terminated due to an error: can't load config: can't unmarshal config by viper (flags, file): 1 error(s) decoding:

  • 'output.formats' expected a map, got 'string'


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/lib/csv/replace_finder_test.go (1)

46-71: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Bound each replacement-chain traversal.

Lines 51, 58, and 67 call GetFinalCSVInReplacing synchronously. If a future change restores the infinite loop, the package stays blocked until the external go test timeout expires. Run each call in a goroutine and fail with a short test-local timeout.

Proposed test helper
 import (
 	"testing"
+	"time"
 )
 
+func getFinalWithin(t *testing.T, f func() *v1alpha1.ClusterServiceVersion) *v1alpha1.ClusterServiceVersion {
+	t.Helper()
+	result := make(chan *v1alpha1.ClusterServiceVersion, 1)
+	go func() { result <- f() }()
+
+	select {
+	case csv := <-result:
+		return csv
+	case <-time.After(time.Second):
+		t.Fatal("replacement-chain traversal did not terminate")
+		return nil
+	}
+}
+
-	if got := finder.GetFinalCSVInReplacing(self, setOf(self)); got != nil {
+	if got := getFinalWithin(t, func() *v1alpha1.ClusterServiceVersion {
+		return finder.GetFinalCSVInReplacing(self, setOf(self))
+	}); got != nil {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/lib/csv/replace_finder_test.go` around lines 46 - 71, Update
TestGetFinalCSVInReplacing to execute each GetFinalCSVInReplacing call in a
goroutine and enforce a short test-local timeout, failing the test if traversal
does not return promptly. Apply this to the self-loop, two-CSV cycle, and
linear-chain cases while preserving their existing result assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/lib/csv/replace_finder_test.go`:
- Around line 46-71: Update TestGetFinalCSVInReplacing to execute each
GetFinalCSVInReplacing call in a goroutine and enforce a short test-local
timeout, failing the test if traversal does not return promptly. Apply this to
the self-loop, two-CSV cycle, and linear-chain cases while preserving their
existing result assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0317cd24-a29c-45f8-88a1-08ebed4e536a

📥 Commits

Reviewing files that changed from the base of the PR and between de656a4 and 9d4f913.

📒 Files selected for processing (2)
  • pkg/lib/csv/replace_finder.go
  • pkg/lib/csv/replace_finder_test.go

@tmshort

tmshort commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

/ok-to-test

@openshift-ci openshift-ci Bot added ok-to-test Indicates a non-member PR verified by an org member that is safe to test. and removed needs-ok-to-test Indicates a PR that requires an org member to verify it is safe to test. labels Aug 11, 2026
@haklein
haklein force-pushed the OCPBUGS-23954-the-olm-operator-pod-jumps-to-100-cpu-utilization-if-pending-csv-replaces-itself branch from 9d4f913 to 65fb2c9 Compare August 12, 2026 06:18
Comment thread pkg/lib/csv/replace_finder.go Outdated
Comment thread pkg/lib/csv/replace_finder.go Outdated
Comment thread pkg/lib/csv/replace_finder.go
@tmshort

tmshort commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Worth noting a migration concern: before this change, IsBeingReplaced matched a self-referencing CSV against itself and returned it (non-nil), causing checkReplacementsAndUpdateStatus to move it to CSVPhaseReplacing. Any cluster already running with such a CSV in CSVPhaseReplacing will see different behavior after upgrading OLM:

  • isReplacing now returns nil (new self-ref guard), so the leaf-check at operator.go:2534 no longer fires.
  • isBeingReplaced also returns nil, so next is nil.
  • Without UnsafeFailForward: the CSV enters a perpetual syncError = "marked as replacement, but no replacement CSV found" loop with no recovery path.
  • With UnsafeFailForward: operator.go:2543 calls next.GetNamespace() on nil → panic.

The nil-before-FailForward dereference at line 2543 is a pre-existing latent bug, but this change makes it reachable in practice. A nil guard there (and/or a note in the commit message about the migration impact) would harden the fix.

@haklein

haklein commented Aug 14, 2026

Copy link
Copy Markdown
Author

Worth noting a migration concern: before this change, IsBeingReplaced matched a self-referencing CSV against itself and returned it (non-nil), causing checkReplacementsAndUpdateStatus to move it to CSVPhaseReplacing. Any cluster already running with such a CSV in CSVPhaseReplacing will see different behavior after upgrading OLM

Thanks a lot for catching this. This reproduced exactly as you've described:

  1. Deploy stock OLM v0.26.0 (pre-fix)
  2. Install a CSV and wait for Succeeded
  3. Patch spec.replaces to the CSV's own name. OLM moves the CSV to Replacing
  4. Upgrade the olm-operator image to the nil guarded one. With UnsafeFailForward on the OperatorGroup, the operator panics at operator.go:2543 on the first sync and enters CrashLoopBackOff. The rollout never completes, so the OLM upgrade hangs.

Per your recommendation I would add:

  • a nil guard at the fail-forward branch: if next != nil && ... A nil next falls through to "no replacement CSV found" error, as with the default strategy.
  • Two regression cases in TestTransitionCSVFailForward: a self-referencing CSV in Replacing, with fail-forward on and off. The fail-forward case panics without the guard.

with nil guard the upgraded operator stays healthy on the test setup. CSV stays in Replacing with the "no replacement CSV found" sync error, which can be resolved by deleting the CSV. I will add a note of this migration behavior in the commit message.

@tmshort Shall I squash before merging the new commits?

@tmshort

tmshort commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

@haklein

@tmshort Shall I squash before merging the new commits?

This is small enough it doesn't matter; AFAICT github will squash upon merge

EDIT: Although I'd be OK with a squash

A CSV with spec.replaces equal to its own name sends the replacement
chain walk in GetFinalCSVInReplacing into an infinite loop: the CSV
watch notification goroutine spins at 100% CPU and writes one Info log
line per iteration. Deletion of the CSV does not recover the operator,
only a pod restart does. The same walk loops on any cycle that spans
two or more CSVs.

Guard the finder at all three points:
- IsBeingReplaced skips the input CSV, a CSV cannot replace itself.
- IsReplacing returns nil for a self-referencing spec.replaces. A
  self-replacing CSV now installs normally instead of deadlocking in
  Pending, where it waited for itself to reach the Replacing phase.
- GetFinalCSVInReplacing tracks visited CSVs and stops on the first
  repeat, which also covers multi-CSV cycles.

All three guards log at Debug: the controller handles the condition
silently, so warning-level logging would only repeat an identical line
on every reconcile of a permanently misconfigured CSV.

Migration note: a CSV left in CSVPhaseReplacing by a previous OLM
version whose spec.replaces names itself now resolves to no previous
and no next CSV. The fail-forward branch of the Replacing phase guards
against the nil next CSV (previously an unguarded dereference, a latent
panic this change made reachable). Both upgrade strategies converge on
the existing "marked as replacement, but no replacement CSV found"
sync error. Recovery is deletion of the CSV.

Fixes OCPBUGS-23954.

Co-Authored-By: Claude <noreply@anthropic.com>
@haklein
haklein force-pushed the OCPBUGS-23954-the-olm-operator-pod-jumps-to-100-cpu-utilization-if-pending-csv-replaces-itself branch from 65fb2c9 to 6e31ea7 Compare August 17, 2026 15:00
@haklein

haklein commented Aug 17, 2026

Copy link
Copy Markdown
Author

@tmshort force pushed the squashed commit with the changes as requested, br Hari

@tmshort

tmshort commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

/approve

@openshift-ci

openshift-ci Bot commented Aug 17, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: tmshort

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files. ok-to-test Indicates a non-member PR verified by an org member that is safe to test.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants