Skip to content

Commit

Permalink
Add java version variants of entitlements checker (elastic#116878)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rjernst committed Nov 22, 2024
1 parent 06b0595 commit 140477e
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
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.JavaCompile;
import org.gradle.api.tasks.javadoc.Javadoc;
import org.gradle.api.tasks.testing.Test;
import org.gradle.external.javadoc.CoreJavadocOptions;
import org.gradle.jvm.tasks.Jar;
import org.gradle.jvm.toolchain.JavaLanguageVersion;
import org.gradle.jvm.toolchain.JavaToolchainService;
Expand All @@ -41,7 +44,6 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;

import javax.inject.Inject;

import static de.thetaphi.forbiddenapis.gradle.ForbiddenApisPlugin.FORBIDDEN_APIS_TASK_NAME;
Expand Down Expand Up @@ -79,6 +81,7 @@ public void apply(Project project) {
String mainSourceSetName = SourceSet.MAIN_SOURCE_SET_NAME + javaVersion;
SourceSet mainSourceSet = addSourceSet(project, javaExtension, mainSourceSetName, mainSourceSets, javaVersion);
configureSourceSetInJar(project, mainSourceSet, javaVersion);
addJar(project, mainSourceSet, javaVersion);
mainSourceSets.add(mainSourceSetName);
testSourceSets.add(mainSourceSetName);

Expand Down Expand Up @@ -142,6 +145,29 @@ private SourceSet addSourceSet(
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) {
project.getTasks().withType(JavaCompile.class).named(sourceSet.getCompileJavaTaskName()).configure(compileTask -> {
CompileOptions compileOptions = compileTask.getOptions();
compileOptions.getCompilerArgs().add("--enable-preview");
compileOptions.getCompilerArgs().add("-Xlint:-preview");

compileTask.doLast(t -> { stripPreviewFromFiles(compileTask.getDestinationDirectory().getAsFile().get().toPath()); });
});
project.getTasks().withType(Javadoc.class).named(name -> name.equals(sourceSet.getJavadocTaskName())).configureEach(javadocTask -> {
CoreJavadocOptions options = (CoreJavadocOptions) javadocTask.getOptions();
options.addBooleanOption("-enable-preview", true);
options.addStringOption("-release", String.valueOf(javaVersion));
});
}

private void configureSourceSetInJar(Project project, SourceSet sourceSet, int javaVersion) {
var jarTask = project.getTasks().withType(Jar.class).named(JavaPlugin.JAR_TASK_NAME);
jarTask.configure(task -> task.into("META-INF/versions/" + javaVersion, copySpec -> copySpec.from(sourceSet.getOutput())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,22 @@
import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL;

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.
*/
Expand Down Expand Up @@ -269,13 +285,7 @@ private void invokeInstrumentationMethod() {
}

protected void pushEntitlementChecker(MethodVisitor mv) {
mv.visitMethodInsn(
INVOKESTATIC,
"org/elasticsearch/entitlement/bridge/EntitlementCheckerHandle",
"instance",
"()Lorg/elasticsearch/entitlement/bridge/EntitlementChecker;",
false
);
mv.visitMethodInsn(INVOKESTATIC, handleClass, "instance", "()" + checkerClassDescriptor, false);
}

public record ClassFileInfo(String fileName, byte[] bytecodes) {}
Expand Down
17 changes: 8 additions & 9 deletions libs/entitlement/bridge/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,18 @@
* 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.mrjar'

configurations {
bridgeJar {
canBeConsumed = true
canBeResolved = false
tasks.named('jar').configure {
// guarding for intellij
if (sourceSets.findByName("main23")) {
from sourceSets.main23.output
}
}

artifacts {
bridgeJar(jar)
}

tasks.named('forbiddenApisMain').configure {
tasks.withType(CheckForbiddenApisTask).configureEach {
replaceSignatureFiles 'jdk-signatures'
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@

package org.elasticsearch.entitlement.bridge;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
* Makes the {@link EntitlementChecker} available to injected bytecode.
*/
Expand All @@ -35,27 +32,7 @@ private static class Holder {
* 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.
*/
private static final EntitlementChecker instance;
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);
}
}
private static final EntitlementChecker instance = HandleLoader.load(EntitlementChecker.class);
}

// no construction
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {}
}
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
@@ -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() {}
}
12 changes: 10 additions & 2 deletions libs/entitlement/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* 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.publish'

apply plugin: 'elasticsearch.embedded-providers'
apply plugin: 'elasticsearch.mrjar'

embeddedProviders {
impl 'entitlement', project(':libs:entitlement:asm-provider')
Expand All @@ -23,8 +26,13 @@ dependencies {
testImplementation(project(":test:framework")) {
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'
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import java.lang.instrument.Instrumentation;
import java.lang.module.ModuleFinder;
import java.lang.module.ModuleReference;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
Expand Down Expand Up @@ -59,7 +61,7 @@ public static EntitlementChecker checker() {

// Note: referenced by agent reflectively
public static void initialize(Instrumentation inst) throws Exception {
manager = new ElasticsearchEntitlementChecker(createPolicyManager());
manager = initChecker();

Map<MethodKey, CheckerMethod> methodMap = INSTRUMENTER_FACTORY.lookupMethodsToInstrument(
"org.elasticsearch.entitlement.bridge.EntitlementChecker"
Expand Down Expand Up @@ -137,6 +139,36 @@ private static Set<String> getModuleNames(Path pluginRoot, boolean isModular) {
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) {
return c.getName().replace('.', '/');
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}

0 comments on commit 140477e

Please sign in to comment.