From 00671fd8babdca7039d5fcb5dcb1823bef081152 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Thu, 17 Apr 2025 13:56:58 +0200 Subject: [PATCH 1/7] Add package-info.java and javadocs to document Entitlements design and implementation --- .../entitlement/bridge/package-info.java | 63 ++++++ .../EntitlementInitialization.java | 34 ++++ .../InstrumentationService.java | 20 ++ .../instrumentation/Instrumenter.java | 25 +++ .../entitlement/package-info.java | 189 ++++++++++++++++++ .../runtime/policy/FileAccessTree.java | 76 +++++++ .../runtime/policy/PolicyManager.java | 78 ++++++++ 7 files changed, 485 insertions(+) create mode 100644 libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java create mode 100644 libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java new file mode 100644 index 0000000000000..21f005cb45520 --- /dev/null +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java @@ -0,0 +1,63 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + *

+ * Contains classes that need to be used directly from instrumented methods. + * It's a minimal shim that is patched into the {@code java.base} module so that it is callable from the class library methods instrumented + * by the agent. The shim retains a {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} instance (inside its + * {@link org.elasticsearch.entitlement.bridge.EntitlementCheckerHandle} holder) and forwards the entitlement checks to the main library, + * which is loaded normally. + * {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} holds all the entitlements check definitions, one for each instrumented + * method. + *

+ *

+ * In order to work across multiple Java versions, this project uses multi-release jars via the {@code mrjar} plugin, which makes it is + * possible to specify classes for specific Java versions in specific {@code src} folders (e.g. {@code main23} for classes available to + * Java 23+). + * We use the mrjar plugin in a particular way, merging all the classes into the same jar. Therefore, we want to prefix the file name + * with the version, e.g. {@code Java23EntitlementCheckerHandle} and {@code Java23EntitlementChecker}. + *

+ *

+ * At runtime, we identify and instantiate the correct class using the runtime Java version to prepend the correct prefix to the class + * name (see {@code EntitlementInitialization#getVersionSpecificCheckerClass}). + *

+ *

+ * These different classes are needed to hold entitlements check definitions that are specific to a Java version. + * As an example, consider the Linker API. + *

+ *

+ * Note: the current version of Elasticsearch supports Java 21+; this is only an example (taken from the 8.x branch) that + * illustrates a complex scenario. + *

+ *

+ * The API went through multiple previews, and therefore changes between Java 19, 20 and 21; in order to support this correctly on these + * versions, we should introduce 2 utility interfaces, "preview" and "stable". + * For example, for the Java 20 specific signatures and functions, we would create {@code Java20StableEntitlementChecker} and + * {@code Java20PreviewEntitlementChecker}. + *

+ *

+ * The linker API in Java 20 introduces the final form for {@code downcallHandle}, which has different argument types from the one in + * Java 19. To instrument and check it, we would add a + * {@code check$jdk_internal_foreign_abi_AbstractLinker$downcallHandle(FunctionDescriptor, Linker.Option...)} method for it to the + * {@code Java20StableEntitlementChecker} interface, which extends {@link org.elasticsearch.entitlement.bridge.EntitlementChecker}. + * This interface would then be used by both the Java 20 specific interface ({@code Java20EntitlementChecker}) and any interface for newer + * Java versions (e.g. {@code Java21EntitlementChecker}, which extends {@code Java20StableEntitlementChecker}): this way when we run on + * either Java 20, Java 21, or following versions, we always instrument {@code downcallHandle} with the Java 20+ signature defined in + * {@code Java20StableEntitlementChecker}. + * Java 20 also introduces the {@code upcallStub} function; this function is not in its final form, as it has different parameters in the + * following (21+) previews and in the final API. + * In this case, we would add a {@code jdk_internal_foreign_abi_AbstractLinker$upcallStub(MethodHandle, FunctionDescriptor, SegmentScope)} + * function to the {@code Java20PreviewEntitlementChecker} interface. {@code Java20EntitlementChecker} would inherit from this interface + * too, but {@code Java21EntitlementChecker} and following would not. This way when we run on Java 20 we would instrument {@code upcallStub} + * with the Java 20 signature {@code (FunctionDescriptor, Linker.Option...)}, but we would not when we run on following (Java 21+) versions. + * Those will have the newer (final) {@code upcallStub} definition introduced in {@code Java21EntitlementChecker}. + *

+ */ +package org.elasticsearch.entitlement.bridge; diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index 8c13fc398e5d4..8db8fda53754c 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -98,6 +98,40 @@ public static EntitlementChecker checker() { return manager; } + /** + * Initializes the Entitlement system: + *
    + *
  1. + * Finds the version-specific subclass of {@link EntitlementChecker} to use + *
  2. + *
  3. + * Builds the set of methods to instrument using {@link InstrumentationService#lookupMethods} + *
  4. + *
  5. + * Augment this set “dynamically” using {@link InstrumentationService#lookupImplementationMethod} + *
  6. + *
  7. + * Creates an {@link Instrumenter} via {@link InstrumentationService#newInstrumenter}, and adds a new {@link Transformer} (derived from + * {@link java.lang.instrument.ClassFileTransformer}) that uses it. Transformers are invoked when a class is about to load, after its + * bytes have been deserialized to memory but before the class is initialized. + *
  8. + *
  9. + * Re-transforms all already loaded classes: we force the {@link Instrumenter} to run on classes that might have been already loaded + * before entitlement initialization by calling the {@link java.lang.instrument.Instrumentation#retransformClasses} method on all + * classes that were already loaded. + *
  10. + *
+ * + *

+ * The third step is needed as the JDK exposes some API through interfaces that have different (internal) implementations + * depending on the JVM host platform. As we cannot instrument an interfaces, we find its concrete implementation. + * A prime example is {@link FileSystemProvider}, which has different implementations (e.g. {@code UnixFileSystemProvider} or + * {@code WindowsFileSystemProvider}). At runtime, we find the implementation class which is currently used by the JVM, and add + * its methods to the set of methods to instrument. See e.g. {@link EntitlementInitialization#fileSystemProviderChecks}. + *

+ * + * @param inst the JVM instrumentation class instance + */ // Note: referenced by agent reflectively public static void initialize(Instrumentation inst) throws Exception { manager = initChecker(); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java index ece51a8414b70..dc03e31194e87 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java @@ -22,6 +22,26 @@ record InstrumentationInfo(MethodKey targetMethod, CheckMethod checkMethod) {} Instrumenter newInstrumenter(Class clazz, Map methods); + /** + * This method uses the method names of the provided class to identify the JDK method to instrument; it examines all methods prefixed + * by {@code check$}, and parses the rest of the name to extract the JDK method: + * + * @param clazz the class to inspect to find methods to instrument + * @throws ClassNotFoundException if the class is not defined or cannot be inspected + */ Map lookupMethods(Class clazz) throws ClassNotFoundException; InstrumentationInfo lookupImplementationMethod( diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java index c94dc70ae6262..13db1d805d278 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java @@ -10,5 +10,30 @@ package org.elasticsearch.entitlement.instrumentation; public interface Instrumenter { + + /** + * Instruments a class by adding a prologue to check for entitlements to a set of its methods. + * The prologue: + *
    + *
  1. + * gets the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} instance from the + * {@link org.elasticsearch.entitlement.bridge.EntitlementCheckerHandle} holder; + *
  2. + *
  3. + * identifies the caller class and pushes it onto the stack; + *
  4. + *
  5. + * forwards the instrumented function parameters; + *
  6. + *
  7. + * calls the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} method corresponding to the method it is injected into + * (e.g. {@code check$java_net_DatagramSocket$receive} for {@link java.net.DatagramSocket#receive}). + *
  8. + *
+ * @param className the name of the class to instrument + * @param classfileBuffer its bytecode + * @param verify whether we should verify the bytecode before and after instrumentation + * @return the instrumented class bytes + */ byte[] instrumentClass(String className, byte[] classfileBuffer, boolean verify); } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java new file mode 100644 index 0000000000000..b5738a00f4bff --- /dev/null +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java @@ -0,0 +1,189 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + *

Implements the Elasticsearch Entitlement System

+ * The Entitlement system has some basic ingredients: + *
    + *
  • + * Load policiesfor the various layers + *
  • + *
  • + * Instrumentationof JDK methods that perform sensitive actions to inject calls to a checker + *
  • + *
  • + * Caller identification: identify the class that is responsible for the sensitive action + *
  • + *
  • + * Map to a policy: find the set of entitlements granted to the caller. We do that by identifying the "layer" (server, + * agent, ES plugin/module) and the associated policy, and the caller class module to identify which scope within the policy. + *
  • + *
  • + * Check: does the set of entitlements grant the caller to perform the sensitive action? In other words, is the caller + * entitled to call that JDK method? + *
  • + *
+ * + *

Load policies

+ * + *

+ * Policies for ES plugins and modules are stored in a YAML file bundled with the module/plugin + * (@see README.md for details). The files are + * extracted from the bundles and parsed during Elasticsearch initialization ({@code Elasticsearch#initPhase2}); at the same time, we parse + * both plugin and server policy patches from the command line. Patches to plugin policies are applied immediately, the server policy patch + * is passed down to {@link org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap}. + *

+ *

+ * The server and agent (APM) policies are created in EntitlementInitialization, just before the creation of + * {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager}. The server policy patch (if any) is read from the + * {@link org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap} arguments and applied here. + *

+ *

+ * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization} creates + * {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager} passing down the policies it just created (server and agent) and the + * plugin policies it read from {@link org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap}. + *

+ * + *

Instrumentation

+ *

+ * Instrumentation happens dynamically via a Java Agent ({@code EntitlementAgent}, see the {@code agent} subproject). + *

+ *

+ * The Agent is loaded dynamically by {@link org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap}. We load the agent dynamically + * because we don't want to define additional permissions that server would need; we perform several sensitive actions once during + * Elasticsearch initialization. By initializing entitlements after we have performed those actions, we are able to never allow certain + * actions, like process execution. An additional benefit is that we are able to collect all the information needed before creating the + * entitlement objects, so they can be immutable. + *

+ *

+ * {@code EntitlementAgent} creates {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization} and calls + * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization#initialize(java.lang.instrument.Instrumentation)} + * on it, both by using reflection. Agents are loaded into the unnamed module, which makes module exports awkward. To work around this, + * we keep minimal code in the agent itself, and instead use reflection to call into this library. + * {@link org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap} uses {@link java.lang.Module#addExports} to export + * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization} to the agent and make it available. + *

+ *

+ * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization} loads an + * {@link org.elasticsearch.entitlement.instrumentation.InstrumentationService} instance. + * {@link org.elasticsearch.entitlement.instrumentation.InstrumentationService} is an interface that encapsulates all bytecode manipulation + * operations. We use SPI to load an implementation for it; currently, the implementation uses ASM, and it is located in the + * {@code asm-provider} subproject. + *

+ * + *

How we identify the methods to instrument

+ * + *

+ * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization} builds the set of methods to instrument using + * {@link org.elasticsearch.entitlement.instrumentation.InstrumentationService#lookupMethods} on the version-specific subclass of + * {@link org.elasticsearch.entitlement.bridge.EntitlementChecker}. + * {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} is the interface that contains the definition of all the check methods; + * it needs to be accessible by both this project and the code injected by the agent, therefore is located in a small, self-contained + * library ({@see the {@code bridge} subproject}). + *

+ *

+ * See {@link org.elasticsearch.entitlement.instrumentation.InstrumentationService#lookupMethods} for details. + *

+ * + *

How that works across different Java versions

+ *

+ * The {@code bridge} subproject uses multi-release jars via the {@code mrjar} plugin, which makes it is possible to specify classes for + * specific Java versions in specific {@code src} folders (e.g. {@code main23} for classes available to Java 23+). + *

+ *

+ * See the {@code bridge} subproject for details. + *

+ * + *

Prologue injection

+ * + *

+ * Agents get access to the Java instrumentation API by receiving a {@link java.lang.instrument.Instrumentation} instance, which we pass + * to {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization#initialize} to setup code needed to transform classes + * as they get loaded. See {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization#initialize} for details. + *

+ *

+ * Our implementation instrument classes by adding a prologue to the methods identified in + * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization} (see previous section). + * See {@link org.elasticsearch.entitlement.instrumentation.Instrumenter#instrumentClass} for details. + *

+ * + *

Caller identification

+ * + *

+ * In order to verify if a method is entitled to perform an action, we need to identify the right policy to check; the first step here is + * to identify the caller. This is done in the injected prologue, via a helper function + * {@link org.elasticsearch.entitlement.bridge.Util#getCallerClass}, which performs a limited stack walk (it "walks up" 2 frames) to + * identify the caller class. This utility is similar to {@link java.lang.StackWalker#getCallerClass}, but handles the case in which it + * is called from the "outermost frame", e.g. when a method is called from a native frame. + *

+ *

+ * Our plan here was to avoid the stack walking entirely for the most cases, by using the {@code CallerSensitive} attribute together with + * the {@code Reflection.getCallerClass()} method; the current implementation (8.18/9.0) does not include this optimization, but we plan to + * introduce that in a later release. + *

+ * + *

Map to a policy

+ * + *

Identify the "layer"

+ * + *

+ * The first step to find the set of entitlements granted to the caller class is to find the "layer" that hosts the class/module. + * Each layer may have a policy attached to it (1-1* relationship). + *

+ *

+ * This starts during Elasticsearch initialization ({@code initPhase2}), just after policies are parsed but before entitlements are + * initialized via {@link org.elasticsearch.entitlement.bootstrap.EntitlementBootstrap}, through a new class named {@code PluginsLoader}. + * Before 8.18, {@code PluginsServices} (created as part of {@code Node} initialization in {@code initPhase3}) had 2 concerns: + * create the "infrastructure" to load an ES plugin (or ES module), e.g. the module layer and class loader, and actually load the main + * plugin class and create an instance of it for the plugin. + * Now the first concern (create the module layer and class loader) has been refactored and moved to {@code PluginsLoader}, so it can + * happen separately and earlier, in Phase 2, before entitlements are initialized. + *

+ *

+ * The module layers and class loaders are used to map a class to a layer (via the {@code PluginsResolver} class): we use them to build a + * Module -> Plugin name (String) map. For modularized plugins we use the list of modules defined in the module layer; for the + * non-modularized ones, we use the unnamed module which is unique to each plugin classloader. + *

+ *

+ * This map is then passed down and stored by {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager}. Alongside this map, + * {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager} builds a set of references to modules + * that belong to what we call the "system layer", i.e. the layer containing what we consider system modules, and the set of modules + * that we consider belonging to the "server layer". + * {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager} uses this info to identify the layer, and therefore the policy and + * entitlements, for the caller class. + *

+ *

+ * See {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager} for details. + *

+ * + *

Checks

+ *

+ * The injected prologue calls a {@code check$} method on {@link org.elasticsearch.entitlement.bridge.EntitlementChecker}; its + * implementation (normally on {@link org.elasticsearch.entitlement.runtime.api.ElasticsearchEntitlementChecker}, unless it is a + * version-specific method) calls the appropriate methods on {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager}, + * forwarding the caller class and a specific set of arguments. These methods all start with check, roughly matching an entitlement type + * (e.g. {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager#checkInboundNetworkAccess}, + * {@link org.elasticsearch.entitlement.runtime.policy.PolicyManager#checkFileRead}). + *

+ *

+ * Most of the entitlements are "flag" entitlements: when present, it grants the caller the right to perform an action (or a set of + * actions); when it's not present, the actions associated with it are denied. Checking is simply a fact checking if the entitlement type + * is present or not. + * The two exceptions are system properties, where we further get the instance of the entitlement for the + * {@link org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement} class, and we check if it contains + * that specific property name, and filesystem access, which is treated separately for convenience and performance reasons. + * See {@link org.elasticsearch.entitlement.runtime.policy.FileAccessTree} for details. + *

+ *

+ * A final special cases that short circuit the checks (resulting in a "trivially allowed" case) is when the caller is null is the special + * {@code NO_CLASS} tag class - this happens if there are no frames in the call stack, e.g. when a call originated directly from native + * code (the JVM itself, a callback stub, a debugger, ...). + *

+ */ +package org.elasticsearch.entitlement; diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java index 0e35ef0f0c72e..d03f620978bb4 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java @@ -36,6 +36,72 @@ import static org.elasticsearch.entitlement.runtime.policy.FileUtils.PATH_ORDER; import static org.elasticsearch.entitlement.runtime.policy.entitlements.FilesEntitlement.Mode.READ_WRITE; +/** + *

+ * This structure facilitates looking up files entitlements for a particular component+module combination, given that grants can occur + * at the directory level in addition to the individual file level. + *

+ *

+ * Broadly, this class operates on strings rather than abstractions like {@link java.io.File} or {@link Path}, since those can have + * behaviour that varies by platform in surprising ways; using strings makes the behaviour predictable. The strings are produced by a + * method called {@link FileAccessTree#normalizePath} to make sure they are absolute paths with consistent separator characters. + *

+ *

+ * Internally, it does not use a tree data structure; the name "tree" refers to the tree structure of the filesystem, not the choice of + * data structure. It takes advantage of the fact that, after normalization, the name of a directory containing a file is a prefix of + * the file's own name. The permissions are maintained in several sorted arrays of {@link String}, and permission checks are implemented + * as a binary search within these arrays. + *

+ *

+ * We want the binary search to locate the relevant entry immediately, either because there's an exact match, or else because there's no + * exact match and the binary search points us as an entry for the containing directory. This seems straightforward if the paths are + * absolute and sorted, but there are several subtleties. + *

+ *

+ * Firstly, there could be intervening siblings; for example, in {@code ["/a", "/a/b"]}, if we look up {@code "/a/c"}, the binary search + * will land on {@code "/a/b"}, which is not a relevant entry for {@code "/a/c"}. The solution here is (1) segregate the read and write + * permissions so that each array covers one homogeneous kind of permission, and (2) prune the list so redundant child entries are removed + * when they are already covered by parent entries. In our example, this pruning process would produce the array {@code ["/a"]} only, + * and so the binary search for {@code "/a/c"} lands on the relevant entry for the containing directory. + *

+ *

+ * Secondly, the path separator (whether slash or backslash) sorts after the dot character. This means, for example, if the array contains + * {@code ["/a", "/a.xml"]} and we look up {@code "/a/b"} using normal string comparison, the binary search would land on {@code "/a.xml"}, + * which is neither an exact match nor a containing directory, and so the lookup would incorrectly report no match, even though + * {@code "/a"} is in the array. To fix this, we define {@link FileUtils#PATH_ORDER} which sorts path separators before any other + * character. In the example, this would cause {@code "/a/b"} to sort between {@code "/a"} and {@code "/a.xml"} so that it correctly + * finds {@code "/a"}. + *

+ *

+ * With the paths pruned, sorted, and segregated by permission, each binary search has the following properties: + *

    + *
  • + * If an exact match exists, it will be found at the returned index + *
  • + *
  • + * Else, if a containing folder exists, it will be found immediately before the returned index + *
  • + *
  • + * Else, there is no match + *
  • + *
+ *

+ *

+ * Permission is granted if both: + *

    + *
  • + * there is no match in exclusivePaths, and + *
  • + *
  • + * there is a match in the array corresponding to the desired operation (read or write). + *
  • + *
+ *

+ *

+ * Some additional care is required in the unit tests for this code, since it must also run on Windows where the separator is a + * backslash and absolute paths don't start with a separator. See {@code FileAccessTreeTests#testDuplicateExclusivePaths} for an example. + *

+ */ public final class FileAccessTree { /** @@ -107,8 +173,18 @@ static void validateExclusivePaths(List exclusivePaths) { private static final Logger logger = LogManager.getLogger(FileAccessTree.class); private static final String FILE_SEPARATOR = getDefaultFileSystem().getSeparator(); + /** + * lists paths that are forbidden for this component+module because some other component has granted exclusive access to one of its + * modules + */ private final String[] exclusivePaths; + /** + * lists paths for which the component has granted read or read_write access to the module + */ private final String[] readPaths; + /** + * lists paths for which the component has granted read_write access to the module + */ private final String[] writePaths; private FileAccessTree( diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index 75e098a95902d..ce8eb9a1c7697 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -59,6 +59,68 @@ import static java.util.zip.ZipFile.OPEN_READ; import static org.elasticsearch.entitlement.bridge.Util.NO_CLASS; +/** + * This class is responsible for finding the "layer" (system, server, plugin, agent) for a caller class to check, retrieve the policy + * and entitlements for that layer, and check them against the action(s) the caller wants to perform. + *

+ * To find a layer: + *
    + *
  • + * For plugins, we use the Module -> Plugin name (String) passed to the ctor + *
  • + *
  • + * For the system layer, we build a set ({@link PolicyManager#SYSTEM_LAYER_MODULES}) of references to modules that belong that layer, i.e. + * the layer containing what we consider system modules. These are the modules that: + *
      + *
    • + * are in the boot module layer ({@link ModuleLayer#boot()}); + *
    • + *
    • + * are defined in {@link ModuleFinder#ofSystem()}; + *
    • + *
    • + * are not in the ({@link PolicyManager#MODULES_EXCLUDED_FROM_SYSTEM_MODULES}) (currently: {@code java.desktop}) + *
    • + *
    + *
  • + *
  • + * For the server layer, we build a set ({@link PolicyManager#SERVER_LAYER_MODULES}) as the set of modules that are in the boot module + * layer but not in the system layer. + *
  • + *
+ *

+ * When a check is performed (e.g. {@link PolicyManager#checkExitVM(Class)}, we get the module the caller class belongs to via + * {@link Class#getModule} and try (in order) to see if that class belongs to: + *

    + *
  1. + * The system layer - if a module is contained in {@link PolicyManager#SYSTEM_LAYER_MODULES} + *
  2. + *
  3. + * The server layer - if a module is contained in {@link PolicyManager#SERVER_LAYER_MODULES} + *
  4. + *
  5. + * One of the plugins or modules layer - if the module is present in the {@code PluginsResolver} map + *
  6. + *
  7. + * A known agent (APM) + *
  8. + *
  9. + * Something else + *
  10. + *
+ *

+ * Once it has a layer, this class maps it to a policy and check the action performed by the caller class against its entitlements, + * either allowing it to proceed or raising a {@link NotEntitledException} if the caller class is not entitled to perform the action. + *

+ *

+ * All these methods start in the same way: the layers identified in the previous section are used to establish if and how to check: + * If the caller class belongs to {@link PolicyManager#SYSTEM_LAYER_MODULES}, no check is performed (the call is trivially allowed, see + * {@link PolicyManager#isTriviallyAllowed}). + * Otherwise, we lazily compute and create a {@link PolicyManager.ModuleEntitlements} record (see + * {@link PolicyManager#computeEntitlements}). The record is cached so it can be used in following checks, stored in a + * {@code Module -> ModuleEntitlement} map. + *

+ */ public class PolicyManager { /** * Use this if you don't have a {@link ModuleEntitlements} in hand. @@ -74,6 +136,22 @@ public class PolicyManager { static final Set MODULES_EXCLUDED_FROM_SYSTEM_MODULES = Set.of("java.desktop"); /** + * This class contains all the entitlements by type, plus the {@link FileAccessTree} for the special case of filesystem entitlements. + *

+ * We use layers when computing {@link ModuleEntitlements}; first, we check whether the module we are building it for is in the + * server layer ({@link PolicyManager#SERVER_LAYER_MODULES}) (*). + * If it is, we use the server policy, using the same caller class module name as the scope, and read the entitlements for that scope. + * Otherwise, we use the {@code PluginResolver} to identify the correct plugin layer and find the policy for it (if any). + * If the plugin is modular, we again use the same caller class module name as the scope, and read the entitlements for that scope. + * If it's not, we use the single {@code ALL-UNNAMED} scope – in this case there is one scope and all entitlements apply + * to all the plugin code. + *

+ *

+ * (*) implementation detail: this is currently done in an indirect way: we know the module is not in the system layer + * (otherwise the check would have been already trivially allowed), so we just check that the module is named, and it belongs to the + * boot {@link ModuleLayer}. We might want to change this in the future to make it more consistent/easier to maintain. + *

+ * * @param componentName the plugin name; or else one of the special component names * like {@link #SERVER_COMPONENT_NAME} or {@link #APM_AGENT_COMPONENT_NAME}. */ From c13d156e925faf561c624114afa74ae51b393b76 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Thu, 17 Apr 2025 18:24:04 +0200 Subject: [PATCH 2/7] Fixes --- .../entitlement/runtime/policy/FileAccessTree.java | 4 ---- .../entitlement/runtime/policy/PolicyManager.java | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java index d03f620978bb4..4272b4876b6b0 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/FileAccessTree.java @@ -72,7 +72,6 @@ * character. In the example, this would cause {@code "/a/b"} to sort between {@code "/a"} and {@code "/a.xml"} so that it correctly * finds {@code "/a"}. *

- *

* With the paths pruned, sorted, and segregated by permission, each binary search has the following properties: *

    *
  • @@ -85,8 +84,6 @@ * Else, there is no match *
  • *
- *

- *

* Permission is granted if both: *

    *
  • @@ -96,7 +93,6 @@ * there is a match in the array corresponding to the desired operation (read or write). *
  • *
- *

*

* Some additional care is required in the unit tests for this code, since it must also run on Windows where the separator is a * backslash and absolute paths don't start with a separator. See {@code FileAccessTreeTests#testDuplicateExclusivePaths} for an example. diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index ce8eb9a1c7697..ccef870a7c25a 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -62,7 +62,7 @@ /** * This class is responsible for finding the "layer" (system, server, plugin, agent) for a caller class to check, retrieve the policy * and entitlements for that layer, and check them against the action(s) the caller wants to perform. - *

+ *

* To find a layer: *

    *
  • From 7a83affa8113a5cb87e77b32dded3545824ce555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Fri, 18 Apr 2025 08:49:02 +0200 Subject: [PATCH 3/7] Apply suggestions from code review Co-authored-by: Patrick Doyle <810052+prdoyle@users.noreply.github.com> --- .../entitlement/instrumentation/Instrumenter.java | 2 +- .../org/elasticsearch/entitlement/package-info.java | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java index 13db1d805d278..b6a0a18c3b74b 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/Instrumenter.java @@ -12,7 +12,7 @@ public interface Instrumenter { /** - * Instruments a class by adding a prologue to check for entitlements to a set of its methods. + * Instruments the appropriate methods of a class by adding a prologue that checks for entitlements. * The prologue: *
      *
    1. diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java index b5738a00f4bff..2f216b6eaf0c3 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java @@ -175,10 +175,17 @@ * Most of the entitlements are "flag" entitlements: when present, it grants the caller the right to perform an action (or a set of * actions); when it's not present, the actions associated with it are denied. Checking is simply a fact checking if the entitlement type * is present or not. - * The two exceptions are system properties, where we further get the instance of the entitlement for the + * There are two entitlements that are not simple flags: + *
        + *
      • system properties, where we further get the instance of the entitlement for the * {@link org.elasticsearch.entitlement.runtime.policy.entitlements.WriteSystemPropertiesEntitlement} class, and we check if it contains - * that specific property name, and filesystem access, which is treated separately for convenience and performance reasons. + * that specific property name, and + *
      • + *
      • + * file access, which is treated separately for convenience and performance reasons. * See {@link org.elasticsearch.entitlement.runtime.policy.FileAccessTree} for details. + *
      • + *
          *

          *

          * A final special cases that short circuit the checks (resulting in a "trivially allowed" case) is when the caller is null is the special From 82beb4c2f43a58d64340166d3908b01f0ce7ef4d Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Fri, 18 Apr 2025 09:05:22 +0200 Subject: [PATCH 4/7] PR comments --- .../entitlement/bridge/package-info.java | 7 ++++++ .../EntitlementInitialization.java | 5 ++-- .../InstrumentationService.java | 6 +++++ .../entitlement/package-info.java | 9 +------ .../runtime/policy/PolicyManager.java | 24 +++++++++---------- 5 files changed, 29 insertions(+), 22 deletions(-) diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java index 21f005cb45520..fd3c71836fb7c 100644 --- a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java @@ -18,6 +18,13 @@ * method. *

          *

          + * NOTE: a subtle point that is worth noting: the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} + * interface needs to be accessible from instrumented methods, hence the need to define it here in this project. But the reference held + * by {@link org.elasticsearch.entitlement.bridge.EntitlementCheckerHandle} is an instance of a concrete class that need not be accessible. + * Once things get underway, the instrumented code is directly calling a method of {@code ElasticsearchEntitlementChecker}, a class that + * it could not resolve, by referencing that method through the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} interface. + *

          + *

          * In order to work across multiple Java versions, this project uses multi-release jars via the {@code mrjar} plugin, which makes it is * possible to specify classes for specific Java versions in specific {@code src} folders (e.g. {@code main23} for classes available to * Java 23+). diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index 8db8fda53754c..0bb5603db63fc 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -121,7 +121,6 @@ public static EntitlementChecker checker() { * classes that were already loaded. * *

    - * *

    * The third step is needed as the JDK exposes some API through interfaces that have different (internal) implementations * depending on the JVM host platform. As we cannot instrument an interfaces, we find its concrete implementation. @@ -129,10 +128,12 @@ public static EntitlementChecker checker() { * {@code WindowsFileSystemProvider}). At runtime, we find the implementation class which is currently used by the JVM, and add * its methods to the set of methods to instrument. See e.g. {@link EntitlementInitialization#fileSystemProviderChecks}. *

    + *

    + * NOTE: this method is referenced by the agent reflectively + *

    * * @param inst the JVM instrumentation class instance */ - // Note: referenced by agent reflectively public static void initialize(Instrumentation inst) throws Exception { manager = initChecker(); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java index dc03e31194e87..48e7288c19d82 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/instrumentation/InstrumentationService.java @@ -39,6 +39,12 @@ record InstrumentationInfo(MethodKey targetMethod, CheckMethod checkMethod) {} * {@link org.elasticsearch.entitlement.bridge.EntitlementChecker#check$java_lang_ClassLoader$} *
  • *
+ *

+ * NOTE: look up of methods using this convention is the primary way we use to identify which methods to instrument, + * but other methods can be added to the map of methods to instrument. See + * {@link org.elasticsearch.entitlement.initialization.EntitlementInitialization#initialize} for details. + *

+ * * @param clazz the class to inspect to find methods to instrument * @throws ClassNotFoundException if the class is not defined or cannot be inspected */ diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java index b5738a00f4bff..42eda1cab689e 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java @@ -118,14 +118,7 @@ *

* In order to verify if a method is entitled to perform an action, we need to identify the right policy to check; the first step here is * to identify the caller. This is done in the injected prologue, via a helper function - * {@link org.elasticsearch.entitlement.bridge.Util#getCallerClass}, which performs a limited stack walk (it "walks up" 2 frames) to - * identify the caller class. This utility is similar to {@link java.lang.StackWalker#getCallerClass}, but handles the case in which it - * is called from the "outermost frame", e.g. when a method is called from a native frame. - *

- *

- * Our plan here was to avoid the stack walking entirely for the most cases, by using the {@code CallerSensitive} attribute together with - * the {@code Reflection.getCallerClass()} method; the current implementation (8.18/9.0) does not include this optimization, but we plan to - * introduce that in a later release. + * {@link org.elasticsearch.entitlement.bridge.Util#getCallerClass}, which performs a limited stack walk. *

* *

Map to a policy

diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java index ccef870a7c25a..9ee6708c6a3bf 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/policy/PolicyManager.java @@ -60,17 +60,17 @@ import static org.elasticsearch.entitlement.bridge.Util.NO_CLASS; /** - * This class is responsible for finding the "layer" (system, server, plugin, agent) for a caller class to check, retrieve the policy - * and entitlements for that layer, and check them against the action(s) the caller wants to perform. + * This class is responsible for finding the component (system, server, plugin, agent) for a caller class to check, + * retrieve the policy and entitlements for that component, and check them against the action(s) the caller wants to perform. *

- * To find a layer: + * To find a component: *

    *
  • * For plugins, we use the Module -> Plugin name (String) passed to the ctor *
  • *
  • - * For the system layer, we build a set ({@link PolicyManager#SYSTEM_LAYER_MODULES}) of references to modules that belong that layer, i.e. - * the layer containing what we consider system modules. These are the modules that: + * For the system component, we build a set ({@link PolicyManager#SYSTEM_LAYER_MODULES}) of references to modules that belong that + * component, i.e. the component containing what we consider system modules. These are the modules that: *
      *
    • * are in the boot module layer ({@link ModuleLayer#boot()}); @@ -84,8 +84,8 @@ *
    *
  • *
  • - * For the server layer, we build a set ({@link PolicyManager#SERVER_LAYER_MODULES}) as the set of modules that are in the boot module - * layer but not in the system layer. + * For the server component, we build a set ({@link PolicyManager#SERVER_LAYER_MODULES}) as the set of modules that are in the boot module + * layer but not in the system component. *
  • *
*

@@ -93,13 +93,13 @@ * {@link Class#getModule} and try (in order) to see if that class belongs to: *

    *
  1. - * The system layer - if a module is contained in {@link PolicyManager#SYSTEM_LAYER_MODULES} + * The system component - if a module is contained in {@link PolicyManager#SYSTEM_LAYER_MODULES} *
  2. *
  3. - * The server layer - if a module is contained in {@link PolicyManager#SERVER_LAYER_MODULES} + * The server component - if a module is contained in {@link PolicyManager#SERVER_LAYER_MODULES} *
  4. *
  5. - * One of the plugins or modules layer - if the module is present in the {@code PluginsResolver} map + * One of the plugins or modules - if the module is present in the {@code PluginsResolver} map *
  6. *
  7. * A known agent (APM) @@ -109,11 +109,11 @@ *
  8. *
*

- * Once it has a layer, this class maps it to a policy and check the action performed by the caller class against its entitlements, + * Once it has a component, this class maps it to a policy and check the action performed by the caller class against its entitlements, * either allowing it to proceed or raising a {@link NotEntitledException} if the caller class is not entitled to perform the action. *

*

- * All these methods start in the same way: the layers identified in the previous section are used to establish if and how to check: + * All these methods start in the same way: the components identified in the previous section are used to establish if and how to check: * If the caller class belongs to {@link PolicyManager#SYSTEM_LAYER_MODULES}, no check is performed (the call is trivially allowed, see * {@link PolicyManager#isTriviallyAllowed}). * Otherwise, we lazily compute and create a {@link PolicyManager.ModuleEntitlements} record (see From 6f85e2004f97151743100db9b85705ce5fb3fb12 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Fri, 18 Apr 2025 09:06:41 +0200 Subject: [PATCH 5/7] Fix --- .../main/java/org/elasticsearch/entitlement/package-info.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java index e6a2c89d655d1..b27898a22c835 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java @@ -168,6 +168,7 @@ * Most of the entitlements are "flag" entitlements: when present, it grants the caller the right to perform an action (or a set of * actions); when it's not present, the actions associated with it are denied. Checking is simply a fact checking if the entitlement type * is present or not. + *

* There are two entitlements that are not simple flags: *
    *
  • system properties, where we further get the instance of the entitlement for the @@ -178,8 +179,7 @@ * file access, which is treated separately for convenience and performance reasons. * See {@link org.elasticsearch.entitlement.runtime.policy.FileAccessTree} for details. *
  • - *
      - *

      + *
    *

    * A final special cases that short circuit the checks (resulting in a "trivially allowed" case) is when the caller is null is the special * {@code NO_CLASS} tag class - this happens if there are no frames in the call stack, e.g. when a call originated directly from native From 68dfda475df7a007b11d0e53a73914e2b2fed8cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Tue, 22 Apr 2025 09:28:56 +0200 Subject: [PATCH 6/7] Apply suggestions from code review Co-authored-by: Ryan Ernst --- .../org/elasticsearch/entitlement/bridge/package-info.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java index fd3c71836fb7c..6ee7522dce9e1 100644 --- a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java @@ -13,7 +13,7 @@ * It's a minimal shim that is patched into the {@code java.base} module so that it is callable from the class library methods instrumented * by the agent. The shim retains a {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} instance (inside its * {@link org.elasticsearch.entitlement.bridge.EntitlementCheckerHandle} holder) and forwards the entitlement checks to the main library, - * which is loaded normally. + * that exists in the system classloader. * {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} holds all the entitlements check definitions, one for each instrumented * method. *

    @@ -28,7 +28,7 @@ * In order to work across multiple Java versions, this project uses multi-release jars via the {@code mrjar} plugin, which makes it is * possible to specify classes for specific Java versions in specific {@code src} folders (e.g. {@code main23} for classes available to * Java 23+). - * We use the mrjar plugin in a particular way, merging all the classes into the same jar. Therefore, we want to prefix the file name + * All the versioned Java classes are merged into the bridge jar. Therefore, we must prefix the class name * with the version, e.g. {@code Java23EntitlementCheckerHandle} and {@code Java23EntitlementChecker}. *

    *

    From 96e3380463a9ff9bc3904e283c7eb12e49dfb1f5 Mon Sep 17 00:00:00 2001 From: Lorenzo Dematte Date: Tue, 22 Apr 2025 09:40:58 +0200 Subject: [PATCH 7/7] PR comments --- .../entitlement/bridge/package-info.java | 44 ------------------- .../entitlement/package-info.java | 37 ++++++++++++++-- 2 files changed, 34 insertions(+), 47 deletions(-) diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java index 6ee7522dce9e1..2d37cbc7b8399 100644 --- a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/package-info.java @@ -8,7 +8,6 @@ */ /** - *

    * Contains classes that need to be used directly from instrumented methods. * It's a minimal shim that is patched into the {@code java.base} module so that it is callable from the class library methods instrumented * by the agent. The shim retains a {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} instance (inside its @@ -16,14 +15,6 @@ * that exists in the system classloader. * {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} holds all the entitlements check definitions, one for each instrumented * method. - *

    - *

    - * NOTE: a subtle point that is worth noting: the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} - * interface needs to be accessible from instrumented methods, hence the need to define it here in this project. But the reference held - * by {@link org.elasticsearch.entitlement.bridge.EntitlementCheckerHandle} is an instance of a concrete class that need not be accessible. - * Once things get underway, the instrumented code is directly calling a method of {@code ElasticsearchEntitlementChecker}, a class that - * it could not resolve, by referencing that method through the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} interface. - *

    *

    * In order to work across multiple Java versions, this project uses multi-release jars via the {@code mrjar} plugin, which makes it is * possible to specify classes for specific Java versions in specific {@code src} folders (e.g. {@code main23} for classes available to @@ -31,40 +22,5 @@ * All the versioned Java classes are merged into the bridge jar. Therefore, we must prefix the class name * with the version, e.g. {@code Java23EntitlementCheckerHandle} and {@code Java23EntitlementChecker}. *

    - *

    - * At runtime, we identify and instantiate the correct class using the runtime Java version to prepend the correct prefix to the class - * name (see {@code EntitlementInitialization#getVersionSpecificCheckerClass}). - *

    - *

    - * These different classes are needed to hold entitlements check definitions that are specific to a Java version. - * As an example, consider the Linker API. - *

    - *

    - * Note: the current version of Elasticsearch supports Java 21+; this is only an example (taken from the 8.x branch) that - * illustrates a complex scenario. - *

    - *

    - * The API went through multiple previews, and therefore changes between Java 19, 20 and 21; in order to support this correctly on these - * versions, we should introduce 2 utility interfaces, "preview" and "stable". - * For example, for the Java 20 specific signatures and functions, we would create {@code Java20StableEntitlementChecker} and - * {@code Java20PreviewEntitlementChecker}. - *

    - *

    - * The linker API in Java 20 introduces the final form for {@code downcallHandle}, which has different argument types from the one in - * Java 19. To instrument and check it, we would add a - * {@code check$jdk_internal_foreign_abi_AbstractLinker$downcallHandle(FunctionDescriptor, Linker.Option...)} method for it to the - * {@code Java20StableEntitlementChecker} interface, which extends {@link org.elasticsearch.entitlement.bridge.EntitlementChecker}. - * This interface would then be used by both the Java 20 specific interface ({@code Java20EntitlementChecker}) and any interface for newer - * Java versions (e.g. {@code Java21EntitlementChecker}, which extends {@code Java20StableEntitlementChecker}): this way when we run on - * either Java 20, Java 21, or following versions, we always instrument {@code downcallHandle} with the Java 20+ signature defined in - * {@code Java20StableEntitlementChecker}. - * Java 20 also introduces the {@code upcallStub} function; this function is not in its final form, as it has different parameters in the - * following (21+) previews and in the final API. - * In this case, we would add a {@code jdk_internal_foreign_abi_AbstractLinker$upcallStub(MethodHandle, FunctionDescriptor, SegmentScope)} - * function to the {@code Java20PreviewEntitlementChecker} interface. {@code Java20EntitlementChecker} would inherit from this interface - * too, but {@code Java21EntitlementChecker} and following would not. This way when we run on Java 20 we would instrument {@code upcallStub} - * with the Java 20 signature {@code (FunctionDescriptor, Linker.Option...)}, but we would not when we run on following (Java 21+) versions. - * Those will have the newer (final) {@code upcallStub} definition introduced in {@code Java21EntitlementChecker}. - *

    */ package org.elasticsearch.entitlement.bridge; diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java index b27898a22c835..fd6638703725c 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/package-info.java @@ -8,8 +8,7 @@ */ /** - *

    Implements the Elasticsearch Entitlement System

    - * The Entitlement system has some basic ingredients: + * Implements the Elasticsearch Entitlement System. The Entitlement system has some basic ingredients: *
      *
    • * Load policiesfor the various layers @@ -97,7 +96,39 @@ * specific Java versions in specific {@code src} folders (e.g. {@code main23} for classes available to Java 23+). *

      *

      - * See the {@code bridge} subproject for details. + * At runtime, we identify and instantiate the correct class using the runtime Java version to prepend the correct prefix to the class + * name, e.g. {@code Java21EntitlementChecker} for Java version 21 (see {@code EntitlementInitialization#getVersionSpecificCheckerClass}). + *

      + *

      + * These different classes are needed to hold entitlements check definitions that are specific to a Java version. + * As an example, consider the Linker API. + *

      + *

      + * Note: the current version of Elasticsearch supports Java 21+; this is only an example (taken from the 8.x branch) that + * illustrates a complex scenario. + *

      + *

      + * The API went through multiple previews, and therefore changes between Java 19, 20 and 21; in order to support this correctly on these + * versions, we should introduce 2 utility interfaces, "preview" and "stable". + * For example, for the Java 20 specific signatures and functions, we would create {@code Java20StableEntitlementChecker} and + * {@code Java20PreviewEntitlementChecker}. + *

      + *

      + * The linker API in Java 20 introduces the final form for {@code downcallHandle}, which has different argument types from the one in + * Java 19. To instrument and check it, we would add a + * {@code check$jdk_internal_foreign_abi_AbstractLinker$downcallHandle(FunctionDescriptor, Linker.Option...)} method for it to the + * {@code Java20StableEntitlementChecker} interface, which extends {@link org.elasticsearch.entitlement.bridge.EntitlementChecker}. + * This interface would then be used by both the Java 20 specific interface ({@code Java20EntitlementChecker}) and any interface for newer + * Java versions (e.g. {@code Java21EntitlementChecker}, which extends {@code Java20StableEntitlementChecker}): this way when we run on + * either Java 20, Java 21, or following versions, we always instrument {@code downcallHandle} with the Java 20+ signature defined in + * {@code Java20StableEntitlementChecker}. + * Java 20 also introduces the {@code upcallStub} function; this function is not in its final form, as it has different parameters in the + * following (21+) previews and in the final API. + * In this case, we would add a {@code jdk_internal_foreign_abi_AbstractLinker$upcallStub(MethodHandle, FunctionDescriptor, SegmentScope)} + * function to the {@code Java20PreviewEntitlementChecker} interface. {@code Java20EntitlementChecker} would inherit from this interface + * too, but {@code Java21EntitlementChecker} and following would not. This way when we run on Java 20 we would instrument {@code upcallStub} + * with the Java 20 signature {@code (FunctionDescriptor, Linker.Option...)}, but we would not when we run on following (Java 21+) versions. + * Those will have the newer (final) {@code upcallStub} definition introduced in {@code Java21EntitlementChecker}. *

      * *

      Prologue injection