You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pdfbox.apache.org by le...@apache.org on 2020/10/18 17:10:54 UTC

svn commit: r1882619 - in /pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io: IOUtils.java RandomAccessReadMemoryMappedFile.java

Author: lehmi
Date: Sun Oct 18 17:10:54 2020
New Revision: 1882619

URL: http://svn.apache.org/viewvc?rev=1882619&view=rev
Log:
PDFBOX-4855: adapt sambox unmapping implementation for memory mapped byte buffers based on org.apache.lucene.store.MMapDirectory

Modified:
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java
    pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/RandomAccessReadMemoryMappedFile.java

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java?rev=1882619&r1=1882618&r2=1882619&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/IOUtils.java Sun Oct 18 17:10:54 2020
@@ -19,11 +19,30 @@
 
 package org.apache.pdfbox.io;
 
+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.MethodHandles.lookup;
+import static java.lang.invoke.MethodType.methodType;
+import static java.util.Objects.nonNull;
+
 import java.io.ByteArrayOutputStream;
 import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles.Lookup;
+
+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 java.util.Optional;
+import java.util.function.Consumer;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
@@ -35,6 +54,13 @@ public final class IOUtils
 {
 
     //TODO PDFBox should really use Apache Commons IO.
+    private static final Optional<Consumer<ByteBuffer>> UNMAPPER;
+
+    static
+    {
+        UNMAPPER = Optional.ofNullable(AccessController
+                .doPrivileged((PrivilegedAction<Consumer<ByteBuffer>>) IOUtils::unmapper));
+    }
 
     /**
      * Log instance.
@@ -153,4 +179,132 @@ public final class IOUtils
         }
         return initialException;
     }
+
+    /**
+     * Unmap memory mapped byte buffers. This is a hack waiting for a proper JVM provided solution expected in java 10
+     * https://bugs.openjdk.java.net/browse/JDK-4724038 The issue here is that even when closed, memory mapped byte
+     * buffers hold a lock on the underlying file until GC is executes and this in turns result in an error if the user
+     * tries to move or delete the file.
+     * 
+     * @param buf
+     */
+    public static void unmap(ByteBuffer buf)
+    {
+        try
+        {
+            UNMAPPER.ifPresent(u -> u.accept(buf));
+        }
+        catch (Exception e)
+        {
+            LOG.error("Unable to unmap ByteBuffer.", e);
+        }
+    }
+
+    /**
+     * This is adapted from org.apache.lucene.store.MMapDirectory
+     * 
+     * @return
+     */
+    private static Consumer<ByteBuffer> unmapper()
+    {
+        final Lookup lookup = 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 MH 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 { noop(cleaner); // the noop is needed because
+                 * MethodHandles#guardWithTest always needs ELSE } }
+                 */
+                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)
+        {
+            LOG.error(
+                    "Unmapping is not supported because of missing permissions. Please grant at least the following permissions: RuntimePermission(\"accessClassInPackage.sun.misc\") "
+                            + " and ReflectPermission(\"suppressAccessChecks\")",
+                    se);
+
+        }
+        catch (ReflectiveOperationException | RuntimeException e)
+        {
+            LOG.error("Unmapping is not supported.", e);
+        }
+        return null;
+    }
+
+    private static Consumer<ByteBuffer> newBufferCleaner(final Class<?> unmappableBufferClass,
+            final MethodHandle unmapper)
+    {
+        assert Objects.equals(methodType(void.class, ByteBuffer.class), unmapper.type());
+        return (ByteBuffer 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 e = AccessController.doPrivileged((PrivilegedAction<Throwable>) () -> {
+                try
+                {
+                    unmapper.invokeExact(buffer);
+                    return null;
+                }
+                catch (Throwable t)
+                {
+                    return t;
+                }
+            });
+            if (nonNull(e))
+            {
+                LOG.error("Unable to unmap the mapped buffer", e);
+            }
+        };
+    }
+
 }

Modified: pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/RandomAccessReadMemoryMappedFile.java
URL: http://svn.apache.org/viewvc/pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/RandomAccessReadMemoryMappedFile.java?rev=1882619&r1=1882618&r2=1882619&view=diff
==============================================================================
--- pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/RandomAccessReadMemoryMappedFile.java (original)
+++ pdfbox/trunk/pdfbox/src/main/java/org/apache/pdfbox/io/RandomAccessReadMemoryMappedFile.java Sun Oct 18 17:10:54 2020
@@ -22,6 +22,8 @@ import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.nio.file.StandardOpenOption;
 import java.util.EnumSet;
+import java.util.Optional;
+import java.util.function.Consumer;
 
 /**
  * An implementation of the RandomAccess interface backed by a memory mapped file channel. The whole file is mapped to
@@ -39,6 +41,9 @@ public class RandomAccessReadMemoryMappe
     // file channel of the file to be read
     private FileChannel fileChannel;
 
+    // function to unmap the byte buffer
+    private Consumer<? super ByteBuffer> unmapper = IOUtils::unmap;
+
     /**
      * Default constructor.
      */
@@ -70,6 +75,8 @@ public class RandomAccessReadMemoryMappe
         mappedByteBuffer = parent.mappedByteBuffer.duplicate();
         size = parent.size;
         mappedByteBuffer.rewind();
+        // unmap doesn't work on duplicate, see Unsafe#invokeCleaner
+        unmapper = null;
     }
 
     /**
@@ -82,6 +89,7 @@ public class RandomAccessReadMemoryMappe
         {
             fileChannel.close();
         }
+        Optional.ofNullable(unmapper).ifPresent(u -> u.accept(mappedByteBuffer));
         mappedByteBuffer = null;
     }