Browse Source

Use open and fstat in preallocate (#105171)

Preallocate opens a FileInputStream in order to get a native file
desctiptor to pass to native functions. However, getting at the file
descriptor requires breaking modular access. This commit adds native
posix functions for opening/closing and retrieving stats on a file in
order to avoid requiring additional permissions.
Ryan Ernst 1 year ago
parent
commit
18a1ac09e7

+ 148 - 0
libs/preallocate/src/main/java/org/elasticsearch/preallocate/AbstractPosixPreallocator.java

@@ -0,0 +1,148 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.preallocate;
+
+import com.sun.jna.FunctionMapper;
+import com.sun.jna.Library;
+import com.sun.jna.Native;
+import com.sun.jna.NativeLong;
+import com.sun.jna.Platform;
+import com.sun.jna.Structure;
+
+import java.io.IOException;
+import java.security.AccessController;
+import java.security.PrivilegedAction;
+import java.util.Locale;
+import java.util.Map;
+
+abstract class AbstractPosixPreallocator implements Preallocator {
+
+    /**
+     * Constants relating to posix libc.
+     *
+     * @param SIZEOF_STAT The size of the stat64 structure, ie sizeof(stat64_t), found by importing sys/stat.h
+     * @param STAT_ST_SIZE_OFFSET The offsite into stat64 at which st_size exists, ie offsetof(stat64_t, st_size),
+     *                           found by importing sys/stat.h
+     * @param O_CREAT The file mode for creating a file upon opening, found by importing fcntl.h
+     */
+    protected record PosixConstants(int SIZEOF_STAT, int STAT_ST_SIZE_OFFSET, int O_CREAT) {}
+
+    private static final int O_WRONLY = 1;
+
+    static final class Stat64 extends Structure implements Structure.ByReference {
+        public byte[] _ignore1;
+        public NativeLong st_size = new NativeLong(0);
+        public byte[] _ignore2;
+
+        Stat64(int sizeof, int stSizeOffset) {
+            this._ignore1 = new byte[stSizeOffset];
+            this._ignore2 = new byte[sizeof - stSizeOffset - 8];
+        }
+    }
+
+    private interface NativeFunctions extends Library {
+        String strerror(int errno);
+
+        int open(String filename, int flags, Object... mode);
+
+        int close(int fd);
+    }
+
+    private interface FStat64Function extends Library {
+        int fstat64(int fd, Stat64 stat);
+    }
+
+    public static final boolean NATIVES_AVAILABLE;
+    private static final NativeFunctions functions;
+    private static final FStat64Function fstat64;
+
+    static {
+        functions = AccessController.doPrivileged((PrivilegedAction<NativeFunctions>) () -> {
+            try {
+                return Native.load(Platform.C_LIBRARY_NAME, NativeFunctions.class);
+            } catch (final UnsatisfiedLinkError e) {
+                return null;
+            }
+        });
+        fstat64 = AccessController.doPrivileged((PrivilegedAction<FStat64Function>) () -> {
+            try {
+                return Native.load(Platform.C_LIBRARY_NAME, FStat64Function.class);
+            } catch (final UnsatisfiedLinkError e) {
+                try {
+                    // on Linux fstat64 isn't available as a symbol, but instead uses a special __ name
+                    var options = Map.of(Library.OPTION_FUNCTION_MAPPER, (FunctionMapper) (lib, method) -> "__fxstat64");
+                    return Native.load(Platform.C_LIBRARY_NAME, FStat64Function.class, options);
+                } catch (UnsatisfiedLinkError e2) {
+                    return null;
+                }
+            }
+        });
+        NATIVES_AVAILABLE = functions != null && fstat64 != null;
+    }
+
+    private class PosixNativeFileHandle implements NativeFileHandle {
+
+        private final int fd;
+
+        PosixNativeFileHandle(int fd) {
+            this.fd = fd;
+        }
+
+        @Override
+        public int fd() {
+            return fd;
+        }
+
+        @Override
+        public long getSize() throws IOException {
+            var stat = new Stat64(constants.SIZEOF_STAT, constants.STAT_ST_SIZE_OFFSET);
+            if (fstat64.fstat64(fd, stat) == -1) {
+                throw newIOException("Could not get size of file");
+            }
+            return stat.st_size.longValue();
+        }
+
+        @Override
+        public void close() throws IOException {
+            if (functions.close(fd) != 0) {
+                throw newIOException("Could not close file");
+            }
+        }
+    }
+
+    protected final PosixConstants constants;
+
+    AbstractPosixPreallocator(PosixConstants constants) {
+        this.constants = constants;
+    }
+
+    @Override
+    public boolean useNative() {
+        return false;
+    }
+
+    @Override
+    public NativeFileHandle open(String path) throws IOException {
+        int fd = functions.open(path, O_WRONLY, constants.O_CREAT);
+        if (fd < 0) {
+            throw newIOException(String.format(Locale.ROOT, "Could not open file [%s] for preallocation", path));
+        }
+        return new PosixNativeFileHandle(fd);
+    }
+
+    @Override
+    public String error(int errno) {
+        return functions.strerror(errno);
+    }
+
+    private static IOException newIOException(String prefix) {
+        int errno = Native.getLastError();
+        return new IOException(String.format(Locale.ROOT, "%s(errno=%d): %s", prefix, errno, functions.strerror(errno)));
+    }
+}

+ 6 - 10
libs/preallocate/src/main/java/org/elasticsearch/preallocate/LinuxPreallocator.java

@@ -13,11 +13,15 @@ import com.sun.jna.Platform;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 
-final class LinuxPreallocator implements Preallocator {
+final class LinuxPreallocator extends AbstractPosixPreallocator {
+
+    LinuxPreallocator() {
+        super(new PosixConstants(144, 48, 64));
+    }
 
     @Override
     public boolean useNative() {
-        return Natives.NATIVES_AVAILABLE;
+        return Natives.NATIVES_AVAILABLE && super.useNative();
     }
 
     @Override
@@ -26,11 +30,6 @@ final class LinuxPreallocator implements Preallocator {
         return rc == 0 ? 0 : Native.getLastError();
     }
 
-    @Override
-    public String error(int errno) {
-        return Natives.strerror(errno);
-    }
-
     private static class Natives {
 
         public static final boolean NATIVES_AVAILABLE;
@@ -47,9 +46,6 @@ final class LinuxPreallocator implements Preallocator {
         }
 
         static native int fallocate(int fd, int mode, long offset, long length);
-
-        static native String strerror(int errno);
-
     }
 
 }

+ 6 - 10
libs/preallocate/src/main/java/org/elasticsearch/preallocate/MacOsPreallocator.java

@@ -17,11 +17,15 @@ import java.security.PrivilegedAction;
 import java.util.Arrays;
 import java.util.List;
 
-final class MacOsPreallocator implements Preallocator {
+final class MacOsPreallocator extends AbstractPosixPreallocator {
+
+    MacOsPreallocator() {
+        super(new PosixConstants(144, 96, 512));
+    }
 
     @Override
     public boolean useNative() {
-        return Natives.NATIVES_AVAILABLE;
+        return Natives.NATIVES_AVAILABLE && super.useNative();
     }
 
     @Override
@@ -47,11 +51,6 @@ final class MacOsPreallocator implements Preallocator {
         return 0;
     }
 
-    @Override
-    public String error(final int errno) {
-        return Natives.strerror(errno);
-    }
-
     private static class Natives {
 
         static boolean NATIVES_AVAILABLE;
@@ -99,9 +98,6 @@ final class MacOsPreallocator implements Preallocator {
         static native int fcntl(int fd, int cmd, Fcntl.FStore fst);
 
         static native int ftruncate(int fd, NativeLong length);
-
-        static native String strerror(int errno);
-
     }
 
 }

+ 7 - 0
libs/preallocate/src/main/java/org/elasticsearch/preallocate/NoNativePreallocator.java

@@ -7,6 +7,8 @@
  */
 package org.elasticsearch.preallocate;
 
+import java.io.IOException;
+
 final class NoNativePreallocator implements Preallocator {
 
     @Override
@@ -14,6 +16,11 @@ final class NoNativePreallocator implements Preallocator {
         return false;
     }
 
+    @Override
+    public NativeFileHandle open(String path) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
     @Override
     public int preallocate(final int fd, final long currentSize, final long fileSize) {
         throw new UnsupportedOperationException();

+ 5 - 10
libs/preallocate/src/main/java/org/elasticsearch/preallocate/Preallocate.java

@@ -10,6 +10,7 @@ package org.elasticsearch.preallocate;
 import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.logging.LogManager;
 import org.elasticsearch.logging.Logger;
+import org.elasticsearch.preallocate.Preallocator.NativeFileHandle;
 
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -17,7 +18,6 @@ import java.io.RandomAccessFile;
 import java.lang.reflect.Field;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.security.AccessController;
 import java.security.PrivilegedExceptionAction;
 
 public class Preallocate {
@@ -42,21 +42,16 @@ public class Preallocate {
         }
     }
 
-    @SuppressForbidden(reason = "need access to fd on FileOutputStream")
+    @SuppressForbidden(reason = "need access to toFile for RandomAccessFile")
     private static void preallocate(final Path cacheFile, final long fileSize, final Preallocator prealloactor) throws IOException {
         boolean success = false;
         try {
             if (prealloactor.useNative()) {
-                try (FileOutputStream fileChannel = new FileOutputStream(cacheFile.toFile())) {
-                    long currentSize = fileChannel.getChannel().size();
+                try (NativeFileHandle openFile = prealloactor.open(cacheFile.toAbsolutePath().toString())) {
+                    long currentSize = openFile.getSize();
                     if (currentSize < fileSize) {
                         logger.info("pre-allocating cache file [{}] ({} bytes) using native methods", cacheFile, fileSize);
-                        final Field field = AccessController.doPrivileged(new FileDescriptorFieldAction(fileChannel));
-                        final int errno = prealloactor.preallocate(
-                            (int) field.get(fileChannel.getFD()),
-                            currentSize,
-                            fileSize - currentSize
-                        );
+                        final int errno = prealloactor.preallocate(openFile.fd(), currentSize, fileSize - currentSize);
                         if (errno == 0) {
                             success = true;
                             logger.debug("pre-allocated cache file [{}] using native methods", cacheFile);

+ 19 - 0
libs/preallocate/src/main/java/org/elasticsearch/preallocate/Preallocator.java

@@ -7,11 +7,22 @@
  */
 package org.elasticsearch.preallocate;
 
+import java.io.IOException;
+
 /**
  * Represents platform native methods for pre-allocating files.
  */
 interface Preallocator {
 
+    /** A handle for an open file */
+    interface NativeFileHandle extends AutoCloseable {
+        /** A valid native file descriptor */
+        int fd();
+
+        /** Retrieves the current size of the file */
+        long getSize() throws IOException;
+    }
+
     /**
      * Returns if native methods for pre-allocating files are available.
      *
@@ -19,6 +30,14 @@ interface Preallocator {
      */
     boolean useNative();
 
+    /**
+     * Open a file for preallocation.
+     *
+     * @param path The absolute path to the file to be opened
+     * @return a handle to the open file that may be used for preallocate
+     */
+    NativeFileHandle open(String path) throws IOException;
+
     /**
      * Pre-allocate a file of given current size to the specified size using the given file descriptor.
      *

+ 2 - 2
server/src/main/resources/org/elasticsearch/bootstrap/security.policy

@@ -70,6 +70,7 @@ grant codeBase "${codebase.elasticsearch-cli}" {
 grant codeBase "${codebase.jna}" {
   // for registering native methods
   permission java.lang.RuntimePermission "accessDeclaredMembers";
+  permission java.lang.reflect.ReflectPermission "newProxyInPackage.org.elasticsearch.preallocate";
 };
 
 grant codeBase "${codebase.log4j-api}" {
@@ -79,8 +80,7 @@ grant codeBase "${codebase.log4j-api}" {
 grant codeBase "${codebase.elasticsearch-preallocate}" {
   // for registering native methods
   permission java.lang.RuntimePermission "accessDeclaredMembers";
-  // for accessing the file descriptor field in FileChannel
-  permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
+  permission java.lang.reflect.ReflectPermission "newProxyInPackage.org.elasticsearch.preallocate";
 };
 
 //// Everything else: