View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.bcel.classfile;
21  
22  import static org.junit.jupiter.api.Assertions.assertNotNull;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  import static org.junit.jupiter.api.Assertions.fail;
25  
26  import java.io.ByteArrayInputStream;
27  import java.util.concurrent.Callable;
28  import java.util.concurrent.ExecutionException;
29  import java.util.concurrent.ExecutorService;
30  import java.util.concurrent.Executors;
31  import java.util.concurrent.Future;
32  import java.util.concurrent.TimeUnit;
33  import java.util.concurrent.TimeoutException;
34  
35  import org.apache.bcel.Repository;
36  import org.apache.bcel.util.SyntheticRepository;
37  import org.junit.jupiter.api.AfterEach;
38  import org.junit.jupiter.api.BeforeEach;
39  import org.junit.jupiter.api.Test;
40  
41  /**
42   * Tests {@link JavaClass}.
43   * <p>
44   * Tests for cyclic hierarchy vulnerabilities in {@link JavaClass}. These tests demonstrate CWE-674 (Uncontrolled Recursion) vulnerabilities:
45   * <ul>
46   * <li>getAllInterfaces(): infinite queue growth with cyclic interfaces</li>
47   * <li>getSuperClasses(): infinite loop with cyclic superclasses Without the fix, these tests will fail. With the fix, they pass.</li>
48   * </ul>
49   */
50  class JavaClassCyclicTest {
51  
52      private JavaClass cyclicClassA;
53  
54      private JavaClass cyclicClassB;
55  
56      private JavaClass cyclicInterfaceA;
57  
58      private JavaClass cyclicInterfaceB;
59  
60      private JavaClass cyclicTestClass;
61  
62      private SyntheticRepository repo;
63  
64      @BeforeEach
65      void setUp() throws Exception {
66          repo = SyntheticRepository.getInstance();
67          Repository.setRepository(repo);
68          // Create cyclic interfaces: A extends B, B extends A
69          final byte[] interfaceABytes = JavaClassTest.createInterface("CyclicInterfaceA", "CyclicInterfaceB");
70          final byte[] interfaceBBytes = JavaClassTest.createInterface("CyclicInterfaceB", "CyclicInterfaceA");
71          final byte[] testClassBytes = JavaClassTest.createClass("CyclicTestClass", "java.lang.Object", "CyclicInterfaceA");
72          cyclicInterfaceA = new ClassParser(new ByteArrayInputStream(interfaceABytes), "CyclicInterfaceA.class").parse();
73          cyclicInterfaceB = new ClassParser(new ByteArrayInputStream(interfaceBBytes), "CyclicInterfaceB.class").parse();
74          cyclicTestClass = new ClassParser(new ByteArrayInputStream(testClassBytes), "CyclicTestClass.class").parse();
75          repo.storeClass(cyclicInterfaceA);
76          repo.storeClass(cyclicInterfaceB);
77          repo.storeClass(cyclicTestClass);
78          // Create cyclic classes: A extends B, B extends A
79          final byte[] classABytes = JavaClassTest.createClass("CyclicClassA", "CyclicClassB");
80          final byte[] classBBytes = JavaClassTest.createClass("CyclicClassB", "CyclicClassA");
81          cyclicClassA = new ClassParser(new ByteArrayInputStream(classABytes), "CyclicClassA.class").parse();
82          cyclicClassB = new ClassParser(new ByteArrayInputStream(classBBytes), "CyclicClassB.class").parse();
83          repo.storeClass(cyclicClassA);
84          repo.storeClass(cyclicClassB);
85      }
86  
87      @AfterEach
88      void tearDown() {
89          if (cyclicInterfaceA != null) {
90              repo.removeClass(cyclicInterfaceA);
91          }
92          if (cyclicInterfaceB != null) {
93              repo.removeClass(cyclicInterfaceB);
94          }
95          if (cyclicTestClass != null) {
96              repo.removeClass(cyclicTestClass);
97          }
98          if (cyclicClassA != null) {
99              repo.removeClass(cyclicClassA);
100         }
101         if (cyclicClassB != null) {
102             repo.removeClass(cyclicClassB);
103         }
104     }
105 
106     /**
107      * Tests that getAllInterfaces() handles cyclic interface hierarchies gracefully. BUG: Without fix, getAllInterfaces() has no visited-node check before
108      * enqueueing, causing infinite queue growth and eventual heap exhaustion (OutOfMemoryError). FIXED: With the fix, already-visited nodes are skipped,
109      * preventing infinite growth.
110      */
111     @Test
112     void testGetAllInterfacesCyclic() throws Exception {
113         // TODO Use the test method once ClassCircularityError is implemented for this case
114         // test(cyclicTestClass::getAllInterfaces);
115         // TOOO Remove once the above is used
116         final ExecutorService executor = Executors.newSingleThreadExecutor();
117         try {
118             final Future<JavaClass[]> future = executor.submit(() -> cyclicTestClass.getAllInterfaces());
119             // Without fix: will timeout (infinite queue growth) or throw OOM
120             // With fix: completes quickly and returns the interfaces
121             final JavaClass[] interfaces = future.get(3, TimeUnit.SECONDS);
122             assertNotNull(interfaces, "getAllInterfaces() should return non-null array");
123             assertTrue(interfaces.length >= 2, "Should find at least CyclicInterfaceA and CyclicInterfaceB");
124         } catch (final TimeoutException e) {
125             fail("getAllInterfaces() timed out - infinite queue growth vulnerability detected");
126         } catch (final ExecutionException e) {
127             if (e.getCause() instanceof OutOfMemoryError) {
128                 fail("getAllInterfaces() caused OutOfMemoryError - infinite queue growth vulnerability detected");
129             }
130             throw e;
131         } finally {
132             executor.shutdownNow();
133         }
134     }
135 
136     /**
137      * Tests that getSuperClasses() detects cyclic superclass hierarchies. BUG: Without fix, getSuperClasses() has no cycle detection, causing an infinite loop
138      * when traversing cyclic superclass chains. FIXED: With the fix, ClassCircularityError is thrown when a cycle is detected.
139      */
140     @Test
141     void testGetSuperClassesCyclic() throws Exception {
142         test(cyclicClassA::getSuperClasses);
143     }
144 
145     void test(final Callable<JavaClass[]> callable) throws Exception {
146         final ExecutorService executor = Executors.newSingleThreadExecutor();
147         try {
148             final Future<JavaClass[]> future = executor.submit(callable);
149             // Without fix: will timeout (infinite loop)
150             // With fix: throws ClassCircularityError immediately
151             future.get(3, TimeUnit.SECONDS);
152             fail("Should have thrown ClassCircularityError for cyclic hierarchy");
153         } catch (final TimeoutException e) {
154             fail("Timeout: infinite loop vulnerability detected");
155         } catch (final ExecutionException e) {
156             if (e.getCause() instanceof ClassFormatException) {
157                 // Expected with fix - test passes
158                 return;
159             }
160             throw e;
161         } finally {
162             executor.shutdownNow();
163         }
164     }
165 }