View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.lang3;
19  
20  import static org.junit.jupiter.api.Assertions.assertThrows;
21  import static org.junit.jupiter.api.Assertions.assertTimeout;
22  
23  import java.time.Duration;
24  
25  import org.junit.jupiter.api.Test;
26  
27  /**
28   * Primitive joins can OOM before index validation.
29   * <p>
30   * capacity() is called before bounds validation in join(byte[], char, int, int). An endIndex of Integer.MAX_VALUE causes an attempt to allocate ~8 GB before
31   * the array bounds are checked. Negative-startIndex bypass case.
32   * </p>
33   * <p>
34   * For {@code startIndex = -2_000_000_000, endIndex = 1, array.length = 2}: {@code count = endIndex - startIndex = 2_000_000_001}, which is positive (no
35   * overflow into negative territory), so the {@code count <= 0} early-return does not fire. The pre-existing {@code endIndex > array.length} check also passes
36   * (1 &lt; 2). The capacity allocation then attempts to size a StringBuilder for ~2 billion items, triggering OOM before the loop body would have caught the bad
37   * index.
38   * </p>
39   *
40   * Pre-patch: throws OutOfMemoryError or hangs for > 2 seconds. Post-patch: throws ArrayIndexOutOfBoundsException quickly (within 2 seconds).
41   */
42  public class StringUtilsJoinExceptionTest {
43  
44      private static final Class<ArrayIndexOutOfBoundsException> EXPECTED_EX = ArrayIndexOutOfBoundsException.class;
45      private static final Duration TIMEOUT = Duration.ofSeconds(2);
46  
47      @Test
48      public void testBooleanJoinValidatesEndIndexBeforeCapacity() {
49          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new boolean[] { true }, ',', 0, Integer.MAX_VALUE)));
50      }
51  
52      @Test
53      public void testBooleanJoinValidatesNegativeStartIndexBeforeCapacity() {
54          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new boolean[] { true, false }, ',', -2_000_000_000, 1)));
55      }
56  
57      @Test
58      public void testByteJoinValidatesEndIndexBeforeCapacity() {
59          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new byte[] { 1 }, ',', 0, Integer.MAX_VALUE)));
60      }
61  
62      @Test
63      public void testByteJoinValidatesNegativeStartIndexBeforeCapacity() {
64          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new byte[] { 1, 2 }, ',', -2_000_000_000, 1)));
65      }
66  
67      @Test
68      public void testCharJoinValidatesEndIndexBeforeCapacity() {
69          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new char[] { 1 }, ',', 0, Integer.MAX_VALUE)));
70      }
71  
72      @Test
73      public void testCharJoinValidatesNegativeStartIndexBeforeCapacity() {
74          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new char[] { 1, 2 }, ',', -2_000_000_000, 1)));
75      }
76  
77      @Test
78      public void testDoubleJoinValidatesEndIndexBeforeCapacity() {
79          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new double[] { 1 }, ',', 0, Integer.MAX_VALUE)));
80      }
81  
82      @Test
83      public void testDoubleJoinValidatesNegativeStartIndexBeforeCapacity() {
84          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new double[] { 1, 2 }, ',', Integer.MIN_VALUE + 100, 1)));
85      }
86  
87      @Test
88      public void testFloatJoinValidatesEndIndexBeforeCapacity() {
89          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new float[] { 1 }, ',', 0, Integer.MAX_VALUE)));
90      }
91  
92      @Test
93      public void testFloatJoinValidatesNegativeStartIndexBeforeCapacity() {
94          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new float[] { 1, 2 }, ',', Integer.MIN_VALUE + 100, 1)));
95      }
96  
97      @Test
98      public void testIntJoinValidatesEndIndexBeforeCapacity() {
99          assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new int[] { 1 }, ',', 0, Integer.MAX_VALUE)));
100     }
101 
102     @Test
103     public void testIntJoinValidatesNegativeStartIndexBeforeCapacity() {
104         assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new int[] { 1, 2 }, ',', Integer.MIN_VALUE + 100, 1)));
105     }
106 
107     @Test
108     public void testLongJoinValidatesEndIndexBeforeCapacity() {
109         assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new long[] { 1L }, ',', 0, Integer.MAX_VALUE)));
110     }
111 
112     @Test
113     public void testLongJoinValidatesNegativeStartIndexBeforeCapacity() {
114         assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new long[] { 1L, 2L }, ',', -2_000_000_000, 1)));
115     }
116 
117     @Test
118     public void testShortJoinValidatesEndIndexBeforeCapacity() {
119         assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new short[] { 1 }, ',', 0, Integer.MAX_VALUE)));
120     }
121 
122     @Test
123     public void testShortJoinValidatesNegativeStartIndexBeforeCapacity() {
124         assertTimeout(TIMEOUT, () -> assertThrows(EXPECTED_EX, () -> StringUtils.join(new short[] { 1, 2 }, ',', -2_000_000_000, 1)));
125     }
126 }