Skip to content

Commit b65a3d0

Browse files
authored
SQL: Limit how much space some string functions can use (#107333) (#107632)
* SQL: Limit how much space some string functions can use (#107333) 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. (cherry picked from commit f1bcb33) * Fix use of forbidden APIs * Style
1 parent 2154d32 commit b65a3d0

File tree

12 files changed

+150
-8
lines changed

12 files changed

+150
-8
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: 3 additions & 1 deletion
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,7 +44,7 @@ public enum BinaryStringNumericOperation implements BiFunction<String, Number, S
4244
if (i <= 0) {
4345
return null;
4446
}
45-
47+
checkResultLength(s.length() * c.longValue()); // mul is safe: c's checked by doProcess() to be within Integer's range
4648
StringBuilder sb = new StringBuilder(s.length() * i);
4749
for (int j = 0; j < i; j++) {
4850
sb.append(s);

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
@@ -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 BinaryStringNumericProcessorTests extends AbstractWireSerializingTestCase<BinaryStringNumericProcessor> {
2224

@@ -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() {
@@ -182,5 +188,14 @@ public void testRepeatFunctionInputsValidation() {
182188
() -> new Repeat(EMPTY, l("foo"), l(1.0)).makePipe().asProcessor().process(null)
183189
);
184190
assertEquals("A fixed point number is required for [count]; received [java.lang.Double]", siae.getMessage());
191+
192+
maxResultLengthTest(
193+
MAX_RESULT_LENGTH + 1,
194+
() -> new Repeat(EMPTY, l("f"), l(MAX_RESULT_LENGTH + 1)).makePipe().asProcessor().process(null)
195+
);
196+
197+
String str = "foo";
198+
long count = (MAX_RESULT_LENGTH / str.length()) + 1;
199+
maxResultLengthTest(count * str.length(), () -> new Repeat(EMPTY, l(str), l(count)).makePipe().asProcessor().process(null));
185200
}
186201
}

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

Lines changed: 17 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

@@ -60,4 +62,19 @@ public void testConcatFunctionInputsValidation() {
6062
);
6163
assertEquals("A string/char is required; received [3]", siae.getMessage());
6264
}
65+
66+
public void testMaxResultLength() {
67+
String str = repeat('a', (int) MAX_RESULT_LENGTH - 1);
68+
assertEquals(MAX_RESULT_LENGTH, new Concat(EMPTY, l(str), l("b")).makePipe().asProcessor().process(null).toString().length());
69+
70+
maxResultLengthTest(MAX_RESULT_LENGTH + 1, () -> new Concat(EMPTY, l(str), l("bb")).makePipe().asProcessor().process(null));
71+
}
72+
73+
public static String repeat(char ch, int count) {
74+
StringBuilder sb = new StringBuilder(count);
75+
for (int i = 0; i < count; i++) {
76+
sb.append(ch);
77+
}
78+
return sb.toString();
79+
}
6380
}

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
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.ConcatProcessorTests.repeat;
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;
1922

2023
public class InsertProcessorTests extends AbstractWireSerializingTestCase<InsertFunctionProcessor> {
2124

@@ -117,5 +120,21 @@ public void testInsertInputsValidation() {
117120
() -> new Insert(EMPTY, l("foobar"), l(1), l((long) Integer.MAX_VALUE + 1), l("bar")).makePipe().asProcessor().process(null)
118121
);
119122
assertEquals("[length] out of the allowed range [0, 2147483647], received [2147483648]", siae.getMessage());
123+
124+
String str = repeat('a', (int) MAX_RESULT_LENGTH);
125+
String replaceWith = "bar";
126+
assertEquals(
127+
MAX_RESULT_LENGTH,
128+
new Insert(EMPTY, l(str), l(1), l(replaceWith.length()), l(replaceWith)).makePipe()
129+
.asProcessor()
130+
.process(null)
131+
.toString()
132+
.length()
133+
);
134+
135+
maxResultLengthTest(
136+
MAX_RESULT_LENGTH + 1,
137+
() -> new Insert(EMPTY, l(str), l(1), l(replaceWith.length() - 1), l(replaceWith)).makePipe().asProcessor().process(null)
138+
);
120139
}
121140
}

0 commit comments

Comments
 (0)