Skip to content
Original file line number Diff line number Diff line change
@@ -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".
*/

/**
* <p>
* 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.
* </p>
* <p>
* 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}.
* </p>
* <p>
* 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}).
* </p>
* <p>
* These different classes are needed to hold entitlements check definitions that are specific to a Java version.
* As an example, consider the Linker API.
* </p>
* <p>
* <strong>Note:</strong> the current version of Elasticsearch supports Java 21+; this is only an example (taken from the 8.x branch) that
* illustrates a complex scenario.
* </p>
* <p>
* 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}.
* </p>
* <p>
* 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}.
* </p>
*/
package org.elasticsearch.entitlement.bridge;
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,40 @@ public static EntitlementChecker checker() {
return manager;
}

/**
* Initializes the Entitlement system:
* <ol>
* <li>
* Finds the version-specific subclass of {@link EntitlementChecker} to use
* </li>
* <li>
* Builds the set of methods to instrument using {@link InstrumentationService#lookupMethods}
* </li>
* <li>
* Augment this set “dynamically” using {@link InstrumentationService#lookupImplementationMethod}
* </li>
* <li>
* 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.
* </li>
* <li>
* 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.
* </li>
* </ol>
*
* <p>
* 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}.
* </p>
*
* @param inst the JVM instrumentation class instance
*/
// Note: referenced by agent reflectively
public static void initialize(Instrumentation inst) throws Exception {
manager = initChecker();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,26 @@ record InstrumentationInfo(MethodKey targetMethod, CheckMethod checkMethod) {}

Instrumenter newInstrumenter(Class<?> clazz, Map<MethodKey, CheckMethod> 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:
* <ul>
* <li>
* Instance methods have the fully qualified class name (with . replaced by _), followed by $, followed by the method name. Example:
* {@link org.elasticsearch.entitlement.bridge.EntitlementChecker#check$java_lang_Runtime$halt}
* </li>
* <li>
* Static methods have the fully qualified class name (with . replaced by _), followed by $$, followed by the method name. Example:
* {@link org.elasticsearch.entitlement.bridge.EntitlementChecker#check$java_lang_System$$exit}
* </li>
* <li>
* Constructors have the fully qualified class name (with . replaced by _), followed by $ and nothing else. Example:
* {@link org.elasticsearch.entitlement.bridge.EntitlementChecker#check$java_lang_ClassLoader$}
* </li>
* </ul>
* @param clazz the class to inspect to find methods to instrument
* @throws ClassNotFoundException if the class is not defined or cannot be inspected
*/
Map<MethodKey, CheckMethod> lookupMethods(Class<?> clazz) throws ClassNotFoundException;

InstrumentationInfo lookupImplementationMethod(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
* <ol>
* <li>
* gets the {@link org.elasticsearch.entitlement.bridge.EntitlementChecker} instance from the
* {@link org.elasticsearch.entitlement.bridge.EntitlementCheckerHandle} holder;
* </li>
* <li>
* identifies the caller class and pushes it onto the stack;
* </li>
* <li>
* forwards the instrumented function parameters;
* </li>
* <li>
* 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}).
* </li>
* </ol>
* @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);
}
Loading