Skip to content

Commit 1375558

Browse files
authored
SQL: Limit how much space some string functions can use (#107333) (#107621)
This will check and fail if certain functions would generate a result exceeding a certain fixed byte size. This prevents an operation/query to fail the entire VM.
1 parent 1d52dcc commit 1375558

File tree

12 files changed

+141
-13
lines changed

12 files changed

+141
-13
lines changed

docs/changelog/107333.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
pr: 107333
2+
summary: Limit how much space some string functions can use
3+
area: SQL
4+
type: breaking
5+
issues: []
6+
breaking:
7+
title: Limit how much space some string functions can use
8+
area: REST API
9+
details: "Before this change, some of the string functions could return a result\
10+
\ of any arbitrary length, which could force the VM to allocate large chunks of\
11+
\ memory or even make it exit. Any user with access to the SQL API can invoke\
12+
\ these functions. This change introduces a limitation of how much memory the\
13+
\ result returned by a function call can consume. The functions affected by this\
14+
\ change are: CONCAT, INSERT, REPEAT, REPLACE and SPACE."
15+
impact: "The affected functions used to return a result of any length. After this\
16+
\ change, a result can no longer exceed 1MB in length. Note that this is a bytes\
17+
\ length, the character count may be lower."
18+
notable: false

docs/reference/sql/functions/string.asciidoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ CONCAT(
109109

110110
*Description*: Returns a character string that is the result of concatenating `string_exp1` to `string_exp2`.
111111

112+
The resulting string cannot exceed a byte length of 1 MB.
113+
112114
[source, sql]
113115
--------------------------------------------------
114116
include-tagged::{sql-specs}/docs/docs.csv-spec[stringConcat]
@@ -137,6 +139,8 @@ INSERT(
137139

138140
*Description*: Returns a string where `length` characters have been deleted from `source`, beginning at `start`, and where `replacement` has been inserted into `source`, beginning at `start`.
139141

142+
The resulting string cannot exceed a byte length of 1 MB.
143+
140144
[source, sql]
141145
--------------------------------------------------
142146
include-tagged::{sql-specs}/docs/docs.csv-spec[stringInsert]
@@ -330,6 +334,8 @@ REPEAT(
330334

331335
*Description*: Returns a character string composed of `string_exp` repeated `count` times.
332336

337+
The resulting string cannot exceed a byte length of 1 MB.
338+
333339
[source, sql]
334340
--------------------------------------------------
335341
include-tagged::{sql-specs}/docs/docs.csv-spec[stringRepeat]
@@ -356,6 +362,8 @@ REPLACE(
356362

357363
*Description*: Search `source` for occurrences of `pattern`, and replace with `replacement`.
358364

365+
The resulting string cannot exceed a byte length of 1 MB.
366+
359367
[source, sql]
360368
--------------------------------------------------
361369
include-tagged::{sql-specs}/docs/docs.csv-spec[stringReplace]
@@ -423,6 +431,8 @@ SPACE(count) <1>
423431

424432
*Description*: Returns a character string consisting of `count` spaces.
425433

434+
The resulting string cannot exceed a byte length of 1 MB.
435+
426436
[source, sql]
427437
--------------------------------------------------
428438
include-tagged::{sql-specs}/docs/docs.csv-spec[stringSpace]

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessor.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.io.IOException;
1717
import java.util.function.BiFunction;
1818

19+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.checkResultLength;
20+
1921
/**
2022
* Processor class covering string manipulating functions that have the first parameter as string,
2123
* second parameter as numeric and a string result.
@@ -42,12 +44,8 @@ public enum BinaryStringNumericOperation implements BiFunction<String, Number, S
4244
if (i <= 0) {
4345
return null;
4446
}
45-
46-
StringBuilder sb = new StringBuilder(s.length() * i);
47-
for (int j = 0; j < i; j++) {
48-
sb.append(s);
49-
}
50-
return sb.toString();
47+
checkResultLength(s.length() * c.longValue()); // mul is safe: c's checked by doProcess() to be within Integer's range
48+
return s.repeat(i);
5149
});
5250

5351
BinaryStringNumericOperation(BiFunction<String, Number, String> op) {

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatFunctionProcessor.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import java.io.IOException;
1717
import java.util.Objects;
1818

19+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.checkResultLength;
20+
1921
public class ConcatFunctionProcessor extends BinaryProcessor {
2022

2123
public static final String NAME = "scon";
@@ -62,7 +64,10 @@ public static Object process(Object source1, Object source2) {
6264
throw new SqlIllegalArgumentException("A string/char is required; received [{}]", source2);
6365
}
6466

65-
return source1.toString().concat(source2.toString());
67+
String str1 = source1.toString();
68+
String str2 = source2.toString();
69+
checkResultLength(str1.length() + str2.length());
70+
return str1.concat(str2);
6671
}
6772

6873
@Override

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertFunctionProcessor.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import java.io.IOException;
1616
import java.util.Objects;
1717

18+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.checkResultLength;
19+
1820
public class InsertFunctionProcessor implements Processor {
1921

2022
private final Processor input, start, length, replacement;
@@ -71,7 +73,9 @@ public static Object doProcess(Object input, Object start, Object length, Object
7173
StringBuilder sb = new StringBuilder(input.toString());
7274
String replString = (replacement.toString());
7375

74-
return sb.replace(realStart, realStart + ((Number) length).intValue(), replString).toString();
76+
int cutLength = ((Number) length).intValue();
77+
checkResultLength(sb.length() - cutLength + replString.length());
78+
return sb.replace(realStart, realStart + cutLength, replString).toString();
7579
}
7680

7781
@Override

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ReplaceFunctionProcessor.java

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,20 @@ public static Object doProcess(Object input, Object pattern, Object replacement)
5858
throw new SqlIllegalArgumentException("A string/char is required; received [{}]", replacement);
5959
}
6060

61-
return Strings.replace(
62-
input instanceof Character ? input.toString() : (String) input,
63-
pattern instanceof Character ? pattern.toString() : (String) pattern,
64-
replacement instanceof Character ? replacement.toString() : (String) replacement
65-
);
61+
String inputStr = input instanceof Character ? input.toString() : (String) input;
62+
String patternStr = pattern instanceof Character ? pattern.toString() : (String) pattern;
63+
String replacementStr = replacement instanceof Character ? replacement.toString() : (String) replacement;
64+
checkResultLength(inputStr, patternStr, replacementStr);
65+
return Strings.replace(inputStr, patternStr, replacementStr);
66+
}
67+
68+
private static void checkResultLength(String input, String pattern, String replacement) {
69+
int patternLen = pattern.length();
70+
long matches = 0;
71+
for (int i = input.indexOf(pattern); i >= 0; i = input.indexOf(pattern, i + patternLen)) {
72+
matches++;
73+
}
74+
StringProcessor.checkResultLength(input.length() + matches * (replacement.length() - patternLen));
6675
}
6776

6877
@Override

x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/StringProcessor.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
import java.util.Locale;
1818
import java.util.function.Function;
1919

20+
import static org.elasticsearch.common.unit.ByteSizeUnit.MB;
21+
2022
public class StringProcessor implements Processor {
2123

24+
static final long MAX_RESULT_LENGTH = MB.toBytes(1);
25+
2226
private interface StringFunction<R> {
2327
default R apply(Object o) {
2428
if ((o instanceof String || o instanceof Character) == false) {
@@ -60,6 +64,7 @@ public enum StringOperation {
6064
if (i < 0) {
6165
return null;
6266
}
67+
checkResultLength(n.longValue());
6368
char[] spaces = new char[i];
6469
char whitespace = ' ';
6570
Arrays.fill(spaces, whitespace);
@@ -125,6 +130,17 @@ StringOperation processor() {
125130
return processor;
126131
}
127132

133+
static void checkResultLength(long needed) {
134+
if (needed > MAX_RESULT_LENGTH) {
135+
throw new SqlIllegalArgumentException(
136+
"Required result length [{}] exceeds implementation limit [{}] bytes",
137+
needed,
138+
MAX_RESULT_LENGTH
139+
);
140+
}
141+
142+
}
143+
128144
@Override
129145
public boolean equals(Object obj) {
130146
if (obj == null || obj.getClass() != getClass()) {

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/BinaryStringNumericProcessorTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l;
2020
import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
21+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringFunctionProcessorTests.maxResultLengthTest;
22+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.MAX_RESULT_LENGTH;
2123

2224
public class BinaryStringNumericProcessorTests extends AbstractWireSerializingTestCase<BinaryStringNumericProcessor> {
2325

@@ -150,6 +152,10 @@ public void testRepeatFunctionWithEdgeCases() {
150152
assertNull(new Repeat(EMPTY, l("foo"), l(-1)).makePipe().asProcessor().process(null));
151153
assertNull(new Repeat(EMPTY, l("foo"), l(0)).makePipe().asProcessor().process(null));
152154
assertNull(new Repeat(EMPTY, l('f'), l(Integer.MIN_VALUE)).makePipe().asProcessor().process(null));
155+
assertEquals(
156+
MAX_RESULT_LENGTH,
157+
new Repeat(EMPTY, l('f'), l(MAX_RESULT_LENGTH)).makePipe().asProcessor().process(null).toString().length()
158+
);
153159
}
154160

155161
public void testRepeatFunctionInputsValidation() {
@@ -179,5 +185,14 @@ public void testRepeatFunctionInputsValidation() {
179185

180186
e = expectThrows(InvalidArgumentException.class, () -> new Repeat(EMPTY, l("foo"), l(1.0)).makePipe().asProcessor().process(null));
181187
assertEquals("A fixed point number is required for [count]; received [java.lang.Double]", e.getMessage());
188+
189+
maxResultLengthTest(
190+
MAX_RESULT_LENGTH + 1,
191+
() -> new Repeat(EMPTY, l("f"), l(MAX_RESULT_LENGTH + 1)).makePipe().asProcessor().process(null)
192+
);
193+
194+
String str = "foo";
195+
long count = (MAX_RESULT_LENGTH / str.length()) + 1;
196+
maxResultLengthTest(count * str.length(), () -> new Repeat(EMPTY, l(str), l(count)).makePipe().asProcessor().process(null));
182197
}
183198
}

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/ConcatProcessorTests.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l;
1818
import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
19+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringFunctionProcessorTests.maxResultLengthTest;
20+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.MAX_RESULT_LENGTH;
1921

2022
public class ConcatProcessorTests extends AbstractWireSerializingTestCase<ConcatFunctionProcessor> {
2123

@@ -65,4 +67,11 @@ public void testConcatFunctionInputsValidation() {
6567
);
6668
assertEquals("A string/char is required; received [3]", siae.getMessage());
6769
}
70+
71+
public void testMaxResultLength() {
72+
String str = "a".repeat((int) MAX_RESULT_LENGTH - 1);
73+
assertEquals(MAX_RESULT_LENGTH, new Concat(EMPTY, l(str), l("b")).makePipe().asProcessor().process(null).toString().length());
74+
75+
maxResultLengthTest(MAX_RESULT_LENGTH + 1, () -> new Concat(EMPTY, l(str), l("bb")).makePipe().asProcessor().process(null));
76+
}
6877
}

x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/expression/function/scalar/string/InsertProcessorTests.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import static org.elasticsearch.xpack.ql.expression.function.scalar.FunctionTestUtils.l;
1919
import static org.elasticsearch.xpack.ql.tree.Source.EMPTY;
20+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringFunctionProcessorTests.maxResultLengthTest;
21+
import static org.elasticsearch.xpack.sql.expression.function.scalar.string.StringProcessor.MAX_RESULT_LENGTH;
2022

2123
public class InsertProcessorTests extends AbstractWireSerializingTestCase<InsertFunctionProcessor> {
2224

@@ -123,5 +125,21 @@ public void testInsertInputsValidation() {
123125
() -> new Insert(EMPTY, l("foobar"), l(1), l((long) Integer.MAX_VALUE + 1), l("bar")).makePipe().asProcessor().process(null)
124126
);
125127
assertEquals("[length] out of the allowed range [0, 2147483647], received [2147483648]", e.getMessage());
128+
129+
String str = "a".repeat((int) MAX_RESULT_LENGTH);
130+
String replaceWith = "bar";
131+
assertEquals(
132+
MAX_RESULT_LENGTH,
133+
new Insert(EMPTY, l(str), l(1), l(replaceWith.length()), l(replaceWith)).makePipe()
134+
.asProcessor()
135+
.process(null)
136+
.toString()
137+
.length()
138+
);
139+
140+
maxResultLengthTest(
141+
MAX_RESULT_LENGTH + 1,
142+
() -> new Insert(EMPTY, l(str), l(1), l(replaceWith.length() - 1), l(replaceWith)).makePipe().asProcessor().process(null)
143+
);
126144
}
127145
}

0 commit comments

Comments
 (0)