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 commented Aug 11, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign pedjak for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found 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 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
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, with a
  warning that names the misconfigured CSV. 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.

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 9d4f913 to 65fb2c9 Compare August 12, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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