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

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-bcel.git


The following commit(s) were added to refs/heads/master by this push:
     new 38434609 Subroutines.noRecursiveCalls enumerates exponentially many 
JSR call paths (f008).
38434609 is described below

commit 38434609d1873d5230f6817bc843799cae10aa36
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 4 16:04:51 2026 -0400

    Subroutines.noRecursiveCalls enumerates exponentially many JSR call
    paths (f008).
---
 src/changes/changes.xml                            |  1 +
 .../bcel/verifier/structurals/Subroutines.java     | 87 ++++++++++++++++------
 2 files changed, 66 insertions(+), 22 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 4003f910..d12360b7 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -95,6 +95,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">JustIce Pass 2 LVT check amplifies each 10-byte entry into ~131k 
hashtable operations (f005).</action>
       <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Pass 3b dataflow stores frames per instruction and clones ever-growing 
execution chains (f006).</action>
       <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Cubic handlers × range × subroutines scan in Subroutines constructor 
hangs JustIce Pass 3b (f007).</action>
+      <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Subroutines.noRecursiveCalls enumerates exponentially many JSR call 
paths (f008).</action>
       <!-- ADD -->
       <action                  type="add" dev="ggregory" due-to="nbauma109, 
Gary Gregory">Add support for permitted subclasses #493.</action>
       <action                  type="add" dev="ggregory" due-to="nbauma109, 
Gary Gregory">Add RecordComponentInfo.getAttribute(byte tag)#494.</action>
diff --git 
a/src/main/java/org/apache/bcel/verifier/structurals/Subroutines.java 
b/src/main/java/org/apache/bcel/verifier/structurals/Subroutines.java
index 6addf24d..2013b7b4 100644
--- a/src/main/java/org/apache/bcel/verifier/structurals/Subroutines.java
+++ b/src/main/java/org/apache/bcel/verifier/structurals/Subroutines.java
@@ -19,6 +19,7 @@
 package org.apache.bcel.verifier.structurals;
 
 import java.util.ArrayList;
+import java.util.BitSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -223,7 +224,7 @@ public class Subroutines {
             for (final int lv : lvs) {
                 s.add(Integer.valueOf(lv));
             }
-            getRecursivelyAccessedLocalsIndicesHelper(s, subSubs());
+            getRecursivelyAccessedLocalsIndicesHelper(s, subSubs(), new 
HashSet<>());
             final int[] ret = new int[s.size()];
             int j = -1;
             for (final Integer index : s) {
@@ -234,18 +235,24 @@ public class Subroutines {
         }
 
         /**
-         * A recursive helper method for getRecursivelyAccessedLocalsIndices().
+         * A recursive helper method for 
getRecursivelyAccessedLocalsIndices(). Every subroutine is visited at most
+         * once: since the computed set is a plain union, re-exploring an 
already visited subroutine cannot add
+         * anything, but doing so once per call path made this helper 
exponential in the depth of the JSR call graph
+         * (and made it recurse forever on a cyclic one).
          *
          * @see #getRecursivelyAccessedLocalsIndices()
          */
-        private void getRecursivelyAccessedLocalsIndicesHelper(final 
Set<Integer> set, final Subroutine[] subs) {
+        private void getRecursivelyAccessedLocalsIndicesHelper(final 
Set<Integer> set, final Subroutine[] subs, final Set<Subroutine> visited) {
             for (final Subroutine sub : subs) {
+                if (!visited.add(sub)) {
+                    continue;
+                }
                 final int[] lvs = sub.getAccessedLocalsIndices();
                 for (final int lv : lvs) {
                     set.add(Integer.valueOf(lv));
                 }
                 if (sub.subSubs().length != 0) {
-                    getRecursivelyAccessedLocalsIndicesHelper(set, 
sub.subSubs());
+                    getRecursivelyAccessedLocalsIndicesHelper(set, 
sub.subSubs(), visited);
                 }
             }
         }
@@ -577,7 +584,7 @@ public class Subroutines {
         // This includes that subroutines may not call themselves
         // recursively, even not through intermediate calls to other
         // subroutines.
-        noRecursiveCalls(getTopLevel(), new HashSet<>());
+        noRecursiveCalls(getTopLevel());
 
     }
 
@@ -617,30 +624,66 @@ public class Subroutines {
     }
 
     /**
-     * This (recursive) utility method makes sure that no subroutine is 
calling a subroutine that uses the same local
+     * This utility method makes sure that no subroutine is calling a 
subroutine that uses the same local
      * variable for the RET as themselves (recursively). This includes that 
subroutines may not call themselves recursively,
      * even not through intermediate calls to other subroutines.
      *
+     * Every subroutine is fully validated exactly once, memoizing the RET 
local variable indices used anywhere in its
+     * call subtree. The former implementation re-explored a subroutine once 
per call path, which is exponential in the
+     * number of subroutines for a layered JSR call graph.
+     *
      * @throws StructuralCodeConstraintException if the above constraint is 
not satisfied.
      */
-    private void noRecursiveCalls(final Subroutine sub, final Set<Integer> 
set) {
-        final Subroutine[] subs = sub.subSubs();
-
-        for (final Subroutine sub2 : subs) {
-            final int index = ((RET) 
sub2.getLeavingRET().getInstruction()).getIndex();
-
-            if (!set.add(Integer.valueOf(index))) {
-                // Don't use toString() here because of possibly infinite 
recursive subSubs() calls then.
-                final SubroutineImpl si = (SubroutineImpl) sub2;
-                throw new StructuralCodeConstraintException("Subroutine with 
local variable '" + si.localVariable + "', JSRs '" + si.theJSRs + "', RET '"
-                    + si.theRET + "' is called by a subroutine which uses the 
same local variable index as itself; maybe even a recursive call?"
-                    + " JustIce's clean definition of a subroutine forbids 
both.");
-            }
-
-            noRecursiveCalls(sub2, set);
+    private void noRecursiveCalls(final Subroutine sub) {
+        noRecursiveCalls(sub, new BitSet(), new HashMap<>(), new HashMap<>());
+    }
 
-            set.remove(Integer.valueOf(index));
+    /**
+     * The recursive helper for {@link #noRecursiveCalls(Subroutine)}.
+     *
+     * @param sub the subroutine whose callees are validated.
+     * @param pathLocals compact ids (see {@code localIds}) of the RET local 
variables used by the subroutines on the current call path.
+     * @param validated maps every fully validated subroutine to the compact 
ids of the RET local variables used by it and its entire call subtree.
+     * @param localIds maps a RET local variable index to a compact id so the 
bit sets stay small.
+     * @return the compact ids of the RET local variables used by {@code 
sub}'s callees and their call subtrees.
+     * @throws StructuralCodeConstraintException if a subroutine calls a 
subroutine using the same RET local variable.
+     */
+    private BitSet noRecursiveCalls(final Subroutine sub, final BitSet 
pathLocals, final Map<Subroutine, BitSet> validated,
+        final Map<Integer, Integer> localIds) {
+        final BitSet subtreeLocals = new BitSet();
+
+        for (final Subroutine sub2 : sub.subSubs()) {
+            final Integer index = Integer.valueOf(((RET) 
sub2.getLeavingRET().getInstruction()).getIndex());
+            final int localId = localIds.computeIfAbsent(index, k -> 
Integer.valueOf(localIds.size())).intValue();
+
+            BitSet childLocals = validated.get(sub2);
+            if (childLocals == null) {
+                if (pathLocals.get(localId)) {
+                    // sub2 uses a RET local variable also used by a 
subroutine on the current call path;
+                    // this also covers (possibly indirect) recursive calls.
+                    throw recursiveCallException(sub2);
+                }
+                pathLocals.set(localId);
+                childLocals = noRecursiveCalls(sub2, pathLocals, validated, 
localIds);
+                pathLocals.clear(localId);
+                childLocals.set(localId);
+                validated.put(sub2, childLocals);
+            } else if (childLocals.intersects(pathLocals)) {
+                // A subroutine in sub2's (already validated) call subtree 
uses a RET local variable also
+                // used by a subroutine on the current call path.
+                throw recursiveCallException(sub2);
+            }
+            subtreeLocals.or(childLocals);
         }
+        return subtreeLocals;
+    }
+
+    private static StructuralCodeConstraintException 
recursiveCallException(final Subroutine sub2) {
+        // Don't use toString() here because of possibly infinite recursive 
subSubs() calls then.
+        final SubroutineImpl si = (SubroutineImpl) sub2;
+        return new StructuralCodeConstraintException("Subroutine with local 
variable '" + si.localVariable + "', JSRs '" + si.theJSRs + "', RET '"
+            + si.theRET + "' is called by a subroutine which uses the same 
local variable index as itself; maybe even a recursive call?"
+            + " JustIce's clean definition of a subroutine forbids both.");
     }
 
     /**

Reply via email to