]> source.dussan.org Git - poi.git/commitdiff
Bug 62151: Fix warnings about illegal reflective access in Java 9+ by
authorDominik Stadler <centic@apache.org>
Sat, 2 Mar 2019 09:19:45 +0000 (09:19 +0000)
committerDominik Stadler <centic@apache.org>
Sat, 2 Mar 2019 09:19:45 +0000 (09:19 +0000)
using a similar approach as Apache Hadoop via a CleanerUtil class which encapsulates
the two different approaches for Java <= 8 and Java >= 9

git-svn-id: https://svn.apache.org/repos/asf/poi/trunk@1854630 13f79535-47bb-0310-9956-ffa450edef68

src/java/org/apache/poi/poifs/nio/CleanerUtil.java [new file with mode: 0644]
src/java/org/apache/poi/poifs/nio/FileBackedDataSource.java

diff --git a/src/java/org/apache/poi/poifs/nio/CleanerUtil.java b/src/java/org/apache/poi/poifs/nio/CleanerUtil.java
new file mode 100644 (file)
index 0000000..9c70ee6
--- /dev/null
@@ -0,0 +1,201 @@
+/* ====================================================================
+   Licensed to the Apache Software Foundation (ASF) under one or more
+   contributor license agreements.  See the NOTICE file distributed with
+   this work for additional information regarding copyright ownership.
+   The ASF 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 org.apache.poi.poifs.nio;
+
+import org.apache.poi.util.SuppressForbidden;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.nio.ByteBuffer;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Objects;
+
+import static java.lang.invoke.MethodHandles.constant;
+import static java.lang.invoke.MethodHandles.dropArguments;
+import static java.lang.invoke.MethodHandles.filterReturnValue;
+import static java.lang.invoke.MethodHandles.guardWithTest;
+import static java.lang.invoke.MethodType.methodType;
+
+/**
+ * This is taken from Hadoop at https://issues.apache.org/jira/browse/HADOOP-12760 and
+ * https://github.com/apache/hadoop/blob/trunk/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/CleanerUtil.java
+ * Unfortunately this is not available in some general utility library yet, but hopefully will be at some point.
+ *
+ * sun.misc.Cleaner has moved in OpenJDK 9 and
+ * sun.misc.Unsafe#invokeCleaner(ByteBuffer) is the replacement.
+ * This class is a hack to use sun.misc.Cleaner in Java 8 and
+ * use the replacement in Java 9+.
+ * This implementation is inspired by LUCENE-6989.
+ */
+public final class CleanerUtil {
+
+    // Prevent instantiation
+    private CleanerUtil(){}
+
+    /**
+     * <code>true</code>, if this platform supports unmapping mmapped files.
+     */
+    public static final boolean UNMAP_SUPPORTED;
+
+    /**
+     * if {@link #UNMAP_SUPPORTED} is {@code false}, this contains the reason
+     * why unmapping is not supported.
+     */
+    public static final String UNMAP_NOT_SUPPORTED_REASON;
+
+
+    private static final BufferCleaner CLEANER;
+
+    /**
+     * Reference to a BufferCleaner that does unmapping.
+     * @return {@code null} if not supported.
+     */
+    public static BufferCleaner getCleaner() {
+        return CLEANER;
+    }
+
+    static {
+        final Object hack = AccessController.doPrivileged(
+                (PrivilegedAction<Object>) CleanerUtil::unmapHackImpl);
+        if (hack instanceof BufferCleaner) {
+            CLEANER = (BufferCleaner) hack;
+            UNMAP_SUPPORTED = true;
+            UNMAP_NOT_SUPPORTED_REASON = null;
+        } else {
+            CLEANER = null;
+            UNMAP_SUPPORTED = false;
+            UNMAP_NOT_SUPPORTED_REASON = hack.toString();
+        }
+    }
+
+    @SuppressForbidden("Java 9 Jigsaw whitelists access to sun.misc.Cleaner, so setAccessible works")
+    private static Object unmapHackImpl() {
+        final MethodHandles.Lookup lookup = MethodHandles.lookup();
+        try {
+            try {
+                // *** sun.misc.Unsafe unmapping (Java 9+) ***
+                final Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
+                // first check if Unsafe has the right method, otherwise we can
+                // give up without doing any security critical stuff:
+                final MethodHandle unmapper = lookup.findVirtual(unsafeClass,
+                        "invokeCleaner", methodType(void.class, ByteBuffer.class));
+                // fetch the unsafe instance and bind it to the virtual MH:
+                final Field f = unsafeClass.getDeclaredField("theUnsafe");
+                f.setAccessible(true);
+                final Object theUnsafe = f.get(null);
+                return newBufferCleaner(ByteBuffer.class, unmapper.bindTo(theUnsafe));
+            } catch (SecurityException se) {
+                // rethrow to report errors correctly (we need to catch it here,
+                // as we also catch RuntimeException below!):
+                throw se;
+            } catch (ReflectiveOperationException | RuntimeException e) {
+                // *** sun.misc.Cleaner unmapping (Java 8) ***
+                final Class<?> directBufferClass =
+                        Class.forName("java.nio.DirectByteBuffer");
+
+                final Method m = directBufferClass.getMethod("cleaner");
+                m.setAccessible(true);
+                final MethodHandle directBufferCleanerMethod = lookup.unreflect(m);
+                final Class<?> cleanerClass =
+                        directBufferCleanerMethod.type().returnType();
+
+                /*
+                 * "Compile" a MethodHandle that basically is equivalent
+                 * to the following code:
+                 *
+                 * void unmapper(ByteBuffer byteBuffer) {
+                 *   sun.misc.Cleaner cleaner =
+                 *       ((java.nio.DirectByteBuffer) byteBuffer).cleaner();
+                 *   if (Objects.nonNull(cleaner)) {
+                 *     cleaner.clean();
+                 *   } else {
+                 *     // the noop is needed because MethodHandles#guardWithTest
+                 *     // always needs ELSE
+                 *     noop(cleaner);
+                 *   }
+                 * }
+                 */
+                final MethodHandle cleanMethod = lookup.findVirtual(
+                        cleanerClass, "clean", methodType(void.class));
+                final MethodHandle nonNullTest = lookup.findStatic(Objects.class,
+                        "nonNull", methodType(boolean.class, Object.class))
+                        .asType(methodType(boolean.class, cleanerClass));
+                final MethodHandle noop = dropArguments(
+                        constant(Void.class, null).asType(methodType(void.class)),
+                        0, cleanerClass);
+                final MethodHandle unmapper = filterReturnValue(
+                        directBufferCleanerMethod,
+                        guardWithTest(nonNullTest, cleanMethod, noop))
+                        .asType(methodType(void.class, ByteBuffer.class));
+                return newBufferCleaner(directBufferClass, unmapper);
+            }
+        } catch (SecurityException se) {
+            return "Unmapping is not supported, because not all required " +
+                    "permissions are given to the Hadoop JAR file: " + se +
+                    " [Please grant at least the following permissions: " +
+                    "RuntimePermission(\"accessClassInPackage.sun.misc\") " +
+                    " and ReflectPermission(\"suppressAccessChecks\")]";
+        } catch (ReflectiveOperationException | RuntimeException e) {
+            return "Unmapping is not supported on this platform, " +
+                    "because internal Java APIs are not compatible with " +
+                    "this Hadoop version: " + e;
+        }
+    }
+
+    private static BufferCleaner newBufferCleaner(
+            final Class<?> unmappableBufferClass, final MethodHandle unmapper) {
+        assert Objects.equals(
+                methodType(void.class, ByteBuffer.class), unmapper.type());
+        return buffer -> {
+            if (!buffer.isDirect()) {
+                throw new IllegalArgumentException(
+                        "unmapping only works with direct buffers");
+            }
+            if (!unmappableBufferClass.isInstance(buffer)) {
+                throw new IllegalArgumentException("buffer is not an instance of " +
+                        unmappableBufferClass.getName());
+            }
+            final Throwable error = AccessController.doPrivileged(
+                    (PrivilegedAction<Throwable>) () -> {
+                        try {
+                            unmapper.invokeExact(buffer);
+                            return null;
+                        } catch (Throwable t) {
+                            return t;
+                        }
+                    });
+            if (error != null) {
+                throw new IOException("Unable to unmap the mapped buffer", error);
+            }
+        };
+    }
+
+    /**
+     * Pass in an implementation of this interface to cleanup ByteBuffers.
+     * CleanerUtil implements this to allow unmapping of bytebuffers
+     * with private Java APIs.
+     */
+    @FunctionalInterface
+    public interface BufferCleaner {
+        void freeBuffer(ByteBuffer b) throws IOException;
+    }
+}
index f9549cf54239bb343d0582e4b11a2014a068bd18..ee08de13d7827711c80df1ab11fc03bee0fe11ca 100644 (file)
 
 package org.apache.poi.poifs.nio;
 
+import org.apache.poi.util.IOUtils;
+import org.apache.poi.util.POILogFactory;
+import org.apache.poi.util.POILogger;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.RandomAccessFile;
-import java.lang.reflect.Method;
 import java.nio.ByteBuffer;
 import java.nio.channels.Channels;
 import java.nio.channels.FileChannel;
 import java.nio.channels.WritableByteChannel;
-import java.security.AccessController;
-import java.security.PrivilegedAction;
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.poi.util.IOUtils;
-import org.apache.poi.util.POILogFactory;
-import org.apache.poi.util.POILogger;
-import org.apache.poi.util.SuppressForbidden;
-
 /**
  * A POIFS {@link DataSource} backed by a File
  */
@@ -171,22 +167,14 @@ public class FileBackedDataSource extends DataSource {
            return;
        }
 
-       AccessController.doPrivileged(new PrivilegedAction<Void>() {
-           @Override
-           @SuppressForbidden("Java 9 Jigsaw whitelists access to sun.misc.Cleaner, so setAccessible works")
-           public Void run() {
-               try {
-                   final Method getCleanerMethod = buffer.getClass().getMethod("cleaner");
-                   getCleanerMethod.setAccessible(true);
-                   final Object cleaner = getCleanerMethod.invoke(buffer);
-                   if (cleaner != null) {
-                       cleaner.getClass().getMethod("clean").invoke(cleaner);
-                   }
-               } catch (Exception e) {
-                   logger.log(POILogger.WARN, "Unable to unmap memory mapped ByteBuffer.", e);
-               }
-               return null; // Void
+       if (CleanerUtil.UNMAP_SUPPORTED) {
+           try {
+               CleanerUtil.getCleaner().freeBuffer(buffer);
+           } catch (IOException e) {
+               logger.log(POILogger.WARN, "Failed to unmap the buffer", e);
            }
-       });
-    }
+       } else {
+           logger.log(POILogger.DEBUG, CleanerUtil.UNMAP_NOT_SUPPORTED_REASON);
+       }
+   }
 }