This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch 8.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git


The following commit(s) were added to refs/heads/8.0.x by this push:
     new 33964e780e Make the spring security acl jar reproducible
33964e780e is described below

commit 33964e780e1ec44856033e2c42773af20c2e6018
Author: James Daugherty <[email protected]>
AuthorDate: Sat Jul 11 13:20:43 2026 -0400

    Make the spring security acl jar reproducible
---
 .../transform/DetachedCriteriaTransformer.java     |  18 ++++
 .../transform/WhereQueryClosureCaptureSpec.groovy  | 103 +++++++++++++++++++++
 2 files changed, 121 insertions(+)

diff --git 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
index 78ec7c22b6..728508a605 100644
--- 
a/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
+++ 
b/grails-datamapping-core/src/main/groovy/org/grails/datastore/gorm/query/transform/DetachedCriteriaTransformer.java
@@ -68,6 +68,7 @@ import org.codehaus.groovy.ast.stmt.Statement;
 import org.codehaus.groovy.ast.stmt.SwitchStatement;
 import org.codehaus.groovy.ast.stmt.TryCatchStatement;
 import org.codehaus.groovy.ast.stmt.WhileStatement;
+import org.codehaus.groovy.control.ErrorCollector;
 import org.codehaus.groovy.control.SourceUnit;
 import org.codehaus.groovy.control.messages.LocatedMessage;
 import org.codehaus.groovy.syntax.Token;
@@ -101,6 +102,7 @@ public class DetachedCriteriaTransformer extends 
ClassCodeVisitorSupport {
     public static final ConstantExpression WHERE_LAZY = new 
ConstantExpression("whereLazy");
 
     private SourceUnit sourceUnit;
+    private boolean appliedWhereTransform;
     private static final Set<String> CANDIDATE_METHODS = newSet("where", 
"whereLazy", "whereAny", "findAll", "find");
 
     private static final Set<String> SUPPORTED_FUNCTIONS = newSet(
@@ -174,7 +176,22 @@ public class DetachedCriteriaTransformer extends 
ClassCodeVisitorSupport {
     public void visitClass(ClassNode node) {
         try {
             this.currentClassNode = node;
+            this.appliedWhereTransform = false;
             super.visitClass(node);
+            if (appliedWhereTransform) {
+                // The nested closures generated for association criteria 
share the outer
+                // where-closure's VariableScope, which still references every 
local variable the
+                // original closure used. Which locals a generated closure 
captures then depends on
+                // whether a later transformation happens to recompute the 
scopes, making the
+                // compiled closure constructors nondeterministic. Recompute 
the scopes now so each
+                // generated closure captures exactly the variables it 
references.
+                // A throwaway ErrorCollector is used because the code has 
already been scope-checked
+                // once and re-running the visitor must not report duplicate 
errors (see
+                // AbstractMethodDecoratingTransformation for the same 
pattern).
+                SourceUnit dummySourceUnit = new SourceUnit("dummy", "dummy", 
sourceUnit.getConfiguration(),
+                        sourceUnit.getClassLoader(), new 
ErrorCollector(sourceUnit.getConfiguration()));
+                AstUtils.processVariableScopes(dummySourceUnit, node, null);
+            }
         } catch (Exception e) {
             logTransformationError(node, e);
         } finally {
@@ -627,6 +644,7 @@ public class DetachedCriteriaTransformer extends 
ClassCodeVisitorSupport {
             if (!newCode.getStatements().isEmpty()) {
                 closureExpression.putNodeMetaData(TRANSFORMED_MARKER, 
Boolean.TRUE);
                 closureExpression.setCode(newCode);
+                appliedWhereTransform = true;
             }
         } finally {
             this.currentClassNode = previousClassNode;
diff --git 
a/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/transform/WhereQueryClosureCaptureSpec.groovy
 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/transform/WhereQueryClosureCaptureSpec.groovy
new file mode 100644
index 0000000000..f57617b856
--- /dev/null
+++ 
b/grails-datamapping-core/src/test/groovy/org/grails/datastore/gorm/query/transform/WhereQueryClosureCaptureSpec.groovy
@@ -0,0 +1,103 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+package org.grails.datastore.gorm.query.transform
+
+import java.lang.reflect.Constructor
+
+import groovy.lang.Reference
+import spock.lang.Specification
+
+/**
+ * The where-query transform generates nested closures for association 
criteria. Those closures
+ * must capture exactly the local variables they reference; historically they 
shared the outer
+ * where-closure's VariableScope, so their captures depended on whether a 
later transformation
+ * happened to recompute the scopes, producing nondeterministic bytecode 
between builds.
+ */
+class WhereQueryClosureCaptureSpec extends Specification {
+
+    private static final String SERVICE_SOURCE = '''
+import grails.gorm.DetachedCriteria
+import grails.gorm.annotation.Entity
+
+class BookQueryService {
+    protected DetachedCriteria<Book> 
findQueryByBookIdAndAuthorName(Serializable bookId, String authorName) {
+        Book.where {
+            bookId == bookId && author.name == authorName
+        }
+    }
+}
+
+@Entity
+class Book {
+    Long bookId
+    Author author
+}
+
+@Entity
+class Author {
+    String name
+}
+'''
+
+    void "association criteria closures capture only the variables they 
reference"() {
+        given: 'a where query with an association criterion referencing one of 
two parameters'
+        GroovyClassLoader gcl = new GroovyClassLoader()
+        gcl.parseClass(SERVICE_SOURCE)
+
+        when: 'the generated closure classes for the where query are located'
+        List<Class<?>> queryClosures = findQueryClosures(gcl).sort { 
it.name.count('$_closure') }
+        Class<?> outerClosure = queryClosures.first()
+        Class<?> associationClosure = queryClosures.last()
+
+        then: 'the outer closure captures both parameters it references'
+        outerClosure != null
+        capturedReferenceCount(outerClosure) == 2
+
+        and: 'the innermost association closure captures only the parameter it 
references'
+        associationClosure.name.count('$_closure') > 1
+        capturedReferenceCount(associationClosure) == 1
+    }
+
+    void "generated closure constructors are identical across compilations"() {
+        when: 'the same source is compiled twice in isolated class loaders'
+        List<String> firstSignatures = closureConstructorSignatures(new 
GroovyClassLoader())
+        List<String> secondSignatures = closureConstructorSignatures(new 
GroovyClassLoader())
+
+        then:
+        !firstSignatures.empty
+        firstSignatures == secondSignatures
+    }
+
+    private static List<String> closureConstructorSignatures(GroovyClassLoader 
gcl) {
+        gcl.parseClass(SERVICE_SOURCE)
+        findQueryClosures(gcl).collect { Class<?> closureClass ->
+            Constructor<?> constructor = 
closureClass.declaredConstructors.first()
+            "${closureClass.name.replaceAll(/^.*\$_/, 
'')}(${constructor.parameterTypes*.simpleName.join(', ')})".toString()
+        }.sort()
+    }
+
+    private static List<Class<?>> findQueryClosures(GroovyClassLoader gcl) {
+        gcl.loadedClasses.findAll { 
it.name.contains('_findQueryByBookIdAndAuthorName_') }
+    }
+
+    private static int capturedReferenceCount(Class<?> closureClass) {
+        Constructor<?> constructor = closureClass.declaredConstructors.first()
+        constructor.parameterTypes.count { it == Reference }
+    }
+}

Reply via email to