Skip to content

Fix Infinite Loops in ExceptionsHelper#unwrap #42716

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 5, 2019
59 changes: 31 additions & 28 deletions server/src/main/java/org/elasticsearch/ExceptionsHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

public final class ExceptionsHelper {
Expand Down Expand Up @@ -185,22 +186,14 @@ public static <T extends Throwable> T useOrSuppress(T first, T second) {
* @return Corruption indicating exception if one is found, otherwise {@code null}
*/
public static IOException unwrapCorruption(Throwable t) {
if (t != null) {
do {
for (Class<?> clazz : CORRUPTION_EXCEPTIONS) {
if (clazz.isInstance(t)) {
return (IOException) t;
}
return t == null ? null : ExceptionsHelper.<IOException>unwrap(t, logger, cause -> {
for (Class<?> clazz : CORRUPTION_EXCEPTIONS) {
if (clazz.isInstance(cause)) {
return true;
}
for (Throwable suppressed : t.getSuppressed()) {
IOException corruptionException = unwrapCorruption(suppressed);
if (corruptionException != null) {
return corruptionException;
}
}
} while ((t = t.getCause()) != null);
}
return null;
}
return false;
}).orElse(null);
}

/**
Expand All @@ -213,7 +206,13 @@ public static IOException unwrapCorruption(Throwable t) {
*/
public static Throwable unwrap(Throwable t, Class<?>... clazzes) {
if (t != null) {
int iterations = 0;
do {
++iterations;
if (iterations > MAX_ITERATIONS) {
logger.warn("giving up looking for nested cause", t);
return null;
}
for (Class<?> clazz : clazzes) {
if (clazz.isInstance(t)) {
return t;
Expand Down Expand Up @@ -248,16 +247,10 @@ public static boolean reThrowIfNotNull(@Nullable Throwable e) {

static final int MAX_ITERATIONS = 1024;

/**
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
*
* @param cause the root throwable
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
*/
public static Optional<Error> maybeError(final Throwable cause, final Logger logger) {
// early terminate if the cause is already an error
if (cause instanceof Error) {
return Optional.of((Error) cause);
@SuppressWarnings("unchecked")
private static <T extends Throwable> Optional<T> unwrap(Throwable cause, Logger logger, Predicate<Throwable> predicate) {
if (predicate.test(cause)) {
return Optional.of((T) cause);
}

final Queue<Throwable> queue = new LinkedList<>();
Expand All @@ -267,12 +260,12 @@ public static Optional<Error> maybeError(final Throwable cause, final Logger log
iterations++;
// this is a guard against deeply nested or circular chains of exceptions
if (iterations > MAX_ITERATIONS) {
logger.warn("giving up looking for fatal errors", cause);
logger.warn("giving up looking for nested cause", cause);
break;
}
final Throwable current = queue.remove();
if (current instanceof Error) {
return Optional.of((Error) current);
if (predicate.test(current)) {
return Optional.of((T) current);
}
Collections.addAll(queue, current.getSuppressed());
if (current.getCause() != null) {
Expand All @@ -282,6 +275,16 @@ public static Optional<Error> maybeError(final Throwable cause, final Logger log
return Optional.empty();
}

/**
* Unwrap the specified throwable looking for any suppressed errors or errors as a root cause of the specified throwable.
*
* @param cause the root throwable
* @return an optional error if one is found suppressed or a root cause in the tree rooted at the specified throwable
*/
public static Optional<Error> maybeError(final Throwable cause, final Logger logger) {
return unwrap(cause, logger, t -> t instanceof Error);
}

/**
* See {@link #maybeError(Throwable, Logger)}. Uses the class-local logger.
*/
Expand Down
17 changes: 17 additions & 0 deletions server/src/test/java/org/elasticsearch/ExceptionsHelperTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.transport.RemoteClusterAware;

import java.io.IOException;
import java.util.Optional;

import static org.elasticsearch.ExceptionsHelper.MAX_ITERATIONS;
Expand Down Expand Up @@ -211,4 +212,20 @@ public void testUnwrapCorruption() {
withSuppressedException.addSuppressed(new RuntimeException());
assertThat(ExceptionsHelper.unwrapCorruption(withSuppressedException), nullValue());
}

public void testSuppressedCycle() {
RuntimeException e1 = new RuntimeException();
RuntimeException e2 = new RuntimeException();
e1.addSuppressed(e2);
e2.addSuppressed(e1);
ExceptionsHelper.unwrapCorruption(e1);
}

public void testCauseCycle() {
RuntimeException e1 = new RuntimeException();
RuntimeException e2 = new RuntimeException(e1);
e1.initCause(e2);
ExceptionsHelper.unwrap(e1, IOException.class);
ExceptionsHelper.unwrapCorruption(e1);
}
}