Skip to content

Commit 19b59ff

Browse files
authored
StringUtils.joins() for primitive types can throw OOME before index (#1636)
validation.
1 parent b64542b commit 19b59ff

2 files changed

Lines changed: 143 additions & 0 deletions

File tree

src/main/java/org/apache/commons/lang3/StringUtils.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,15 @@ public static String center(String str, final int size, String padStr) {
654654
return rightPad(str, size, padStr);
655655
}
656656

657+
private static void checkFromToIndex(final int startIndex, final int endIndex, final int length) {
658+
if (startIndex < 0) {
659+
throw new ArrayIndexOutOfBoundsException(startIndex);
660+
}
661+
if (endIndex > length) {
662+
throw new ArrayIndexOutOfBoundsException(endIndex);
663+
}
664+
}
665+
657666
/**
658667
* Removes one newline from end of a String if it's there, otherwise leave it alone. A newline is &quot;{@code \n}&quot;, &quot;{@code \r}&quot;, or
659668
* &quot;{@code \r\n}&quot;.
@@ -3866,6 +3875,7 @@ public static String join(final boolean[] array, final char delimiter, final int
38663875
if (array == null) {
38673876
return null;
38683877
}
3878+
checkFromToIndex(startIndex, endIndex, array.length);
38693879
final int count = endIndex - startIndex;
38703880
if (count <= 0) {
38713881
return EMPTY;
@@ -3943,6 +3953,7 @@ public static String join(final byte[] array, final char delimiter, final int st
39433953
if (array == null) {
39443954
return null;
39453955
}
3956+
checkFromToIndex(startIndex, endIndex, array.length);
39463957
final int count = endIndex - startIndex;
39473958
if (count <= 0) {
39483959
return EMPTY;
@@ -4020,6 +4031,7 @@ public static String join(final char[] array, final char delimiter, final int st
40204031
if (array == null) {
40214032
return null;
40224033
}
4034+
checkFromToIndex(startIndex, endIndex, array.length);
40234035
final int count = endIndex - startIndex;
40244036
if (count <= 0) {
40254037
return EMPTY;
@@ -4097,6 +4109,7 @@ public static String join(final double[] array, final char delimiter, final int
40974109
if (array == null) {
40984110
return null;
40994111
}
4112+
checkFromToIndex(startIndex, endIndex, array.length);
41004113
final int count = endIndex - startIndex;
41014114
if (count <= 0) {
41024115
return EMPTY;
@@ -4174,6 +4187,7 @@ public static String join(final float[] array, final char delimiter, final int s
41744187
if (array == null) {
41754188
return null;
41764189
}
4190+
checkFromToIndex(startIndex, endIndex, array.length);
41774191
final int count = endIndex - startIndex;
41784192
if (count <= 0) {
41794193
return EMPTY;
@@ -4251,6 +4265,7 @@ public static String join(final int[] array, final char delimiter, final int sta
42514265
if (array == null) {
42524266
return null;
42534267
}
4268+
checkFromToIndex(startIndex, endIndex, array.length);
42544269
final int count = endIndex - startIndex;
42554270
if (count <= 0) {
42564271
return EMPTY;
@@ -4491,6 +4506,7 @@ public static String join(final long[] array, final char delimiter, final int st
44914506
if (array == null) {
44924507
return null;
44934508
}
4509+
checkFromToIndex(startIndex, endIndex, array.length);
44944510
final int count = endIndex - startIndex;
44954511
if (count <= 0) {
44964512
return EMPTY;
@@ -4687,6 +4703,7 @@ public static String join(final short[] array, final char delimiter, final int s
46874703
if (array == null) {
46884704
return null;
46894705
}
4706+
checkFromToIndex(startIndex, endIndex, array.length);
46904707
final int count = endIndex - startIndex;
46914708
if (count <= 0) {
46924709
return EMPTY;
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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+
}

0 commit comments

Comments
 (0)