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 49a8bcb6 Opcodes tableswitch and lookupswitch add boundary checks 
(f003).
49a8bcb6 is described below

commit 49a8bcb631c1d0858813b12e0a2c94d8adee66d8
Author: Gary Gregory <[email protected]>
AuthorDate: Fri Sep 4 15:35:11 2026 -0400

    Opcodes tableswitch and lookupswitch add boundary checks (f003).
---
 src/changes/changes.xml                            |  1 +
 .../java/org/apache/bcel/generic/LOOKUPSWITCH.java |  6 ++
 .../java/org/apache/bcel/generic/TABLESWITCH.java  | 11 ++-
 .../apache/bcel/generic/SwitchAllocationTest.java  | 93 ++++++++++++++++++++++
 4 files changed, 110 insertions(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 81430405..e76b7f18 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -90,6 +90,7 @@ The <action> type attribute can be add,update,fix,remove.
       <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Fix SpotBugs USO_UNSAFE_METHOD_SYNCHRONIZATION in 
ConstantUtf8.</action>
       <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Nested Code/Record attributes drive unbounded parse-time recursion in 
ClassParser (f001).</action>
       <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Nested annotation element values recurse unboundedly; 
MAX_ARRAY_DIMENSIONS cap bypassed (f002).</action>
+      <action                  type="fix" dev="ggregory" due-to="Gary 
Gregory">Opcodes tableswitch and lookupswitch add boundary checks 
(f003).</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/generic/LOOKUPSWITCH.java 
b/src/main/java/org/apache/bcel/generic/LOOKUPSWITCH.java
index 602b4a06..67e7a20c 100644
--- a/src/main/java/org/apache/bcel/generic/LOOKUPSWITCH.java
+++ b/src/main/java/org/apache/bcel/generic/LOOKUPSWITCH.java
@@ -21,6 +21,7 @@ package org.apache.bcel.generic;
 import java.io.DataOutputStream;
 import java.io.IOException;
 
+import org.apache.bcel.classfile.ClassFormatException;
 import org.apache.bcel.util.ByteSequence;
 
 /**
@@ -89,6 +90,11 @@ public class LOOKUPSWITCH extends Select {
     protected void initFromFile(final ByteSequence bytes, final boolean wide) 
throws IOException {
         super.initFromFile(bytes, wide); // reads padding
         final int matchLength = bytes.readInt();
+        // Require the match table to actually fit into the remaining code 
bytes (8 bytes per match-offset pair). The npairs field is attacker-controlled 
in
+        // a malicious class file and could otherwise request a multi-gigabyte 
allocation, or a negative array size, before a single pair is read.
+        if (matchLength < 0 || matchLength > bytes.available() / 8) {
+            throw new ClassFormatException("Invalid lookupswitch: npairs=" + 
matchLength + ", but only " + bytes.available() + " bytes of code remain.");
+        }
         setMatchLength(matchLength);
         final short fixedLength = (short) (9 + matchLength * 8);
         setFixedLength(fixedLength);
diff --git a/src/main/java/org/apache/bcel/generic/TABLESWITCH.java 
b/src/main/java/org/apache/bcel/generic/TABLESWITCH.java
index b43742f8..736fbc68 100644
--- a/src/main/java/org/apache/bcel/generic/TABLESWITCH.java
+++ b/src/main/java/org/apache/bcel/generic/TABLESWITCH.java
@@ -21,6 +21,7 @@ package org.apache.bcel.generic;
 import java.io.DataOutputStream;
 import java.io.IOException;
 
+import org.apache.bcel.classfile.ClassFormatException;
 import org.apache.bcel.util.ByteSequence;
 
 /**
@@ -92,7 +93,15 @@ public class TABLESWITCH extends Select {
         super.initFromFile(bytes, wide);
         final int low = bytes.readInt();
         final int high = bytes.readInt();
-        final int matchLength = high - low + 1;
+        // Compute in long arithmetic to guard against integer overflow, and 
require the match table to actually fit into the remaining code bytes (4 bytes
+        // per jump offset). The low and high fields are attacker-controlled 
in a malicious class file and could otherwise request a multi-gigabyte
+        // allocation, or a negative array size, before a single table entry 
is read.
+        final long matchLengthLong = (long) high - low + 1;
+        if (matchLengthLong < 0 || matchLengthLong > bytes.available() / 4) {
+            throw new ClassFormatException(
+                    "Invalid tableswitch: low=" + low + ", high=" + high + ", 
but only " + bytes.available() + " bytes of code remain.");
+        }
+        final int matchLength = (int) matchLengthLong;
         setMatchLength(matchLength);
         final short fixedLength = (short) (13 + matchLength * 4);
         setFixedLength(fixedLength);
diff --git a/src/test/java/org/apache/bcel/generic/SwitchAllocationTest.java 
b/src/test/java/org/apache/bcel/generic/SwitchAllocationTest.java
new file mode 100644
index 00000000..3ff7fd4d
--- /dev/null
+++ b/src/test/java/org/apache/bcel/generic/SwitchAllocationTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.apache.bcel.generic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+
+import org.apache.bcel.Const;
+import org.apache.bcel.classfile.ClassFormatException;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests that {@link TABLESWITCH} and {@link LOOKUPSWITCH} validate their 
match table counts against the remaining code bytes instead of allocating 
whatever
+ * amount of memory the raw count fields request.
+ */
+class SwitchAllocationTest {
+
+    private static byte[] switchBytes(final short opcode, final int... 
operands) throws IOException {
+        final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (DataOutputStream dos = new DataOutputStream(baos)) {
+            dos.writeByte(opcode);
+            for (int i = 0; i < 3; i++) {
+                dos.writeByte(0); // padding up to the next 4-byte boundary
+            }
+            for (final int operand : operands) {
+                dos.writeInt(operand);
+            }
+        }
+        return baos.toByteArray();
+    }
+
+    @Test
+    void testLookupswitchHugeNpairsRejected() throws IOException {
+        // default = 0, npairs = Integer.MAX_VALUE, no pairs present.
+        final byte[] code = switchBytes(Const.LOOKUPSWITCH, 0, 
Integer.MAX_VALUE);
+        assertThrows(ClassFormatException.class, () -> new 
InstructionList(code));
+    }
+
+    @Test
+    void testLookupswitchNegativeNpairsRejected() throws IOException {
+        final byte[] code = switchBytes(Const.LOOKUPSWITCH, 0, -1);
+        assertThrows(ClassFormatException.class, () -> new 
InstructionList(code));
+    }
+
+    @Test
+    void testLookupswitchValidAccepted() throws IOException {
+        // default = 0, npairs = 1, one (match, offset) pair branching back to 
the instruction itself.
+        final byte[] code = switchBytes(Const.LOOKUPSWITCH, 0, 1, 5, 0);
+        assertEquals(1, new InstructionList(code).getLength());
+    }
+
+    @Test
+    void testTableswitchHugeRangeRejected() throws IOException {
+        // default = 0, low = 0, high = Integer.MAX_VALUE - 1: requests 
roughly 8 GB across the three tables.
+        final byte[] code = switchBytes(Const.TABLESWITCH, 0, 0, 
Integer.MAX_VALUE - 1);
+        assertThrows(ClassFormatException.class, () -> new 
InstructionList(code));
+    }
+
+    @Test
+    void testTableswitchOverflowingRangeRejected() throws IOException {
+        // high - low + 1 overflows int arithmetic to a negative value.
+        final byte[] code = switchBytes(Const.TABLESWITCH, 0, 
Integer.MIN_VALUE, Integer.MAX_VALUE);
+        assertThrows(ClassFormatException.class, () -> new 
InstructionList(code));
+    }
+
+    @Test
+    void testTableswitchValidAccepted() throws IOException {
+        // default = 0, low = 0, high = 1, two jump offsets branching back to 
the instruction itself.
+        final byte[] code = switchBytes(Const.TABLESWITCH, 0, 0, 1, 0, 0);
+        assertEquals(1, new InstructionList(code).getLength());
+    }
+}

Reply via email to