Skip to content

Commit cd08b1e

Browse files
authored
Improve painless error wrapping (#100872) (#102394)
* Improve painless error wrapping (#100872) Painless sandboxes some errors from Java for which it can recover. These errors are wrapped within a ScriptException. However, retaining the error as a cause can be confusing when walking the error chain. This commit wraps the error so that the real error type does not appear, but maintains the same error message in xcontent serialized form. * fix compile
1 parent 4992962 commit cd08b1e

File tree

8 files changed

+167
-18
lines changed

8 files changed

+167
-18
lines changed

docs/changelog/100872.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 100872
2+
summary: Improve painless error wrapping
3+
area: Infra/Scripting
4+
type: bug
5+
issues: []
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.painless;
10+
11+
import org.elasticsearch.ElasticsearchException;
12+
import org.elasticsearch.xcontent.XContentBuilder;
13+
14+
import java.io.IOException;
15+
import java.util.List;
16+
17+
/**
18+
* A wrapper for Error which hides the underlying Error from the exception cause chain.
19+
* <p>
20+
* Only errors which should be sandboxed and not cause the node to crash are wrapped.
21+
*/
22+
class ErrorCauseWrapper extends ElasticsearchException {
23+
24+
private static final List<Class<? extends Error>> wrappedErrors = org.elasticsearch.core.List.of(
25+
PainlessError.class,
26+
OutOfMemoryError.class,
27+
StackOverflowError.class,
28+
LinkageError.class
29+
);
30+
31+
final Throwable realCause;
32+
33+
private ErrorCauseWrapper(Throwable realCause) {
34+
super(realCause.getMessage());
35+
this.realCause = realCause;
36+
}
37+
38+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
39+
builder.field("type", getExceptionName(realCause));
40+
builder.field("reason", realCause.getMessage());
41+
return builder;
42+
}
43+
44+
static Throwable maybeWrap(Throwable t) {
45+
if (wrappedErrors.contains(t.getClass())) {
46+
return new ErrorCauseWrapper(t);
47+
}
48+
return t;
49+
}
50+
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScript.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,15 @@ default ScriptException convertToScriptException(Throwable t, Map<String, List<S
8282
scriptStack.add(element.toString());
8383
}
8484
}
85-
ScriptException scriptException = new ScriptException("runtime error", t, scriptStack, getName(), PainlessScriptEngine.NAME, pos);
85+
Throwable cause = ErrorCauseWrapper.maybeWrap(t);
86+
ScriptException scriptException = new ScriptException(
87+
"runtime error",
88+
cause,
89+
scriptStack,
90+
getName(),
91+
PainlessScriptEngine.NAME,
92+
pos
93+
);
8694
for (Map.Entry<String, List<String>> entry : extraMetadata.entrySet()) {
8795
scriptException.addMetadata(entry.getKey(), entry.getValue());
8896
}

modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,8 @@ private ScriptException convertToScriptException(String scriptSource, Throwable
492492
break;
493493
}
494494
}
495-
throw new ScriptException("compile error", t, scriptStack, scriptSource, PainlessScriptEngine.NAME, pos);
495+
Throwable cause = ErrorCauseWrapper.maybeWrap(t);
496+
throw new ScriptException("compile error", cause, scriptStack, scriptSource, PainlessScriptEngine.NAME, pos);
496497
}
497498

498499
// very simple heuristic: +/- 25 chars. can be improved later.

modules/lang-painless/src/test/java/org/elasticsearch/painless/BasicStatementTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,8 +671,8 @@ public void testNoLoopCounterInForEach() {
671671
int total = (int) exec("int total = 0; for (int value : params['values']) total += value; return total", params, false);
672672
assertEquals(total, 20000000);
673673

674-
PainlessError pe = expectScriptThrows(
675-
PainlessError.class,
674+
ErrorCauseWrapper pe = expectScriptThrows(
675+
ErrorCauseWrapper.class,
676676
() -> exec(
677677
"int total = 0; for (int value = 0; value < params['values'].length; ++value) total += value; return total",
678678
params,
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0 and the Server Side Public License, v 1; you may not use this file except
5+
* in compliance with, at your election, the Elastic License 2.0 or the Server
6+
* Side Public License, v 1.
7+
*/
8+
9+
package org.elasticsearch.painless;
10+
11+
import org.elasticsearch.test.ESTestCase;
12+
import org.elasticsearch.xcontent.ToXContent;
13+
import org.elasticsearch.xcontent.XContentBuilder;
14+
import org.elasticsearch.xcontent.XContentFactory;
15+
import org.elasticsearch.xcontent.XContentParser;
16+
import org.elasticsearch.xcontent.json.JsonXContent;
17+
18+
import java.io.ByteArrayOutputStream;
19+
import java.io.IOException;
20+
import java.util.Map;
21+
22+
import static org.hamcrest.Matchers.hasEntry;
23+
import static org.hamcrest.Matchers.instanceOf;
24+
import static org.hamcrest.Matchers.is;
25+
import static org.hamcrest.Matchers.nullValue;
26+
27+
public class ErrorCauseWrapperTests extends ESTestCase {
28+
29+
ErrorCauseWrapper assertWraps(Error realError) {
30+
Throwable e = ErrorCauseWrapper.maybeWrap(realError);
31+
assertThat(e.getCause(), nullValue());
32+
assertThat(e, instanceOf(ErrorCauseWrapper.class));
33+
ErrorCauseWrapper wrapper = (ErrorCauseWrapper) e;
34+
assertThat(wrapper.realCause, is(realError));
35+
return wrapper;
36+
}
37+
38+
public void testOutOfMemoryError() {
39+
assertWraps(new OutOfMemoryError("oom"));
40+
}
41+
42+
public void testStackOverflowError() {
43+
assertWraps(new StackOverflowError("soe"));
44+
}
45+
46+
public void testLinkageError() {
47+
assertWraps(new LinkageError("le"));
48+
}
49+
50+
public void testPainlessError() {
51+
assertWraps(new PainlessError("pe"));
52+
}
53+
54+
public void testNotWrapped() {
55+
AssertionError realError = new AssertionError("not wrapped");
56+
Throwable e = ErrorCauseWrapper.maybeWrap(realError);
57+
assertThat(e, is(realError));
58+
}
59+
60+
public void testXContent() throws IOException {
61+
ErrorCauseWrapper e = assertWraps(new PainlessError("some error"));
62+
63+
ByteArrayOutputStream output = new ByteArrayOutputStream();
64+
XContentBuilder builder = XContentFactory.jsonBuilder(output);
65+
builder.startObject();
66+
e.toXContent(builder, ToXContent.EMPTY_PARAMS);
67+
builder.endObject();
68+
builder.flush();
69+
70+
try (XContentParser parser = createParser(JsonXContent.jsonXContent, output.toByteArray())) {
71+
Map<String, String> content = parser.mapStrings();
72+
assertThat(content, hasEntry("type", "painless_error"));
73+
assertThat(content, hasEntry("reason", "some error"));
74+
}
75+
76+
}
77+
}

modules/lang-painless/src/test/java/org/elasticsearch/painless/FunctionTests.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package org.elasticsearch.painless;
1010

1111
import static org.hamcrest.Matchers.containsString;
12+
import static org.hamcrest.Matchers.equalTo;
1213

1314
public class FunctionTests extends ScriptTestCase {
1415

@@ -75,11 +76,12 @@ public void testBadCastFromMethod() {
7576
}
7677

7778
public void testInfiniteLoop() {
78-
Error expected = expectScriptThrows(PainlessError.class, () -> { exec("void test() {boolean x = true; while (x) {}} test()"); });
79-
assertThat(
80-
expected.getMessage(),
81-
containsString("The maximum number of statements that can be executed in a loop has been reached.")
79+
ErrorCauseWrapper e = expectScriptThrows(
80+
ErrorCauseWrapper.class,
81+
() -> { exec("void test() {boolean x = true; while (x) {}} test()"); }
8282
);
83+
assertThat(e.realCause.getClass(), equalTo(PainlessError.class));
84+
assertThat(e.getMessage(), containsString("The maximum number of statements that can be executed in a loop has been reached."));
8385
}
8486

8587
public void testReturnVoid() {

modules/lang-painless/src/test/java/org/elasticsearch/painless/WhenThingsGoWrongTests.java

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static java.util.Collections.emptyMap;
2020
import static java.util.Collections.singletonMap;
21+
import static org.hamcrest.Matchers.equalTo;
2122
import static org.hamcrest.Matchers.instanceOf;
2223

2324
public class WhenThingsGoWrongTests extends ScriptTestCase {
@@ -121,34 +122,34 @@ public void testBogusParameter() {
121122
}
122123

123124
public void testInfiniteLoops() {
124-
PainlessError expected = expectScriptThrows(PainlessError.class, () -> { exec("boolean x = true; while (x) {}"); });
125+
ErrorCauseWrapper expected = expectScriptThrows(ErrorCauseWrapper.class, () -> { exec("boolean x = true; while (x) {}"); });
125126
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
126127

127-
expected = expectScriptThrows(PainlessError.class, () -> { exec("while (true) {int y = 5;}"); });
128+
expected = expectScriptThrows(ErrorCauseWrapper.class, () -> { exec("while (true) {int y = 5;}"); });
128129
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
129130

130-
expected = expectScriptThrows(PainlessError.class, () -> { exec("while (true) { boolean x = true; while (x) {} }"); });
131+
expected = expectScriptThrows(ErrorCauseWrapper.class, () -> { exec("while (true) { boolean x = true; while (x) {} }"); });
131132
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
132133

133-
expected = expectScriptThrows(PainlessError.class, () -> {
134+
expected = expectScriptThrows(ErrorCauseWrapper.class, () -> {
134135
exec("while (true) { boolean x = false; while (x) {} }");
135136
fail("should have hit PainlessError");
136137
});
137138
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
138139

139-
expected = expectScriptThrows(PainlessError.class, () -> {
140+
expected = expectScriptThrows(ErrorCauseWrapper.class, () -> {
140141
exec("boolean x = true; for (;x;) {}");
141142
fail("should have hit PainlessError");
142143
});
143144
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
144145

145-
expected = expectScriptThrows(PainlessError.class, () -> {
146+
expected = expectScriptThrows(ErrorCauseWrapper.class, () -> {
146147
exec("for (;;) {int x = 5;}");
147148
fail("should have hit PainlessError");
148149
});
149150
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
150151

151-
expected = expectScriptThrows(PainlessError.class, () -> {
152+
expected = expectScriptThrows(ErrorCauseWrapper.class, () -> {
152153
exec("def x = true; do {int y = 5;} while (x)");
153154
fail("should have hit PainlessError");
154155
});
@@ -165,7 +166,7 @@ public void testLoopLimits() {
165166
// right below limit: ok
166167
exec("for (int x = 0; x < 999999; ++x) {}");
167168

168-
PainlessError expected = expectScriptThrows(PainlessError.class, () -> { exec("for (int x = 0; x < 1000000; ++x) {}"); });
169+
ErrorCauseWrapper expected = expectScriptThrows(ErrorCauseWrapper.class, () -> { exec("for (int x = 0; x < 1000000; ++x) {}"); });
169170
assertTrue(expected.getMessage().contains("The maximum number of statements that can be executed in a loop has been reached."));
170171
}
171172

@@ -211,11 +212,16 @@ public void testBadBoxingCast() {
211212

212213
public void testOutOfMemoryError() {
213214
assumeTrue("test only happens to work for sure on oracle jre", Constants.JAVA_VENDOR.startsWith("Oracle"));
214-
expectScriptThrows(OutOfMemoryError.class, () -> { exec("int[] x = new int[Integer.MAX_VALUE - 1];"); });
215+
ErrorCauseWrapper e = expectScriptThrows(ErrorCauseWrapper.class, () -> { exec("int[] x = new int[Integer.MAX_VALUE - 1];"); });
216+
assertThat(e.realCause.getClass(), equalTo(OutOfMemoryError.class));
215217
}
216218

217219
public void testStackOverflowError() {
218-
expectScriptThrows(StackOverflowError.class, () -> { exec("void recurse(int x, int y) {recurse(x, y)} recurse(1, 2);"); });
220+
ErrorCauseWrapper e = expectScriptThrows(
221+
ErrorCauseWrapper.class,
222+
() -> { exec("void recurse(int x, int y) {recurse(x, y)} recurse(1, 2);"); }
223+
);
224+
assertThat(e.realCause.getClass(), equalTo(StackOverflowError.class));
219225
}
220226

221227
public void testCanNotOverrideRegexEnabled() {

0 commit comments

Comments
 (0)