Browse Source

Instrument methods on File that require write permission (#122109)

This commit adds instrumentation for File methods that require write
permission. No server or plugins use these methods, so no policy changes
were necessary. Note that since we are not planning to restrict temp
file creation, the bootstrap self test on file writing was removed,
which failed with these changes.
Ryan Ernst 8 months ago
parent
commit
5f00b64ec7

+ 30 - 0
libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java

@@ -501,6 +501,36 @@ public interface EntitlementChecker {
     //
 
     // old io (ie File)
+    void check$java_io_File$createNewFile(Class<?> callerClass, File file);
+
+    void check$java_io_File$$createTempFile(Class<?> callerClass, String prefix, String suffix, File directory);
+
+    void check$java_io_File$delete(Class<?> callerClass, File file);
+
+    void check$java_io_File$deleteOnExit(Class<?> callerClass, File file);
+
+    void check$java_io_File$mkdir(Class<?> callerClass, File file);
+
+    void check$java_io_File$mkdirs(Class<?> callerClass, File file);
+
+    void check$java_io_File$renameTo(Class<?> callerClass, File file, File dest);
+
+    void check$java_io_File$setExecutable(Class<?> callerClass, File file, boolean executable);
+
+    void check$java_io_File$setExecutable(Class<?> callerClass, File file, boolean executable, boolean ownerOnly);
+
+    void check$java_io_File$setLastModified(Class<?> callerClass, File file, long time);
+
+    void check$java_io_File$setReadable(Class<?> callerClass, File file, boolean readable);
+
+    void check$java_io_File$setReadable(Class<?> callerClass, File file, boolean readable, boolean ownerOnly);
+
+    void check$java_io_File$setReadOnly(Class<?> callerClass, File file);
+
+    void check$java_io_File$setWritable(Class<?> callerClass, File file, boolean writable);
+
+    void check$java_io_File$setWritable(Class<?> callerClass, File file, boolean writable, boolean ownerOnly);
+
     void check$java_io_FileOutputStream$(Class<?> callerClass, File file);
 
     void check$java_io_FileOutputStream$(Class<?> callerClass, File file, boolean append);

+ 4 - 0
libs/entitlement/qa/entitled-plugin/src/main/java/org/elasticsearch/entitlement/qa/entitled/EntitledActions.java

@@ -27,4 +27,8 @@ public final class EntitledActions {
     public static UserPrincipal getFileOwner(Path path) throws IOException {
         return Files.getOwner(path);
     }
+
+    public static void createFile(Path path) throws IOException {
+        Files.createFile(path);
+    }
 }

+ 86 - 0
libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/FileCheckActions.java

@@ -12,6 +12,7 @@ package org.elasticsearch.entitlement.qa.test;
 import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.entitlement.qa.entitled.EntitledActions;
 
+import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -45,6 +46,91 @@ class FileCheckActions {
         return testRootDir.resolve("read_write_file");
     }
 
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileCreateNewFile() throws IOException {
+        readWriteDir().resolve("new_file").toFile().createNewFile();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileCreateTempFile() throws IOException {
+        File.createTempFile("prefix", "suffix", readWriteDir().toFile());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileDelete() throws IOException {
+        Path toDelete = readWriteDir().resolve("to_delete");
+        EntitledActions.createFile(toDelete);
+        toDelete.toFile().delete();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileDeleteOnExit() throws IOException {
+        Path toDelete = readWriteDir().resolve("to_delete_on_exit");
+        EntitledActions.createFile(toDelete);
+        toDelete.toFile().deleteOnExit();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileMkdir() throws IOException {
+        Path mkdir = readWriteDir().resolve("mkdir");
+        mkdir.toFile().mkdir();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileMkdirs() throws IOException {
+        Path mkdir = readWriteDir().resolve("mkdirs");
+        mkdir.toFile().mkdirs();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileRenameTo() throws IOException {
+        Path toRename = readWriteDir().resolve("to_rename");
+        EntitledActions.createFile(toRename);
+        toRename.toFile().renameTo(readWriteDir().resolve("renamed").toFile());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetExecutable() throws IOException {
+        readWriteFile().toFile().setExecutable(false);
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetExecutableOwner() throws IOException {
+        readWriteFile().toFile().setExecutable(false, false);
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetLastModified() throws IOException {
+        readWriteFile().toFile().setLastModified(System.currentTimeMillis());
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetReadable() throws IOException {
+        readWriteFile().toFile().setReadable(true);
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetReadableOwner() throws IOException {
+        readWriteFile().toFile().setReadable(true, false);
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetReadOnly() throws IOException {
+        Path readOnly = readWriteDir().resolve("read_only");
+        EntitledActions.createFile(readOnly);
+        readOnly.toFile().setReadOnly();
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetWritable() throws IOException {
+        readWriteFile().toFile().setWritable(true);
+    }
+
+    @EntitlementTest(expectedAccess = PLUGINS)
+    static void fileSetWritableOwner() throws IOException {
+        readWriteFile().toFile().setWritable(true, false);
+    }
+
     @EntitlementTest(expectedAccess = PLUGINS)
     static void createScannerFile() throws FileNotFoundException {
         new Scanner(readFile().toFile());

+ 0 - 35
libs/entitlement/src/main/java/org/elasticsearch/entitlement/bootstrap/EntitlementBootstrap.java

@@ -15,7 +15,6 @@ import com.sun.tools.attach.AttachNotSupportedException;
 import com.sun.tools.attach.VirtualMachine;
 
 import org.elasticsearch.core.CheckedConsumer;
-import org.elasticsearch.core.CheckedSupplier;
 import org.elasticsearch.core.SuppressForbidden;
 import org.elasticsearch.entitlement.initialization.EntitlementInitialization;
 import org.elasticsearch.entitlement.runtime.api.NotEntitledException;
@@ -27,7 +26,6 @@ import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.attribute.FileAttribute;
 import java.util.Map;
 import java.util.function.Function;
 
@@ -149,11 +147,8 @@ public class EntitlementBootstrap {
      */
     private static void selfTest() {
         ensureCannotStartProcess(ProcessBuilder::start);
-        ensureCanCreateTempFile(EntitlementBootstrap::createTempFile);
-
         // Try again with reflection
         ensureCannotStartProcess(EntitlementBootstrap::reflectiveStartProcess);
-        ensureCanCreateTempFile(EntitlementBootstrap::reflectiveCreateTempFile);
     }
 
     private static void ensureCannotStartProcess(CheckedConsumer<ProcessBuilder, ?> startProcess) {
@@ -169,31 +164,6 @@ public class EntitlementBootstrap {
         throw new IllegalStateException("Entitlement protection self-test was incorrectly permitted");
     }
 
-    @SuppressForbidden(reason = "accesses jvm default tempdir as a self-test")
-    private static void ensureCanCreateTempFile(CheckedSupplier<Path, ?> createTempFile) {
-        try {
-            Path p = createTempFile.get();
-            p.toFile().deleteOnExit();
-
-            // Make an effort to clean up the file immediately; also, deleteOnExit leaves the file if the JVM exits abnormally.
-            try {
-                Files.delete(p);
-            } catch (IOException ignored) {
-                // Can be caused by virus scanner
-            }
-        } catch (NotEntitledException e) {
-            throw new IllegalStateException("Entitlement protection self-test was incorrectly forbidden", e);
-        } catch (Exception e) {
-            throw new IllegalStateException("Unable to perform entitlement protection self-test", e);
-        }
-        logger.debug("Success: Entitlement protection correctly permitted temp file creation");
-    }
-
-    @SuppressForbidden(reason = "accesses jvm default tempdir as a self-test")
-    private static Path createTempFile() throws Exception {
-        return Files.createTempFile(null, null);
-    }
-
     private static void reflectiveStartProcess(ProcessBuilder pb) throws Exception {
         try {
             var start = ProcessBuilder.class.getMethod("start");
@@ -203,10 +173,5 @@ public class EntitlementBootstrap {
         }
     }
 
-    private static Path reflectiveCreateTempFile() throws Exception {
-        return (Path) Files.class.getMethod("createTempFile", String.class, String.class, FileAttribute[].class)
-            .invoke(null, null, null, new FileAttribute<?>[0]);
-    }
-
     private static final Logger logger = LogManager.getLogger(EntitlementBootstrap.class);
 }

+ 13 - 1
libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java

@@ -24,6 +24,8 @@ import org.elasticsearch.entitlement.runtime.policy.Scope;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.CreateClassLoaderEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.Entitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.ExitVMEntitlement;
+import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement;
+import org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.FileData;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.InboundNetworkEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.LoadNativeLibrariesEntitlement;
 import org.elasticsearch.entitlement.runtime.policy.entitlements.OutboundNetworkEntitlement;
@@ -39,6 +41,7 @@ import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.spi.FileSystemProvider;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -47,6 +50,8 @@ import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 
+import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE;
+
 /**
  * Called by the agent during {@code agentmain} to configure the entitlement system,
  * instantiate and configure an {@link EntitlementChecker},
@@ -121,6 +126,7 @@ public class EntitlementInitialization {
 
     private static PolicyManager createPolicyManager() {
         Map<String, Policy> pluginPolicies = EntitlementBootstrap.bootstrapArgs().pluginPolicies();
+        Path[] dataDirs = EntitlementBootstrap.bootstrapArgs().dataDirs();
 
         // TODO(ES-10031): Decide what goes in the elasticsearch default policy and extend it
         var serverPolicy = new Policy(
@@ -142,7 +148,13 @@ public class EntitlementInitialization {
                 new Scope("org.apache.httpcomponents.httpclient", List.of(new OutboundNetworkEntitlement())),
                 new Scope("io.netty.transport", List.of(new InboundNetworkEntitlement(), new OutboundNetworkEntitlement())),
                 new Scope("org.apache.lucene.core", List.of(new LoadNativeLibrariesEntitlement())),
-                new Scope("org.elasticsearch.nativeaccess", List.of(new LoadNativeLibrariesEntitlement()))
+                new Scope(
+                    "org.elasticsearch.nativeaccess",
+                    List.of(
+                        new LoadNativeLibrariesEntitlement(),
+                        new FilesEntitlement(Arrays.stream(dataDirs).map(d -> new FileData(d.toString(), READ_WRITE)).toList())
+                    )
+                )
             )
         );
         // agents run without a module, so this is a special hack for the apm agent

+ 76 - 0
libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java

@@ -941,6 +941,82 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker {
 
     // old io (ie File)
 
+    @Override
+    public void check$java_io_File$createNewFile(Class<?> callerClass, File file) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$$createTempFile(Class<?> callerClass, String prefix, String suffix, File directory) {
+        policyManager.checkFileWrite(callerClass, directory);
+    }
+
+    @Override
+    public void check$java_io_File$delete(Class<?> callerClass, File file) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$deleteOnExit(Class<?> callerClass, File file) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$mkdir(Class<?> callerClass, File file) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$mkdirs(Class<?> callerClass, File file) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$renameTo(Class<?> callerClass, File file, File dest) {
+        policyManager.checkFileRead(callerClass, file);
+        policyManager.checkFileWrite(callerClass, dest);
+    }
+
+    @Override
+    public void check$java_io_File$setExecutable(Class<?> callerClass, File file, boolean executable) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setExecutable(Class<?> callerClass, File file, boolean executable, boolean ownerOnly) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setLastModified(Class<?> callerClass, File file, long time) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setReadable(Class<?> callerClass, File file, boolean readable) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setReadable(Class<?> callerClass, File file, boolean readable, boolean ownerOnly) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setReadOnly(Class<?> callerClass, File file) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setWritable(Class<?> callerClass, File file, boolean writable) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
+    @Override
+    public void check$java_io_File$setWritable(Class<?> callerClass, File file, boolean writable, boolean ownerOnly) {
+        policyManager.checkFileWrite(callerClass, file);
+    }
+
     @Override
     public void check$java_io_FileOutputStream$(Class<?> callerClass, String name) {
         policyManager.checkFileWrite(callerClass, new File(name));