Skip to content

Commit 4d3f7a7

Browse files
authored
Ensure we protect Collections obtained from scripts from self-referencing (#28335)
Self referencing maps can cause SOE if they are iterated ie. in their toString methods. This chance adds some protected to the usage of those collections.
1 parent b2ce994 commit 4d3f7a7

File tree

13 files changed

+128
-48
lines changed

13 files changed

+128
-48
lines changed

modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/CustomReflectionObjectHandler.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.script.mustache;
2121

2222
import com.github.mustachejava.reflect.ReflectionObjectHandler;
23+
import org.elasticsearch.common.util.CollectionUtils;
2324
import org.elasticsearch.common.util.iterable.Iterables;
2425

2526
import java.lang.reflect.Array;
@@ -154,4 +155,9 @@ public Iterator<Object> iterator() {
154155
}
155156
}
156157

158+
@Override
159+
public String stringify(Object object) {
160+
CollectionUtils.ensureNoSelfReferences(object);
161+
return super.stringify(object);
162+
}
157163
}

modules/lang-painless/src/test/resources/rest-api-spec/test/painless/15_update.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,4 @@
137137

138138
- match: { error.root_cause.0.type: "remote_transport_exception" }
139139
- match: { error.type: "illegal_argument_exception" }
140-
- match: { error.reason: "Object has already been built and is self-referencing itself" }
140+
- match: { error.reason: "Iterable object is self-referencing itself" }

modules/lang-painless/src/test/resources/rest-api-spec/test/painless/30_search.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,3 +406,39 @@
406406
- match: { hits.hits.0._score: 1.0 }
407407
- match: { aggregations.value_agg.buckets.0.key: 2 }
408408
- match: { aggregations.value_agg.buckets.0.doc_count: 1 }
409+
410+
---
411+
"Return self-referencing map":
412+
- do:
413+
indices.create:
414+
index: test
415+
body:
416+
settings:
417+
number_of_shards: "1"
418+
419+
- do:
420+
index:
421+
index: test
422+
type: test
423+
id: 1
424+
body: { "genre": 1 }
425+
426+
- do:
427+
indices.refresh: {}
428+
429+
- do:
430+
catch: bad_request
431+
index: test
432+
search:
433+
body:
434+
aggs:
435+
genre:
436+
terms:
437+
script:
438+
lang: painless
439+
source: "def x = [:] ; def y = [:] ; x.a = y ; y.a = x ; return x"
440+
441+
- match: { error.root_cause.0.type: "illegal_argument_exception" }
442+
- match: { error.root_cause.0.reason: "Iterable object is self-referencing itself" }
443+
- match: { error.type: "search_phase_execution_exception" }
444+
- match: { error.reason: "all shards failed" }

server/src/main/java/org/elasticsearch/common/util/CollectionUtils.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,20 @@
1919

2020
package org.elasticsearch.common.util;
2121

22+
import java.nio.file.Path;
2223
import java.util.AbstractList;
2324
import java.util.ArrayList;
2425
import java.util.Arrays;
2526
import java.util.Collection;
2627
import java.util.Collections;
2728
import java.util.Comparator;
29+
import java.util.IdentityHashMap;
2830
import java.util.LinkedList;
2931
import java.util.List;
32+
import java.util.Map;
3033
import java.util.Objects;
3134
import java.util.RandomAccess;
35+
import java.util.Set;
3236

3337
import com.carrotsearch.hppc.ObjectArrayList;
3438
import org.apache.lucene.util.BytesRef;
@@ -221,6 +225,40 @@ public static int[] toArray(Collection<Integer> ints) {
221225
return ints.stream().mapToInt(s -> s).toArray();
222226
}
223227

228+
public static void ensureNoSelfReferences(Object value) {
229+
Iterable<?> it = convert(value);
230+
if (it != null) {
231+
ensureNoSelfReferences(it, value, Collections.newSetFromMap(new IdentityHashMap<>()));
232+
}
233+
}
234+
235+
private static Iterable<?> convert(Object value) {
236+
if (value == null) {
237+
return null;
238+
}
239+
if (value instanceof Map) {
240+
return ((Map<?,?>) value).values();
241+
} else if ((value instanceof Iterable) && (value instanceof Path == false)) {
242+
return (Iterable<?>) value;
243+
} else if (value instanceof Object[]) {
244+
return Arrays.asList((Object[]) value);
245+
} else {
246+
return null;
247+
}
248+
}
249+
250+
private static void ensureNoSelfReferences(final Iterable<?> value, Object originalReference, final Set<Object> ancestors) {
251+
if (value != null) {
252+
if (ancestors.add(originalReference) == false) {
253+
throw new IllegalArgumentException("Iterable object is self-referencing itself");
254+
}
255+
for (Object o : value) {
256+
ensureNoSelfReferences(convert(o), o, ancestors);
257+
}
258+
ancestors.remove(originalReference);
259+
}
260+
}
261+
224262
private static class RotatedList<T> extends AbstractList<T> implements RandomAccess {
225263

226264
private final List<T> in;

server/src/main/java/org/elasticsearch/common/xcontent/XContentBuilder.java

Lines changed: 3 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.elasticsearch.common.text.Text;
2929
import org.elasticsearch.common.unit.ByteSizeValue;
3030
import org.elasticsearch.common.unit.TimeValue;
31+
import org.elasticsearch.common.util.CollectionUtils;
3132
import org.joda.time.DateTimeZone;
3233
import org.joda.time.ReadableInstant;
3334
import org.joda.time.format.DateTimeFormatter;
@@ -43,7 +44,6 @@
4344
import java.util.Collections;
4445
import java.util.Date;
4546
import java.util.HashMap;
46-
import java.util.IdentityHashMap;
4747
import java.util.Locale;
4848
import java.util.Map;
4949
import java.util.Objects;
@@ -780,7 +780,6 @@ private XContentBuilder values(Object[] values, boolean ensureNoSelfReferences)
780780
if (values == null) {
781781
return nullValue();
782782
}
783-
784783
return value(Arrays.asList(values), ensureNoSelfReferences);
785784
}
786785

@@ -865,7 +864,7 @@ private XContentBuilder map(Map<String, ?> values, boolean ensureNoSelfReference
865864
// checks that the map does not contain references to itself because
866865
// iterating over map entries will cause a stackoverflow error
867866
if (ensureNoSelfReferences) {
868-
ensureNoSelfReferences(values);
867+
CollectionUtils.ensureNoSelfReferences(values);
869868
}
870869

871870
startObject();
@@ -894,9 +893,8 @@ private XContentBuilder value(Iterable<?> values, boolean ensureNoSelfReferences
894893
// checks that the iterable does not contain references to itself because
895894
// iterating over entries will cause a stackoverflow error
896895
if (ensureNoSelfReferences) {
897-
ensureNoSelfReferences(values);
896+
CollectionUtils.ensureNoSelfReferences(values);
898897
}
899-
900898
startArray();
901899
for (Object value : values) {
902900
// pass ensureNoSelfReferences=false as we already performed the check at a higher level
@@ -1067,32 +1065,4 @@ static void ensureNotNull(Object value, String message) {
10671065
throw new IllegalArgumentException(message);
10681066
}
10691067
}
1070-
1071-
static void ensureNoSelfReferences(Object value) {
1072-
ensureNoSelfReferences(value, Collections.newSetFromMap(new IdentityHashMap<>()));
1073-
}
1074-
1075-
private static void ensureNoSelfReferences(final Object value, final Set<Object> ancestors) {
1076-
if (value != null) {
1077-
1078-
Iterable<?> it;
1079-
if (value instanceof Map) {
1080-
it = ((Map<?,?>) value).values();
1081-
} else if ((value instanceof Iterable) && (value instanceof Path == false)) {
1082-
it = (Iterable<?>) value;
1083-
} else if (value instanceof Object[]) {
1084-
it = Arrays.asList((Object[]) value);
1085-
} else {
1086-
return;
1087-
}
1088-
1089-
if (ancestors.add(value) == false) {
1090-
throw new IllegalArgumentException("Object has already been built and is self-referencing itself");
1091-
}
1092-
for (Object o : it) {
1093-
ensureNoSelfReferences(o, ancestors);
1094-
}
1095-
ancestors.remove(value);
1096-
}
1097-
}
10981068
}

server/src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package org.elasticsearch.search.aggregations.metrics.scripted;
2121

2222
import org.apache.lucene.index.LeafReaderContext;
23+
import org.elasticsearch.common.util.CollectionUtils;
2324
import org.elasticsearch.script.ExecutableScript;
2425
import org.elasticsearch.script.Script;
2526
import org.elasticsearch.script.SearchScript;
@@ -77,6 +78,7 @@ public InternalAggregation buildAggregation(long owningBucketOrdinal) {
7778
Object aggregation;
7879
if (combineScript != null) {
7980
aggregation = combineScript.run();
81+
CollectionUtils.ensureNoSelfReferences(aggregation);
8082
} else {
8183
aggregation = params.get("_agg");
8284
}

server/src/main/java/org/elasticsearch/search/aggregations/pipeline/bucketscript/BucketScriptPipelineAggregator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,11 @@ public InternalAggregation reduce(InternalAggregation aggregation, ReduceContext
112112
} else {
113113
ExecutableScript executableScript = factory.newInstance(vars);
114114
Object returned = executableScript.run();
115+
// no need to check for self references since only numbers are valid
115116
if (returned == null) {
116117
newBuckets.add(bucket);
117118
} else {
118-
if (!(returned instanceof Number)) {
119+
if ((returned instanceof Number) == false) {
119120
throw new AggregationExecutionException("series_arithmetic script for reducer [" + name()
120121
+ "] must return a Number");
121122
}

server/src/main/java/org/elasticsearch/search/aggregations/support/ValuesSource.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.lucene.util.Bits;
3131
import org.apache.lucene.util.BytesRef;
3232
import org.elasticsearch.common.lucene.ScorerAware;
33+
import org.elasticsearch.common.util.CollectionUtils;
3334
import org.elasticsearch.index.fielddata.AbstractSortingNumericDocValues;
3435
import org.elasticsearch.index.fielddata.AtomicOrdinalsFieldData;
3536
import org.elasticsearch.index.fielddata.IndexFieldData;
@@ -460,7 +461,9 @@ public boolean advanceExact(int doc) throws IOException {
460461
for (int i = 0; i < count; ++i) {
461462
final BytesRef value = bytesValues.nextValue();
462463
script.setNextAggregationValue(value.utf8ToString());
463-
values[i].copyChars(script.run().toString());
464+
Object run = script.run();
465+
CollectionUtils.ensureNoSelfReferences(run);
466+
values[i].copyChars(run.toString());
464467
}
465468
sort();
466469
return true;

server/src/main/java/org/elasticsearch/search/aggregations/support/values/ScriptBytesValues.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.apache.lucene.search.Scorer;
2222
import org.elasticsearch.common.lucene.ScorerAware;
23+
import org.elasticsearch.common.util.CollectionUtils;
2324
import org.elasticsearch.index.fielddata.SortedBinaryDocValues;
2425
import org.elasticsearch.index.fielddata.SortingBinaryDocValues;
2526
import org.elasticsearch.script.SearchScript;
@@ -44,6 +45,7 @@ private void set(int i, Object o) {
4445
if (o == null) {
4546
values[i].clear();
4647
} else {
48+
CollectionUtils.ensureNoSelfReferences(o);
4749
values[i].copyChars(o.toString());
4850
}
4951
}

server/src/main/java/org/elasticsearch/search/fetch/subphase/ScriptFieldsFetchSubPhase.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.apache.lucene.index.LeafReaderContext;
2323
import org.apache.lucene.index.ReaderUtil;
2424
import org.elasticsearch.common.document.DocumentField;
25+
import org.elasticsearch.common.util.CollectionUtils;
2526
import org.elasticsearch.script.SearchScript;
2627
import org.elasticsearch.search.SearchHit;
2728
import org.elasticsearch.search.fetch.FetchSubPhase;
@@ -64,6 +65,7 @@ public void hitsExecute(SearchContext context, SearchHit[] hits) throws IOExcept
6465
final Object value;
6566
try {
6667
value = leafScripts[i].run();
68+
CollectionUtils.ensureNoSelfReferences(value);
6769
} catch (RuntimeException e) {
6870
if (scriptFields.get(i).ignoreException()) {
6971
continue;

server/src/main/java/org/elasticsearch/search/sort/ScriptSortBuilder.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.elasticsearch.common.io.stream.Writeable;
3333
import org.elasticsearch.common.logging.DeprecationLogger;
3434
import org.elasticsearch.common.logging.Loggers;
35+
import org.elasticsearch.common.util.CollectionUtils;
3536
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
3637
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
3738
import org.elasticsearch.common.xcontent.XContentBuilder;
@@ -341,7 +342,9 @@ public boolean advanceExact(int doc) throws IOException {
341342
}
342343
@Override
343344
public BytesRef binaryValue() {
344-
spare.copyChars(leafScript.run().toString());
345+
final Object run = leafScript.run();
346+
CollectionUtils.ensureNoSelfReferences(run);
347+
spare.copyChars(run.toString());
345348
return spare.get();
346349
}
347350
};

server/src/test/java/org/elasticsearch/common/util/CollectionUtilsTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,21 @@
2525
import org.apache.lucene.util.Counter;
2626
import org.elasticsearch.test.ESTestCase;
2727

28+
import java.io.IOException;
2829
import java.util.ArrayList;
2930
import java.util.Arrays;
3031
import java.util.Collections;
32+
import java.util.HashMap;
3133
import java.util.HashSet;
3234
import java.util.Iterator;
3335
import java.util.List;
36+
import java.util.Map;
3437
import java.util.SortedSet;
3538
import java.util.TreeSet;
3639

40+
import static java.util.Collections.emptyMap;
3741
import static org.elasticsearch.common.util.CollectionUtils.eagerPartition;
42+
import static org.hamcrest.Matchers.containsString;
3843
import static org.hamcrest.Matchers.equalTo;
3944
import static org.hamcrest.Matchers.is;
4045

@@ -176,4 +181,15 @@ public void testPerfectPartition() {
176181
eagerPartition(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 6)
177182
);
178183
}
184+
185+
public void testEnsureNoSelfReferences() {
186+
CollectionUtils.ensureNoSelfReferences(emptyMap());
187+
CollectionUtils.ensureNoSelfReferences(null);
188+
189+
Map<String, Object> map = new HashMap<>();
190+
map.put("field", map);
191+
192+
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> CollectionUtils.ensureNoSelfReferences(map));
193+
assertThat(e.getMessage(), containsString("Iterable object is self-referencing itself"));
194+
}
179195
}

0 commit comments

Comments
 (0)