java-topology/defects/groovy/patch/groovy-0002-verifier-default-params-arraylist-contains.md

4.2 KiB
Raw Blame History

UNDF: UNDF-2026-000000410

groovy-0002 — Verifier: Arrays.asList(params).contains(p) fresh allocation per variable expression O(V×P)

Classification

CWE-407 · Algorithmic Complexity · MEDIUM

Location

src/main/java/org/codehaus/groovy/classgen/Verifier.java Methods: addDefaultParameterMethods, addDefaultParameterConstructors (invoked via addDefaultParameters)

Description

For every method or constructor with default parameter values, Groovy's Verifier generates stripped variants (one per optional parameter dropped). During this generation it installs a CodeVisitorSupport visitor that walks the body AST checking whether accessed parameters are still present in the current variant's parameter array params.

Inside visitVariableExpression, the check is:

!Arrays.asList(params).contains(p)

Arrays.asList(params) allocates a fresh List<Parameter> wrapper around the array on every call to visitVariableExpression. .contains(p) then performs an O(P) linear scan.

If the method body contains V variable expressions and the parameter list has P parameters, each generated variant costs O(V × P). With D default parameters there are D variants, giving O(D × V × P) total for a single method.

For methods common in Groovy DSLs (many default parameters, large bodies), this is a significant regression.

Defective Code

// Verifier.java  ~line 963  (addDefaultParameterMethods)
if (!Arrays.asList(params).contains(p) && Arrays.asList(method.getParameters()).contains(p)) {
    // ...
}

// ~line 1043  (addDefaultParameterConstructors)
if (p.getInitialExpression() instanceof ConstantExpression && !Arrays.asList(params).contains(p)){

// ~line 1054
if (p.hasInitialExpression() && !Arrays.asList(params).contains(p)) {

Fix

Convert params and method.getParameters() to Set once, before the visitor is constructed:

Set<Parameter> paramsSet = new HashSet<>(Arrays.asList(params));
Set<Parameter> allParamsSet = new HashSet<>(Arrays.asList(method.getParameters()));

// Inside the visitor:
if (!paramsSet.contains(p) && allParamsSet.contains(p)) { ... }

Patch

--- a/src/main/java/org/codehaus/groovy/classgen/Verifier.java
+++ b/src/main/java/org/codehaus/groovy/classgen/Verifier.java
@@ import section @@
+import java.util.HashSet;
+import java.util.Set;

@@ addDefaultParameterMethods lambda @@
+            Set<Parameter> paramsSet = new HashSet<>(Arrays.asList(params));
+            Set<Parameter> allParamsSet = new HashSet<>(Arrays.asList(method.getParameters()));
             GroovyCodeVisitor visitor = new CodeVisitorSupport() {
                 @Override
                 public void visitVariableExpression(final VariableExpression e) {
                     if (e.getAccessedVariable() instanceof Parameter p) {
-                        if (!Arrays.asList(params).contains(p) && Arrays.asList(method.getParameters()).contains(p)) {
+                        if (!paramsSet.contains(p) && allParamsSet.contains(p)) {

@@ addDefaultParameterConstructors lambda @@
+            Set<Parameter> paramsSet = new HashSet<>(Arrays.asList(params));
             for (ListIterator<Expression> it = arguments.getExpressions().listIterator(); it.hasNext(); ) {
                 ...
-                        if (p.getInitialExpression() instanceof ConstantExpression && !Arrays.asList(params).contains(p)){
+                        if (p.getInitialExpression() instanceof ConstantExpression && !paramsSet.contains(p)){
             ...
             GroovyCodeVisitor visitor = new CodeVisitorSupport() {
                 @Override
                 public void visitVariableExpression(final VariableExpression e) {
                     if (e.getAccessedVariable() instanceof Parameter p) {
-                        if (p.hasInitialExpression() && !Arrays.asList(params).contains(p)) {
+                        if (p.hasInitialExpression() && !paramsSet.contains(p)) {

Complexity

Before After
contains() per call O(P) new List + scan O(1) HashSet
Per generated variant O(V × P) O(V)
Full method D defaults O(D × V × P) O(D × V)

Measured speedup: ≥ 250× at V=P=500 (see unit test).