Add java version variants of entitlements checker (#116878)

As each version of Java is released, there may be additional methods we
want to instrument for entitlements. Since new methods won't exist in
the base version of Java that Elasticsearch is compiled with, we need to
hava different classes and compilation for each version.

This commit adds a scaffolding for adding the classes for new versions
of Java. Unfortunately it requires several classes in different
locations. But hopefully these are infrequent enough that the
boilerplate is ok. We could consider adding a helper Gradle task to
templatize the new classes in the future if it is too cumbersome. Note
that the example for Java23 does not have anything meaningful in it yet,
it's only meant as an example until we find go through classes and
methods that were added after Java 21.
This commit is contained in:
Ryan Ernst 2024-11-22 07:40:06 -08:00 committed by GitHub
parent f6ac6e1c3b
commit b45564364b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 185 additions and 44 deletions

View file

@ -21,6 +21,7 @@ import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.compile.CompileOptions; import org.gradle.api.tasks.compile.CompileOptions;
import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.api.tasks.javadoc.Javadoc; import org.gradle.api.tasks.javadoc.Javadoc;
@ -87,6 +88,7 @@ public class MrjarPlugin implements Plugin<Project> {
String mainSourceSetName = SourceSet.MAIN_SOURCE_SET_NAME + javaVersion; String mainSourceSetName = SourceSet.MAIN_SOURCE_SET_NAME + javaVersion;
SourceSet mainSourceSet = addSourceSet(project, javaExtension, mainSourceSetName, mainSourceSets, javaVersion); SourceSet mainSourceSet = addSourceSet(project, javaExtension, mainSourceSetName, mainSourceSets, javaVersion);
configureSourceSetInJar(project, mainSourceSet, javaVersion); configureSourceSetInJar(project, mainSourceSet, javaVersion);
addJar(project, mainSourceSet, javaVersion);
mainSourceSets.add(mainSourceSetName); mainSourceSets.add(mainSourceSetName);
testSourceSets.add(mainSourceSetName); testSourceSets.add(mainSourceSetName);
@ -147,6 +149,14 @@ public class MrjarPlugin implements Plugin<Project> {
return sourceSet; return sourceSet;
} }
private void addJar(Project project, SourceSet sourceSet, int javaVersion) {
project.getConfigurations().register("java" + javaVersion);
TaskProvider<Jar> jarTask = project.getTasks().register("java" + javaVersion + "Jar", Jar.class, task -> {
task.from(sourceSet.getOutput());
});
project.getArtifacts().add("java" + javaVersion, jarTask);
}
private void configurePreviewFeatures(Project project, SourceSet sourceSet, int javaVersion) { private void configurePreviewFeatures(Project project, SourceSet sourceSet, int javaVersion) {
project.getTasks().withType(JavaCompile.class).named(sourceSet.getCompileJavaTaskName()).configure(compileTask -> { project.getTasks().withType(JavaCompile.class).named(sourceSet.getCompileJavaTaskName()).configure(compileTask -> {
CompileOptions compileOptions = compileTask.getOptions(); CompileOptions compileOptions = compileTask.getOptions();

View file

@ -36,6 +36,22 @@ import static org.objectweb.asm.Opcodes.INVOKESTATIC;
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;
public class InstrumenterImpl implements Instrumenter { public class InstrumenterImpl implements Instrumenter {
private static final String checkerClassDescriptor;
private static final String handleClass;
static {
int javaVersion = Runtime.version().feature();
final String classNamePrefix;
if (javaVersion >= 23) {
classNamePrefix = "Java23";
} else {
classNamePrefix = "";
}
String checkerClass = "org/elasticsearch/entitlement/bridge/" + classNamePrefix + "EntitlementChecker";
handleClass = checkerClass + "Handle";
checkerClassDescriptor = Type.getObjectType(checkerClass).getDescriptor();
}
/** /**
* To avoid class name collisions during testing without an agent to replace classes in-place. * To avoid class name collisions during testing without an agent to replace classes in-place.
*/ */
@ -269,13 +285,7 @@ public class InstrumenterImpl implements Instrumenter {
} }
protected void pushEntitlementChecker(MethodVisitor mv) { protected void pushEntitlementChecker(MethodVisitor mv) {
mv.visitMethodInsn( mv.visitMethodInsn(INVOKESTATIC, handleClass, "instance", "()" + checkerClassDescriptor, false);
INVOKESTATIC,
"org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle",
"instance",
"()Lorg/elasticsearch/entitlement/bridge/EntitlementChecker;",
false
);
} }
public record ClassFileInfo(String fileName, byte[] bytecodes) {} public record ClassFileInfo(String fileName, byte[] bytecodes) {}

View file

@ -7,19 +7,18 @@
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
apply plugin: 'elasticsearch.build' import org.elasticsearch.gradle.internal.precommit.CheckForbiddenApisTask
configurations { apply plugin: 'elasticsearch.build'
bridgeJar { apply plugin: 'elasticsearch.mrjar'
canBeConsumed = true
canBeResolved = false tasks.named('jar').configure {
// guarding for intellij
if (sourceSets.findByName("main23")) {
from sourceSets.main23.output
} }
} }
artifacts { tasks.withType(CheckForbiddenApisTask).configureEach {
bridgeJar(jar)
}
tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures' replaceSignatureFiles 'jdk-signatures'
} }

View file

@ -9,9 +9,6 @@
package org.elasticsearch.entitlement.bridge; package org.elasticsearch.entitlement.bridge;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/** /**
* Makes the {@link EntitlementChecker} available to injected bytecode. * Makes the {@link EntitlementChecker} available to injected bytecode.
*/ */
@ -35,27 +32,7 @@ public class EntitlementCheckerHandle {
* The {@code EntitlementInitialization} class is what actually instantiates it and makes it available; * The {@code EntitlementInitialization} class is what actually instantiates it and makes it available;
* here, we copy it into a static final variable for maximum performance. * here, we copy it into a static final variable for maximum performance.
*/ */
private static final EntitlementChecker instance; private static final EntitlementChecker instance = HandleLoader.load(EntitlementChecker.class);
static {
String initClazz = "org.elasticsearch.entitlement.initialization.EntitlementInitialization";
final Class<?> clazz;
try {
clazz = ClassLoader.getSystemClassLoader().loadClass(initClazz);
} catch (ClassNotFoundException e) {
throw new AssertionError("java.base cannot find entitlement initialziation", e);
}
final Method checkerMethod;
try {
checkerMethod = clazz.getMethod("checker");
} catch (NoSuchMethodException e) {
throw new AssertionError("EntitlementInitialization is missing checker() method", e);
}
try {
instance = (EntitlementChecker) checkerMethod.invoke(null);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
}
} }
// no construction // no construction

View file

@ -0,0 +1,40 @@
/*
* 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".
*/
package org.elasticsearch.entitlement.bridge;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class HandleLoader {
static <T extends EntitlementChecker> T load(Class<T> checkerClass) {
String initClassName = "org.elasticsearch.entitlement.initialization.EntitlementInitialization";
final Class<?> initClazz;
try {
initClazz = ClassLoader.getSystemClassLoader().loadClass(initClassName);
} catch (ClassNotFoundException e) {
throw new AssertionError("java.base cannot find entitlement initialization", e);
}
final Method checkerMethod;
try {
checkerMethod = initClazz.getMethod("checker");
} catch (NoSuchMethodException e) {
throw new AssertionError("EntitlementInitialization is missing checker() method", e);
}
try {
return checkerClass.cast(checkerMethod.invoke(null));
} catch (IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);
}
}
// no instance
private HandleLoader() {}
}

View file

@ -0,0 +1,12 @@
/*
* 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".
*/
package org.elasticsearch.entitlement.bridge;
public interface Java23EntitlementChecker extends EntitlementChecker {}

View file

@ -0,0 +1,27 @@
/*
* 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".
*/
package org.elasticsearch.entitlement.bridge;
/**
* Java23 variant of {@link EntitlementChecker} handle holder.
*/
public class Java23EntitlementCheckerHandle {
public static Java23EntitlementChecker instance() {
return Holder.instance;
}
private static class Holder {
private static final Java23EntitlementChecker instance = HandleLoader.load(Java23EntitlementChecker.class);
}
// no construction
private Java23EntitlementCheckerHandle() {}
}

View file

@ -6,10 +6,13 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public * your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1". * License v3.0 only", or the "Server Side Public License, v 1".
*/ */
import org.elasticsearch.gradle.internal.precommit.CheckForbiddenApisTask
apply plugin: 'elasticsearch.build' apply plugin: 'elasticsearch.build'
apply plugin: 'elasticsearch.publish' apply plugin: 'elasticsearch.publish'
apply plugin: 'elasticsearch.embedded-providers' apply plugin: 'elasticsearch.embedded-providers'
apply plugin: 'elasticsearch.mrjar'
embeddedProviders { embeddedProviders {
impl 'entitlement', project(':libs:entitlement:asm-provider') impl 'entitlement', project(':libs:entitlement:asm-provider')
@ -23,8 +26,13 @@ dependencies {
testImplementation(project(":test:framework")) { testImplementation(project(":test:framework")) {
exclude group: 'org.elasticsearch', module: 'entitlement' exclude group: 'org.elasticsearch', module: 'entitlement'
} }
// guarding for intellij
if (sourceSets.findByName("main23")) {
main23CompileOnly project(path: ':libs:entitlement:bridge', configuration: 'java23')
}
} }
tasks.named('forbiddenApisMain').configure { tasks.withType(CheckForbiddenApisTask).configureEach {
replaceSignatureFiles 'jdk-signatures' replaceSignatureFiles 'jdk-signatures'
} }

View file

@ -27,6 +27,8 @@ import java.io.IOException;
import java.lang.instrument.Instrumentation; import java.lang.instrument.Instrumentation;
import java.lang.module.ModuleFinder; import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference; import java.lang.module.ModuleReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
@ -59,7 +61,7 @@ public class EntitlementInitialization {
// Note: referenced by agent reflectively // Note: referenced by agent reflectively
public static void initialize(Instrumentation inst) throws Exception { public static void initialize(Instrumentation inst) throws Exception {
manager = new ElasticsearchEntitlementChecker(createPolicyManager()); manager = initChecker();
Map<MethodKey, CheckerMethod> methodMap = INSTRUMENTER_FACTORY.lookupMethodsToInstrument( Map<MethodKey, CheckerMethod> methodMap = INSTRUMENTER_FACTORY.lookupMethodsToInstrument(
"org.elasticsearch.entitlement.bridge.EntitlementChecker" "org.elasticsearch.entitlement.bridge.EntitlementChecker"
@ -137,6 +139,36 @@ public class EntitlementInitialization {
return Set.of(ALL_UNNAMED); return Set.of(ALL_UNNAMED);
} }
private static ElasticsearchEntitlementChecker initChecker() throws IOException {
final PolicyManager policyManager = createPolicyManager();
int javaVersion = Runtime.version().feature();
final String classNamePrefix;
if (javaVersion >= 23) {
classNamePrefix = "Java23";
} else {
classNamePrefix = "";
}
final String className = "org.elasticsearch.entitlement.runtime.api." + classNamePrefix + "ElasticsearchEntitlementChecker";
Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
throw new AssertionError("entitlement lib cannot find entitlement impl", e);
}
Constructor<?> constructor;
try {
constructor = clazz.getConstructor(PolicyManager.class);
} catch (NoSuchMethodException e) {
throw new AssertionError("entitlement impl is missing no arg constructor", e);
}
try {
return (ElasticsearchEntitlementChecker) constructor.newInstance(policyManager);
} catch (IllegalAccessException | InvocationTargetException | InstantiationException e) {
throw new AssertionError(e);
}
}
private static String internalName(Class<?> c) { private static String internalName(Class<?> c) {
return c.getName().replace('.', '/'); return c.getName().replace('.', '/');
} }

View file

@ -0,0 +1,26 @@
/*
* 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".
*/
package org.elasticsearch.entitlement.runtime.api;
import org.elasticsearch.entitlement.bridge.Java23EntitlementChecker;
import org.elasticsearch.entitlement.runtime.policy.PolicyManager;
public class Java23ElasticsearchEntitlementChecker extends ElasticsearchEntitlementChecker implements Java23EntitlementChecker {
public Java23ElasticsearchEntitlementChecker(PolicyManager policyManager) {
super(policyManager);
}
@Override
public void check$java_lang_System$exit(Class<?> callerClass, int status) {
// TODO: this is just an example, we shouldn't really override a method implemented in the superclass
super.check$java_lang_System$exit(callerClass, status);
}
}