Skip to content

Commit 99a44a0

Browse files
Fix Infinite Loops in ExceptionsHelper#unwrap (#42716) (#43421)
* Fix Infinite Loops in ExceptionsHelper#unwrap * Keep track of all seen exceptions and break out on loops * Closes #42340
1 parent 39fef83 commit 99a44a0

File tree

3 files changed

+60
-51
lines changed

3 files changed

+60
-51
lines changed

server/src/main/java/org/elasticsearch/ExceptionsHelper.java

+30-40
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@
3838
import java.util.Arrays;
3939
import java.util.Collections;
4040
import java.util.HashSet;
41+
import java.util.IdentityHashMap;
4142
import java.util.LinkedList;
4243
import java.util.List;
4344
import java.util.Objects;
4445
import java.util.Optional;
4546
import java.util.Queue;
4647
import java.util.Set;
48+
import java.util.function.Predicate;
4749
import java.util.stream.Collectors;
4850

4951
public final class ExceptionsHelper {
@@ -185,22 +187,14 @@ public static <T extends Throwable> T useOrSuppress(T first, T second) {
185187
* @return Corruption indicating exception if one is found, otherwise {@code null}
186188
*/
187189
public static IOException unwrapCorruption(Throwable t) {
188-
if (t != null) {
189-
do {
190-
for (Class<?> clazz : CORRUPTION_EXCEPTIONS) {
191-
if (clazz.isInstance(t)) {
192-
return (IOException) t;
193-
}
194-
}
195-
for (Throwable suppressed : t.getSuppressed()) {
196-
IOException corruptionException = unwrapCorruption(suppressed);
197-
if (corruptionException != null) {
198-
return corruptionException;
199-
}
190+
return t == null ? null : ExceptionsHelper.<IOException>unwrapCausesAndSuppressed(t, cause -> {
191+
for (Class<?> clazz : CORRUPTION_EXCEPTIONS) {
192+
if (clazz.isInstance(cause)) {
193+
return true;
200194
}
201-
} while ((t = t.getCause()) != null);
202-
}
203-
return null;
195+
}
196+
return false;
197+
}).orElse(null);
204198
}
205199

206200
/**
@@ -213,7 +207,11 @@ public static IOException unwrapCorruption(Throwable t) {
213207
*/
214208
public static Throwable unwrap(Throwable t, Class<?>... clazzes) {
215209
if (t != null) {
210+
final Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
216211
do {
212+
if (seen.add(t) == false) {
213+
return null;
214+
}
217215
for (Class<?> clazz : clazzes) {
218216
if (clazz.isInstance(t)) {
219217
return t;
@@ -246,33 +244,22 @@ public static boolean reThrowIfNotNull(@Nullable Throwable e) {
246244
return true;
247245
}
248246

249-
static final int MAX_ITERATIONS = 1024;
250-
251-
/**
252-
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
253-
*
254-
* @param cause the root throwable
255-
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
256-
*/
257-
public static Optional<Error> maybeError(final Throwable cause, final Logger logger) {
258-
// early terminate if the cause is already an error
259-
if (cause instanceof Error) {
260-
return Optional.of((Error) cause);
247+
@SuppressWarnings("unchecked")
248+
private static <T extends Throwable> Optional<T> unwrapCausesAndSuppressed(Throwable cause, Predicate<Throwable> predicate) {
249+
if (predicate.test(cause)) {
250+
return Optional.of((T) cause);
261251
}
262252

263253
final Queue<Throwable> queue = new LinkedList<>();
264254
queue.add(cause);
265-
int iterations = 0;
255+
final Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<>());
266256
while (queue.isEmpty() == false) {
267-
iterations++;
268-
// this is a guard against deeply nested or circular chains of exceptions
269-
if (iterations > MAX_ITERATIONS) {
270-
logger.warn("giving up looking for fatal errors", cause);
271-
break;
272-
}
273257
final Throwable current = queue.remove();
274-
if (current instanceof Error) {
275-
return Optional.of((Error) current);
258+
if (seen.add(current) == false) {
259+
continue;
260+
}
261+
if (predicate.test(current)) {
262+
return Optional.of((T) current);
276263
}
277264
Collections.addAll(queue, current.getSuppressed());
278265
if (current.getCause() != null) {
@@ -283,21 +270,24 @@ public static Optional<Error> maybeError(final Throwable cause, final Logger log
283270
}
284271

285272
/**
286-
* See {@link #maybeError(Throwable, Logger)}. Uses the class-local logger.
273+
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
274+
*
275+
* @param cause the root throwable
276+
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
287277
*/
288278
public static Optional<Error> maybeError(final Throwable cause) {
289-
return maybeError(cause, logger);
279+
return unwrapCausesAndSuppressed(cause, t -> t instanceof Error);
290280
}
291281

292282
/**
293283
* If the specified cause is an unrecoverable error, this method will rethrow the cause on a separate thread so that it can not be
294284
* caught and bubbles up to the uncaught exception handler. Note that the cause tree is examined for any {@link Error}. See
295-
* {@link #maybeError(Throwable, Logger)} for the semantics.
285+
* {@link #maybeError(Throwable)} for the semantics.
296286
*
297287
* @param throwable the throwable to possibly throw on another thread
298288
*/
299289
public static void maybeDieOnAnotherThread(final Throwable throwable) {
300-
ExceptionsHelper.maybeError(throwable, logger).ifPresent(error -> {
290+
ExceptionsHelper.maybeError(throwable).ifPresent(error -> {
301291
/*
302292
* Here be dragons. We want to rethrow this so that it bubbles up to the uncaught exception handler. Yet, sometimes the stack
303293
* contains statements that catch any throwable (e.g., Netty, and the JDK futures framework). This means that a rethrow here

server/src/main/java/org/elasticsearch/index/engine/Engine.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,7 @@ public abstract void forceMerge(boolean flush, int maxNumSegments, boolean onlyE
11421142
*/
11431143
@SuppressWarnings("finally")
11441144
private void maybeDie(final String maybeMessage, final Throwable maybeFatal) {
1145-
ExceptionsHelper.maybeError(maybeFatal, logger).ifPresent(error -> {
1145+
ExceptionsHelper.maybeError(maybeFatal).ifPresent(error -> {
11461146
try {
11471147
logger.error(maybeMessage, error);
11481148
} finally {

server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java

+29-10
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
import org.elasticsearch.test.ESTestCase;
3636
import org.elasticsearch.transport.RemoteClusterAware;
3737

38+
import java.io.IOException;
3839
import java.util.Optional;
3940

40-
import static org.elasticsearch.ExceptionsHelper.MAX_ITERATIONS;
4141
import static org.elasticsearch.ExceptionsHelper.maybeError;
4242
import static org.hamcrest.CoreMatchers.equalTo;
4343
import static org.hamcrest.CoreMatchers.instanceOf;
@@ -81,20 +81,14 @@ public void testMaybeError() {
8181
if (fatal) {
8282
assertError(cause, error);
8383
} else {
84-
assertFalse(maybeError(cause, logger).isPresent());
84+
assertFalse(maybeError(cause).isPresent());
8585
}
8686

87-
assertFalse(maybeError(new Exception(new DecoderException()), logger).isPresent());
88-
89-
Throwable chain = outOfMemoryError;
90-
for (int i = 0; i < MAX_ITERATIONS; i++) {
91-
chain = new Exception(chain);
92-
}
93-
assertFalse(maybeError(chain, logger).isPresent());
87+
assertFalse(maybeError(new Exception(new DecoderException())).isPresent());
9488
}
9589

9690
private void assertError(final Throwable cause, final Error error) {
97-
final Optional<Error> maybeError = maybeError(cause, logger);
91+
final Optional<Error> maybeError = maybeError(cause);
9892
assertTrue(maybeError.isPresent());
9993
assertThat(maybeError.get(), equalTo(error));
10094
}
@@ -211,4 +205,29 @@ public void testUnwrapCorruption() {
211205
withSuppressedException.addSuppressed(new RuntimeException());
212206
assertThat(ExceptionsHelper.unwrapCorruption(withSuppressedException), nullValue());
213207
}
208+
209+
public void testSuppressedCycle() {
210+
RuntimeException e1 = new RuntimeException();
211+
RuntimeException e2 = new RuntimeException();
212+
e1.addSuppressed(e2);
213+
e2.addSuppressed(e1);
214+
ExceptionsHelper.unwrapCorruption(e1);
215+
216+
final CorruptIndexException corruptIndexException = new CorruptIndexException("corrupt", "resource");
217+
RuntimeException e3 = new RuntimeException(corruptIndexException);
218+
e3.addSuppressed(e1);
219+
assertThat(ExceptionsHelper.unwrapCorruption(e3), equalTo(corruptIndexException));
220+
221+
RuntimeException e4 = new RuntimeException(e1);
222+
e4.addSuppressed(corruptIndexException);
223+
assertThat(ExceptionsHelper.unwrapCorruption(e4), equalTo(corruptIndexException));
224+
}
225+
226+
public void testCauseCycle() {
227+
RuntimeException e1 = new RuntimeException();
228+
RuntimeException e2 = new RuntimeException(e1);
229+
e1.initCause(e2);
230+
ExceptionsHelper.unwrap(e1, IOException.class);
231+
ExceptionsHelper.unwrapCorruption(e1);
232+
}
214233
}

0 commit comments

Comments
 (0)