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 e60d2aff Disassembler and Class2HTML re-implement the unchecked 
switch-table allocation (f009).
e60d2aff is described below

commit e60d2affd5f934b2c5c7410b7699be288adcea85
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 4 16:10:21 2026 -0400

    Disassembler and Class2HTML re-implement the unchecked switch-table
    allocation (f009).
---
 src/changes/changes.xml                            |  1 +
 .../java/org/apache/bcel/classfile/Utility.java    | 13 ++++++++++++-
 src/main/java/org/apache/bcel/util/CodeHTML.java   | 14 +++++++++++++-
 .../org/apache/bcel/classfile/UtilityTest.java     | 22 ++++++++++++++++++++++
 4 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index d12360b7..7e1ad79e 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -96,6 +96,7 @@ The <action> type attribute can be add,update,fix,remove.
       <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>
+      <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Disassembler and Class2HTML re-implement the unchecked switch-table 
allocation (f009).</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/classfile/Utility.java 
b/src/main/java/org/apache/bcel/classfile/Utility.java
index 7dddc70a..ddee58cf 100644
--- a/src/main/java/org/apache/bcel/classfile/Utility.java
+++ b/src/main/java/org/apache/bcel/classfile/Utility.java
@@ -343,8 +343,15 @@ public abstract class Utility {
             high = bytes.readInt();
             offset = bytes.getIndex() - 12 - noPadBytes - 1;
             defaultOffset += offset;
+            // Each jump table entry is a 4 byte offset, so a well-formed 
table cannot declare more entries than fit
+            // into the remaining byte code; checking before allocating keeps 
a crafted low/high pair from forcing a
+            // huge allocation.
+            final long jumpTableLength = (long) high - low + 1;
+            if (jumpTableLength < 0 || jumpTableLength * 4 > 
bytes.available()) {
+                throw new ClassFormatException("Invalid TABLESWITCH: low = " + 
low + ", high = " + high + " but only " + bytes.available() + " bytes remain");
+            }
             buf.append("\tdefault = ").append(defaultOffset).append(", low = 
").append(low).append(", high = ").append(high).append("(");
-            jumpTable = new int[high - low + 1];
+            jumpTable = new int[(int) jumpTableLength];
             for (int i = 0; i < jumpTable.length; i++) {
                 jumpTable[i] = offset + bytes.readInt();
                 buf.append(jumpTable[i]);
@@ -360,6 +367,10 @@ public abstract class Utility {
         case Const.LOOKUPSWITCH: {
             npairs = bytes.readInt();
             offset = bytes.getIndex() - 8 - noPadBytes - 1;
+            // Each match-offset pair is 8 bytes, see the TABLESWITCH check 
above.
+            if (npairs < 0 || (long) npairs * 8 > bytes.available()) {
+                throw new ClassFormatException("Invalid LOOKUPSWITCH: npairs = 
" + npairs + " but only " + bytes.available() + " bytes remain");
+            }
             match = new int[npairs];
             jumpTable = new int[npairs];
             defaultOffset += offset;
diff --git a/src/main/java/org/apache/bcel/util/CodeHTML.java 
b/src/main/java/org/apache/bcel/util/CodeHTML.java
index bb7b09b7..3b9d1b90 100644
--- a/src/main/java/org/apache/bcel/util/CodeHTML.java
+++ b/src/main/java/org/apache/bcel/util/CodeHTML.java
@@ -25,6 +25,7 @@ import java.util.BitSet;
 
 import org.apache.bcel.Const;
 import org.apache.bcel.classfile.Attribute;
+import org.apache.bcel.classfile.ClassFormatException;
 import org.apache.bcel.classfile.Code;
 import org.apache.bcel.classfile.CodeException;
 import org.apache.bcel.classfile.ConstantFieldref;
@@ -109,9 +110,16 @@ final class CodeHTML {
             high = bytes.readInt();
             offset = bytes.getIndex() - 12 - noPadBytes - 1;
             defaultOffset += offset;
+            // Each jump table entry is a 4 byte offset, so a well-formed 
table cannot declare more entries than fit
+            // into the remaining byte code; checking before allocating keeps 
a crafted low/high pair from forcing a
+            // huge allocation.
+            final long jumpTableLength = (long) high - low + 1;
+            if (jumpTableLength < 0 || jumpTableLength * 4 > 
bytes.available()) {
+                throw new ClassFormatException("Invalid TABLESWITCH: low = " + 
low + ", high = " + high + " but only " + bytes.available() + " bytes remain");
+            }
             buf.append("<TABLE BORDER=1><TR>");
             // Print switch indices in first row (and default)
-            jumpTable = new int[high - low + 1];
+            jumpTable = new int[(int) jumpTableLength];
             for (int i = 0; i < jumpTable.length; i++) {
                 jumpTable[i] = offset + bytes.readInt();
                 buf.append("<TH>").append(low + i).append("</TH>");
@@ -130,6 +138,10 @@ final class CodeHTML {
         case Const.LOOKUPSWITCH:
             final int npairs = bytes.readInt();
             offset = bytes.getIndex() - 8 - noPadBytes - 1;
+            // Each match-offset pair is 8 bytes, see the TABLESWITCH check 
above.
+            if (npairs < 0 || (long) npairs * 8 > bytes.available()) {
+                throw new ClassFormatException("Invalid LOOKUPSWITCH: npairs = 
" + npairs + " but only " + bytes.available() + " bytes remain");
+            }
             jumpTable = new int[npairs];
             defaultOffset += offset;
             buf.append("<TABLE BORDER=1><TR>");
diff --git a/src/test/java/org/apache/bcel/classfile/UtilityTest.java 
b/src/test/java/org/apache/bcel/classfile/UtilityTest.java
index 12ebb059..3d276583 100644
--- a/src/test/java/org/apache/bcel/classfile/UtilityTest.java
+++ b/src/test/java/org/apache/bcel/classfile/UtilityTest.java
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -90,6 +91,27 @@ class UtilityTest {
         }
     }
 
+    @Test
+    void testCodeToStringOversizedLookupSwitch() {
+        // A LOOKUPSWITCH claiming Integer.MAX_VALUE pairs without any table 
bytes behind it
+        // must be rejected instead of allocating two huge arrays.
+        final byte[] code = { (byte) Const.LOOKUPSWITCH, 0, 0, 0, // opcode 
plus 3 padding bytes
+            0, 0, 0, 0, // default offset
+            0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff }; // npairs = 
Integer.MAX_VALUE
+        assertThrows(ClassFormatException.class, () -> 
Utility.codeToString(new ByteSequence(code), new ConstantPool()));
+    }
+
+    @Test
+    void testCodeToStringOversizedTableSwitch() {
+        // A TABLESWITCH claiming 2^32 entries without any table bytes behind 
it must be
+        // rejected instead of allocating a huge jump table.
+        final byte[] code = { (byte) Const.TABLESWITCH, 0, 0, 0, // opcode 
plus 3 padding bytes
+            0, 0, 0, 0, // default offset
+            (byte) 0x80, 0, 0, 0, // low = Integer.MIN_VALUE
+            0x7f, (byte) 0xff, (byte) 0xff, (byte) 0xff }; // high = 
Integer.MAX_VALUE
+        assertThrows(ClassFormatException.class, () -> 
Utility.codeToString(new ByteSequence(code), new ConstantPool()));
+    }
+
     @Test
     void testCodeToStringWideIsThreadLocal() throws Exception {
         // A WIDE opcode disassembled on one thread must not change how the 
next

Reply via email to