Skip to content

Fix StackOverflow in RELEASE_PATTERN by reducing regex branch nodes#7231

Merged
Jenson3210 merged 9 commits intomainfrom
fix-latestrelease-nfe-6plus-parts
Apr 1, 2026
Merged

Fix StackOverflow in RELEASE_PATTERN by reducing regex branch nodes#7231
Jenson3210 merged 9 commits intomainfrom
fix-latestrelease-nfe-6plus-parts

Conversation

@Jenson3210
Copy link
Copy Markdown
Contributor

@Jenson3210 Jenson3210 commented Apr 1, 2026

Summary

  • Replace 9 optional capturing groups with 5 capturing groups + a non-capturing (?:\.\d+)* consumer
  • Parse version parts directly from strings in compare() instead of relying on regex groups
  • Use a named (?<qualifier>...) group for qualifier extraction

Problem

Solution

Keep 5 explicit capturing groups (needed by XRange) and add a single non-capturing (?:\.\d+)* to consume any additional numeric parts. This creates a simple loop node instead of multiple Branch nodes, eliminating the stack overflow risk. The compare() method parses version parts directly from the string rather than relying on regex group access, so it handles any number of parts.

Test plan

  • All LatestReleaseTest tests pass (including 6+ part versions)

  • All semver tests pass

  • RemoveRedundantDependencyVersions tests pass

  • Verified no StackOverflow with 256k stack size

  • Fixes moderneinc/customer-requests#2142

Reproduces NumberFormatException in LatestRelease.compare() when version
strings have 6+ numeric parts, as the loop exceeds the 5 regex groups.
LatestRelease.compare() iterates based on countVersionParts(), which can
return >5 for versions with many numeric segments (e.g. 1.2.3.4.5.6).
However, RELEASE_PATTERN only has 5 numeric capturing groups, so group(6)
returns the qualifier suffix (e.g. ".6") causing NumberFormatException in
Long.parseLong(). Cap the loop at 5 to stay within the regex groups; any
remaining parts are handled by the string comparison fallback.

Fixes moderneinc/customer-requests#2121
Instead of using RELEASE_PATTERN's 5 numeric capturing groups, parse
numeric version parts and qualifier suffix directly from the version
string. This supports versions with any number of numeric parts (e.g.
1.2.3.4.5.6.7) and correctly extracts qualifiers like -RC1 even when
they follow more than 5 numeric segments.
Verify that comparing versions with different numbers of numeric parts
works correctly when either side has more than 5 parts (e.g. 3 vs 6,
5 vs 6), including the case where trailing zeros make versions equal.
RELEASE_PATTERN only had 5 numeric capturing groups, which caused
isValid() to reject 6+ part versions by misclassifying the 6th numeric
part as a qualifier suffix. Extend to 9 groups and use a named group
for the qualifier so checkVersion() is independent of the group count.
…groups

The extractVersionParts/extractQualifierSuffix helpers are no longer needed
since RELEASE_PATTERN has enough numeric groups. Restore the original
compare() logic with just the cap removed and group(6) replaced by the
named "qualifier" group.
.*?$ is non-greedy but still captures to end of string, so it works
correctly with both .matches() and .find() callers.
…erflow

The 9 optional capturing groups in RELEASE_PATTERN created too many
Branch nodes in Java's regex engine, causing StackOverflowError.
Instead, keep 5 capturing groups (for XRange compatibility) and use a
non-capturing `(?:\.\d+)*` to consume any additional numeric parts.
The compare() method parses version parts directly from the string
rather than relying on regex groups.

Fixes moderneinc/customer-requests#2142
Copy link
Copy Markdown
Member

@timtebeek timtebeek left a comment

Choose a reason for hiding this comment

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

Approved already; still see some conflicts.

@github-project-automation github-project-automation bot moved this from In Progress to Ready to Review in OpenRewrite Apr 1, 2026
@Jenson3210
Copy link
Copy Markdown
Contributor Author

Verification against customer-requests#2142

I tested this PR's approach by shadowing VersionComparator and LatestRelease in rewrite-java-security with the same iterative parsing changes, using a reproduction test with a POM similar to the customer's (many dependencies with advisories: jackson-databind, log4j, tomcat, snakeyaml, etc.).

Results

Stack Size Without fix With fix
256KB Fails (Pattern$Branch.match) Fails (AdaptiveRadixTree.InternalNode.insert)
272KB Fails (Pattern$Branch.match) Fails (AdaptiveRadixTree.InternalNode.insert)
280KB Fails (Pattern$Branch.match) Passes
288KB Passes Passes

Assessment

The fix correctly eliminates the regex Branch node recursion as a contributor to the stack overflow. At 280KB the fix makes the difference between pass and fail.

At very small stack sizes (≤272KB), a second recursive component becomes the bottleneck: AdaptiveRadixTree.InternalNode.insert() (used by JavaTypeCache). This is a recursive trie insertion that overflows independently of the regex fix. If customers hit this after the PR lands, that would be a separate issue to address.

For the customer's actual environment (Moderne platform workers, likely ≥512KB stack), this fix should be sufficient.

@timtebeek
Copy link
Copy Markdown
Member

Ok to merge then I think, right?

@Jenson3210 Jenson3210 merged commit 03fe804 into main Apr 1, 2026
1 check passed
@Jenson3210 Jenson3210 deleted the fix-latestrelease-nfe-6plus-parts branch April 1, 2026 18:08
@github-project-automation github-project-automation bot moved this from Ready to Review to Done in OpenRewrite Apr 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants