Replace reflective access of Throwable#addSuppressed with version guarded access

Motivation:

In environments with a security manager, the reflective access to get the reference to
Throwable#addSuppressed can cause issues that result in Netty failing to load. The main
motivation in this pull request is to remove the use of reflection to prevent issues in
these environments.

Modifications:

ThrowableUtil no longer uses Class#getDeclaredMembers to get the Method that references
Throwable#addSuppressed and instead guards the call to Throwable#addSuppressed with a
Java version check.

Additionally, a annotation was added that suppresses the animal sniffer java16 signature
check on the given method. The benefit of the annotation is that it limits the exclusion
of Throwable to just the ThrowableUtil class and has string text indicating the reason
for suppressing the java16 signature check.

Result:

Netty no longer requires the use of Class#getDeclaredMethod for ThrowableUtil and will
work in environments restricted by a security manager without needing to grant reflection
permissions.

Fixes #7614
This commit is contained in:
jaymode 2018-01-23 14:28:25 -07:00 committed by Norman Maurer
parent b640797de1
commit f0c76cacc3
4 changed files with 43 additions and 42 deletions

View File

@ -0,0 +1,32 @@
/*
* Copyright 2018 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.util.internal;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation to suppress the Java 6 source code requirement checks for a method.
*/
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.METHOD })
public @interface SuppressJava6Requirement {
String reason();
}

View File

@ -18,26 +18,10 @@ package io.netty.util.internal;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List; import java.util.List;
public final class ThrowableUtil { public final class ThrowableUtil {
private static final Method addSupressedMethod = getAddSuppressed();
private static Method getAddSuppressed() {
if (PlatformDependent.javaVersion() < 7) {
return null;
}
try {
// addSuppressed is final, so we only need to look it up on Throwable.
return Throwable.class.getDeclaredMethod("addSuppressed", Throwable.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
private ThrowableUtil() { } private ThrowableUtil() { }
/** /**
@ -71,20 +55,15 @@ public final class ThrowableUtil {
} }
public static boolean haveSuppressed() { public static boolean haveSuppressed() {
return addSupressedMethod != null; return PlatformDependent.javaVersion() >= 7;
} }
@SuppressJava6Requirement(reason = "Throwable addSuppressed is only available for >= 7. Has check for < 7.")
public static void addSuppressed(Throwable target, Throwable suppressed) { public static void addSuppressed(Throwable target, Throwable suppressed) {
if (!haveSuppressed()) { if (!haveSuppressed()) {
return; return;
} }
try { target.addSuppressed(suppressed);
addSupressedMethod.invoke(target, suppressed);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
}
} }
public static void addSuppressedAndClear(Throwable target, List<Throwable> suppressed) { public static void addSuppressedAndClear(Throwable target, List<Throwable> suppressed) {

View File

@ -17,7 +17,6 @@ package io.netty.util.internal;
import org.junit.Test; import org.junit.Test;
import java.lang.reflect.Method;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.util.UUID; import java.util.UUID;
@ -26,8 +25,6 @@ import static org.junit.Assert.fail;
public class NativeLibraryLoaderTest { public class NativeLibraryLoaderTest {
private static final Method getSupressedMethod = getGetSuppressed();
@Test @Test
public void testFileNotFound() { public void testFileNotFound() {
try { try {
@ -35,7 +32,7 @@ public class NativeLibraryLoaderTest {
fail(); fail();
} catch (UnsatisfiedLinkError error) { } catch (UnsatisfiedLinkError error) {
assertTrue(error.getCause() instanceof FileNotFoundException); assertTrue(error.getCause() instanceof FileNotFoundException);
if (getSupressedMethod != null) { if (PlatformDependent.javaVersion() >= 7) {
verifySuppressedException(error, UnsatisfiedLinkError.class); verifySuppressedException(error, UnsatisfiedLinkError.class);
} }
} }
@ -48,34 +45,24 @@ public class NativeLibraryLoaderTest {
fail(); fail();
} catch (UnsatisfiedLinkError error) { } catch (UnsatisfiedLinkError error) {
assertTrue(error.getCause() instanceof FileNotFoundException); assertTrue(error.getCause() instanceof FileNotFoundException);
if (getSupressedMethod != null) { if (PlatformDependent.javaVersion() >= 7) {
verifySuppressedException(error, ClassNotFoundException.class); verifySuppressedException(error, ClassNotFoundException.class);
} }
} }
} }
@SuppressJava6Requirement(reason = "uses Java 7+ Throwable#getSuppressed but is guarded by version checks")
private static void verifySuppressedException(UnsatisfiedLinkError error, private static void verifySuppressedException(UnsatisfiedLinkError error,
Class<?> expectedSuppressedExceptionClass) { Class<?> expectedSuppressedExceptionClass) {
try { try {
Throwable[] suppressed = (Throwable[]) getSupressedMethod.invoke(error.getCause()); Throwable[] suppressed = error.getCause().getSuppressed();
assertTrue(suppressed.length == 1); assertTrue(suppressed.length == 1);
assertTrue(suppressed[0] instanceof UnsatisfiedLinkError); assertTrue(suppressed[0] instanceof UnsatisfiedLinkError);
suppressed = (Throwable[]) getSupressedMethod.invoke(suppressed[0]); suppressed = (suppressed[0]).getSuppressed();
assertTrue(suppressed.length == 1); assertTrue(suppressed.length == 1);
assertTrue(expectedSuppressedExceptionClass.isInstance(suppressed[0])); assertTrue(expectedSuppressedExceptionClass.isInstance(suppressed[0]));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }
private static Method getGetSuppressed() {
if (PlatformDependent.javaVersion() < 7) {
return null;
}
try {
return Throwable.class.getDeclaredMethod("getSuppressed");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
} }

View File

@ -721,6 +721,9 @@
<ignore>java.nio.file.Path</ignore> <ignore>java.nio.file.Path</ignore>
<ignore>java.io.File</ignore> <ignore>java.io.File</ignore>
</ignores> </ignores>
<annotations>
<annotation>io.netty.util.internal.SuppressJava6Requirement</annotation>
</annotations>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>