From 9dbaf29636b546671104e876689af1969e47d585 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Thu, 14 Aug 2025 11:53:55 +0200 Subject: [PATCH 1/7] Add JDK Api extractor tool to help with entitlement reviews --- .../entitlement/tools/Utils.java | 16 +- .../tools/jdk-api-extractor/README.md | 14 ++ .../tools/jdk-api-extractor/build.gradle | 68 ++++++ .../licenses/asm-LICENSE.txt | 26 +++ .../jdk-api-extractor/licenses/asm-NOTICE.txt | 1 + .../tools/jdkapi/JdkApiExtractor.java | 219 ++++++++++++++++++ .../tools/public-callers-finder/build.gradle | 12 +- .../securitymanager-scanner/build.gradle | 12 +- 8 files changed, 362 insertions(+), 6 deletions(-) create mode 100644 libs/entitlement/tools/jdk-api-extractor/README.md create mode 100644 libs/entitlement/tools/jdk-api-extractor/build.gradle create mode 100644 libs/entitlement/tools/jdk-api-extractor/licenses/asm-LICENSE.txt create mode 100644 libs/entitlement/tools/jdk-api-extractor/licenses/asm-NOTICE.txt create mode 100644 libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java diff --git a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/Utils.java b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/Utils.java index c6a71f55db4c6..96173e8826182 100644 --- a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/Utils.java +++ b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/Utils.java @@ -24,12 +24,22 @@ public class Utils { + // TODO Currently ServerProcessBuilder is using --add-modules=ALL-MODULE-PATH, should this rather + // reflect below excludes (except for java.desktop which requires a special handling)? + // internal and incubator modules are also excluded private static final Set EXCLUDED_MODULES = Set.of( "java.desktop", "jdk.jartool", "jdk.jdi", "java.security.jgss", - "jdk.jshell" + "jdk.jshell", + "jdk.jcmd", + "jdk.hotspot.agent", + "jdk.jfr", + "jdk.javadoc", + // "jdk.jpackage", // Do we want to include this? + // "jdk.jlink", // Do we want to include this? + "jdk.localedata" // noise, change here are not interesting ); private static Map> findModuleExports(FileSystem fs) throws IOException { @@ -70,7 +80,9 @@ public static void walkJdkModules(JdkModuleConsumer c) throws IOException { for (var kv : modules.entrySet()) { var moduleName = kv.getKey(); - if (Utils.EXCLUDED_MODULES.contains(moduleName) == false) { + if (Utils.EXCLUDED_MODULES.contains(moduleName) == false + && moduleName.contains(".internal.") == false + && moduleName.contains(".incubator.") == false) { var thisModuleExports = moduleExports.get(moduleName); c.accept(moduleName, kv.getValue(), thisModuleExports); } diff --git a/libs/entitlement/tools/jdk-api-extractor/README.md b/libs/entitlement/tools/jdk-api-extractor/README.md new file mode 100644 index 0000000000000..ba371db30da40 --- /dev/null +++ b/libs/entitlement/tools/jdk-api-extractor/README.md @@ -0,0 +1,14 @@ +This tool scans the JDK on which it is running to extract its public accessible API. +That is: +- public methods (including constructors) of public, exported classes as well as protected methods of these if not final. +- internal implementations (overwrites) of above. + +The output of this tool is meant to be diffed against the output for another JDK +version to identify changes that need to be reviewed for entitlements. + +Usage example: +```bash +./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=24 --args="api-jdk24.tsv" +./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=25 --args="api-jdk25.tsv" +diff libs/entitlement/tools/jdk-api-extractor/api-jdk24.tsv libs/entitlement/tools/jdk-api-extractor/api-jdk25.tsv +``` diff --git a/libs/entitlement/tools/jdk-api-extractor/build.gradle b/libs/entitlement/tools/jdk-api-extractor/build.gradle new file mode 100644 index 0000000000000..021e1205375a4 --- /dev/null +++ b/libs/entitlement/tools/jdk-api-extractor/build.gradle @@ -0,0 +1,68 @@ +plugins { + id 'application' +} + +apply plugin: 'elasticsearch.build' + +tasks.named("dependencyLicenses").configure { + mapping from: /asm-.*/, to: 'asm' +} + +group = 'org.elasticsearch.entitlement.tools' + +ext { + javaMainClass = "org.elasticsearch.entitlement.tools.jdkapi.JdkApiExtractor" +} + +application { + mainClass.set(javaMainClass) + applicationDefaultJvmArgs = [ + '--add-exports', 'java.base/sun.security.util=ALL-UNNAMED', + '--add-opens', 'java.base/java.lang=ALL-UNNAMED', + '--add-opens', 'java.base/java.net=ALL-UNNAMED', + '--add-opens', 'java.base/java.net.spi=ALL-UNNAMED', + '--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED', + '--add-opens', 'java.base/javax.crypto=ALL-UNNAMED', + '--add-opens', 'java.base/javax.security.auth=ALL-UNNAMED', + '--add-opens', 'java.base/jdk.internal.logger=ALL-UNNAMED', + '--add-opens', 'java.base/sun.nio.ch=ALL-UNNAMED', + '--add-opens', 'jdk.management.jfr/jdk.management.jfr=ALL-UNNAMED', + '--add-opens', 'java.logging/java.util.logging=ALL-UNNAMED', + '--add-opens', 'java.logging/sun.util.logging.internal=ALL-UNNAMED', + '--add-opens', 'java.naming/javax.naming.ldap.spi=ALL-UNNAMED', + '--add-opens', 'java.rmi/sun.rmi.runtime=ALL-UNNAMED', + '--add-opens', 'jdk.dynalink/jdk.dynalink=ALL-UNNAMED', + '--add-opens', 'jdk.dynalink/jdk.dynalink.linker=ALL-UNNAMED', + '--add-opens', 'java.desktop/sun.awt=ALL-UNNAMED', + '--add-opens', 'java.sql.rowset/javax.sql.rowset.spi=ALL-UNNAMED', + '--add-opens', 'java.sql/java.sql=ALL-UNNAMED', + '--add-opens', 'java.xml.crypto/com.sun.org.apache.xml.internal.security.utils=ALL-UNNAMED' + ] +} + +// TODO Better integrate this into the existing tooling, +// usage of Druntime.java=24 doesn't work here at the moment out of the box +tasks.withType(JavaExec) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java")) + } +} + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(project(':libs:core')) + implementation 'org.ow2.asm:asm:9.8' + implementation 'org.ow2.asm:asm-util:9.8' + implementation(project(':libs:entitlement:tools:common')) +} + +tasks.named('forbiddenApisMain').configure { + replaceSignatureFiles 'jdk-signatures' +} + +tasks.named("thirdPartyAudit").configure { + ignoreMissingClasses() +} diff --git a/libs/entitlement/tools/jdk-api-extractor/licenses/asm-LICENSE.txt b/libs/entitlement/tools/jdk-api-extractor/licenses/asm-LICENSE.txt new file mode 100644 index 0000000000000..afb064f2f2666 --- /dev/null +++ b/libs/entitlement/tools/jdk-api-extractor/licenses/asm-LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 2012 France Télécom +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/entitlement/tools/jdk-api-extractor/licenses/asm-NOTICE.txt b/libs/entitlement/tools/jdk-api-extractor/licenses/asm-NOTICE.txt new file mode 100644 index 0000000000000..8d1c8b69c3fce --- /dev/null +++ b/libs/entitlement/tools/jdk-api-extractor/licenses/asm-NOTICE.txt @@ -0,0 +1 @@ + diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java new file mode 100644 index 0000000000000..63115ef1c6d9f --- /dev/null +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -0,0 +1,219 @@ +/* + * 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.tools.jdkapi; + +import org.elasticsearch.entitlement.tools.Utils; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; + +import java.io.IOException; +import java.lang.constant.ClassDesc; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Stream; + +import static org.objectweb.asm.Opcodes.ACC_FINAL; +import static org.objectweb.asm.Opcodes.ACC_PROTECTED; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; +import static org.objectweb.asm.Opcodes.ASM9; + +public class JdkApiExtractor { + + // exclude both final and non-final variants of these + private static final Set EXCLUDES = Set.of( + new AccessibleMethod("toString", "()Ljava/lang/String;", true, false, false), + new AccessibleMethod("hashCode", "()I", true, false, false), + new AccessibleMethod("equals", "(Ljava/lang/Object;)Z", true, false, false), + new AccessibleMethod("close", "()V", true, false, false), + new AccessibleMethod("toString", "()Ljava/lang/String;", true, true, false), + new AccessibleMethod("hashCode", "()I", true, false, true), + new AccessibleMethod("equals", "(Ljava/lang/Object;)Z", true, true, false), + new AccessibleMethod("close", "()V", true, false, true) + ); + + public static void main(String[] args) throws IOException { + if (args.length != 1) { + System.err.println("Exactly one argument is required: the output file path"); + System.exit(1); + } + + Map> accessibleImplementationsByClass = new TreeMap<>(); + Map> accessibleForOverwritesByClass = new TreeMap<>(); + + Utils.walkJdkModules((moduleName, moduleClasses, moduleExports) -> { + var visitor = new AccessibleClassVisitor(moduleExports, accessibleImplementationsByClass, accessibleForOverwritesByClass); + for (var classFile : moduleClasses) { + try { + ClassReader cr = new ClassReader(Files.newInputStream(classFile)); + cr.accept(visitor, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }); + + Path path = Path.of(args[0]); + System.out.println("Writing accessible methods of " + Runtime.version() + " to " + path.toAbsolutePath()); + + Files.write( + path, + () -> accessibleImplementationsByClass.entrySet().stream().flatMap(AccessibleMethod::toLines).iterator(), + StandardCharsets.UTF_8 + ); + } + + record AccessibleMethod(String method, String descriptor, boolean isPublic, boolean isFinal, boolean isStatic) { + + private static final String SEPARATOR = "\t"; + + private static final Comparator COMPARATOR = Comparator.comparing(AccessibleMethod::method) + .thenComparing(AccessibleMethod::descriptor) + .thenComparing(AccessibleMethod::isStatic); + + CharSequence toLine(String clazz) { + return String.join( + SEPARATOR, + clazz, + method, + descriptor, + isPublic ? "PUBLIC" : "PROTECTED", + isStatic ? "STATIC" : "", + isFinal ? "FINAL" : "" + ); + } + + static Stream toLines(Map.Entry> entry) { + return entry.getValue().stream().map(m -> m.toLine(entry.getKey())); + } + } + + static class AccessibleClassVisitor extends ClassVisitor { + private final Set moduleExports; + private final Map> accessibleImplementationsByClass; + private final Map> accessibleForOverwritesByClass; + + private Set accessibleImplementations; + private Set accessibleForOverwrites; + + private String className; + private boolean isPublicClass; + private boolean isFinalClass; + private boolean isExported; + + AccessibleClassVisitor( + Set moduleExports, + Map> accessibleImplementationsByClass, + Map> accessibleForOverwritesByClass + ) { + super(ASM9); + this.moduleExports = moduleExports; + this.accessibleImplementationsByClass = accessibleImplementationsByClass; + this.accessibleForOverwritesByClass = accessibleForOverwritesByClass; + } + + private Set getMethods(Map> methods, String clazz) { + return methods.computeIfAbsent(clazz, k -> new TreeSet<>(AccessibleMethod.COMPARATOR)); + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (superName != null) { + visitSuperClass(superName); + getMethods(accessibleForOverwritesByClass, name).addAll(getMethods(accessibleForOverwritesByClass, superName)); + } + if (interfaces != null && interfaces.length > 0) { + for (var interfaceName : interfaces) { + visitInterface(interfaceName); + getMethods(accessibleForOverwritesByClass, name).addAll(getMethods(accessibleForOverwritesByClass, interfaceName)); + } + } + super.visit(version, access, name, signature, superName, interfaces); + this.isExported = moduleExports.contains(getPackageName(name)); + this.className = name; + this.isPublicClass = (access & ACC_PUBLIC) != 0; + this.isFinalClass = (access & ACC_FINAL) != 0; + this.accessibleImplementations = getMethods(accessibleImplementationsByClass, name); + this.accessibleForOverwrites = getMethods(accessibleForOverwritesByClass, name); + } + + @Override + public void visitEnd() { + super.visitEnd(); + if (accessibleImplementations.isEmpty()) { + accessibleImplementationsByClass.remove(className); + } + if (accessibleForOverwrites.isEmpty()) { + accessibleForOverwritesByClass.remove(className); + } + } + + private void visitSuperClass(String superName) { + try { + ClassReader cr = new ClassReader(superName); + cr.accept(this, 0); + } catch (IOException e) { + System.out.println("Failed to visit super class [" + superName + "]:" + e.getMessage()); + } + } + + private void visitInterface(String interfaceName) { + try { + ClassReader cr = new ClassReader(interfaceName); + cr.accept(this, 0); + } catch (IOException e) { + System.out.println("Failed to visit interface [" + interfaceName + "]:" + e.getMessage()); + + } + } + + public String getClassName() { + return className; + } + + private static String getPackageName(String className) { + return ClassDesc.ofInternalName(className).packageName(); + } + + @Override + public final MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + boolean isPublic = (access & ACC_PUBLIC) != 0; + boolean isProtected = (access & ACC_PROTECTED) != 0; + boolean isFinal = (access & ACC_FINAL) != 0; + boolean isStatic = (access & ACC_STATIC) != 0; + if ((isPublic || isProtected) == false) { + return mv; + } + + var method = new AccessibleMethod(name, descriptor, isPublic, isFinal, isStatic); + if (isPublicClass && isExported && EXCLUDES.contains(method) == false) { + // class is public and exported, for final classes skip non-public methods + if (isPublic || isFinalClass == false) { + accessibleImplementations.add(method); + } + // if not static, the method is accessible for overwrites + if (isStatic == false) { + accessibleForOverwrites.add(method); + } + } else if (accessibleForOverwrites.contains(method)) { + accessibleImplementations.add(method); + } + return mv; + } + } +} diff --git a/libs/entitlement/tools/public-callers-finder/build.gradle b/libs/entitlement/tools/public-callers-finder/build.gradle index 664e6e763c4dd..6cece20be5cb7 100644 --- a/libs/entitlement/tools/public-callers-finder/build.gradle +++ b/libs/entitlement/tools/public-callers-finder/build.gradle @@ -40,14 +40,22 @@ application { ] } +// TODO Better integrate this into the existing tooling, +// usage of Druntime.java=24 doesn't work here at the moment out of the box +tasks.withType(JavaExec) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java")) + } +} + repositories { mavenCentral() } dependencies { compileOnly(project(':libs:core')) - implementation 'org.ow2.asm:asm:9.7.1' - implementation 'org.ow2.asm:asm-util:9.7.1' + implementation 'org.ow2.asm:asm:9.8' + implementation 'org.ow2.asm:asm-util:9.8' implementation(project(':libs:entitlement:tools:common')) } diff --git a/libs/entitlement/tools/securitymanager-scanner/build.gradle b/libs/entitlement/tools/securitymanager-scanner/build.gradle index 93a4f74360a6e..8f4fa32d8345c 100644 --- a/libs/entitlement/tools/securitymanager-scanner/build.gradle +++ b/libs/entitlement/tools/securitymanager-scanner/build.gradle @@ -40,14 +40,22 @@ application { ] } +// TODO Better integrate this into the existing tooling, +// usage of Druntime.java=24 doesn't work here at the moment out of the box +tasks.withType(JavaExec) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java")) + } +} + repositories { mavenCentral() } dependencies { compileOnly(project(':libs:core')) - implementation 'org.ow2.asm:asm:9.7.1' - implementation 'org.ow2.asm:asm-util:9.7.1' + implementation 'org.ow2.asm:asm:9.8' + implementation 'org.ow2.asm:asm-util:9.8' implementation(project(':libs:entitlement:tools:common')) } From 6b6d284b7813b782a413cd2e601a3f566d35c156 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Thu, 14 Aug 2025 12:14:40 +0200 Subject: [PATCH 2/7] add defaults --- libs/entitlement/tools/jdk-api-extractor/build.gradle | 2 +- libs/entitlement/tools/public-callers-finder/build.gradle | 2 +- libs/entitlement/tools/securitymanager-scanner/build.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/entitlement/tools/jdk-api-extractor/build.gradle b/libs/entitlement/tools/jdk-api-extractor/build.gradle index 021e1205375a4..bcbf31f86f720 100644 --- a/libs/entitlement/tools/jdk-api-extractor/build.gradle +++ b/libs/entitlement/tools/jdk-api-extractor/build.gradle @@ -44,7 +44,7 @@ application { // usage of Druntime.java=24 doesn't work here at the moment out of the box tasks.withType(JavaExec) { javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java")) + languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java", "24")) } } diff --git a/libs/entitlement/tools/public-callers-finder/build.gradle b/libs/entitlement/tools/public-callers-finder/build.gradle index 6cece20be5cb7..33c53e577875b 100644 --- a/libs/entitlement/tools/public-callers-finder/build.gradle +++ b/libs/entitlement/tools/public-callers-finder/build.gradle @@ -44,7 +44,7 @@ application { // usage of Druntime.java=24 doesn't work here at the moment out of the box tasks.withType(JavaExec) { javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java")) + languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java", "24")) } } diff --git a/libs/entitlement/tools/securitymanager-scanner/build.gradle b/libs/entitlement/tools/securitymanager-scanner/build.gradle index 8f4fa32d8345c..ac7be93bc4981 100644 --- a/libs/entitlement/tools/securitymanager-scanner/build.gradle +++ b/libs/entitlement/tools/securitymanager-scanner/build.gradle @@ -44,7 +44,7 @@ application { // usage of Druntime.java=24 doesn't work here at the moment out of the box tasks.withType(JavaExec) { javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java")) + languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java", "21")) } } From 8cb1760a20cd8f5787a8759bb4ea7cfbded4e9f4 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Thu, 14 Aug 2025 13:23:13 +0200 Subject: [PATCH 3/7] Add support to review deprecations --- .../tools/jdk-api-extractor/README.md | 6 ++ .../tools/jdkapi/JdkApiExtractor.java | 76 +++++++++++++------ 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/libs/entitlement/tools/jdk-api-extractor/README.md b/libs/entitlement/tools/jdk-api-extractor/README.md index ba371db30da40..e67b8511f1d3c 100644 --- a/libs/entitlement/tools/jdk-api-extractor/README.md +++ b/libs/entitlement/tools/jdk-api-extractor/README.md @@ -12,3 +12,9 @@ Usage example: ./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=25 --args="api-jdk25.tsv" diff libs/entitlement/tools/jdk-api-extractor/api-jdk24.tsv libs/entitlement/tools/jdk-api-extractor/api-jdk25.tsv ``` + +To review the diff of deprecations, use `--deprecations-only` as 2nd argument. + +```bash +./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=24 --args="deprecations-jdk24.tsv --deprecations-only" +``` diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java index 63115ef1c6d9f..977afeb3343cc 100644 --- a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -26,6 +26,7 @@ import java.util.TreeSet; import java.util.stream.Stream; +import static org.objectweb.asm.Opcodes.ACC_DEPRECATED; import static org.objectweb.asm.Opcodes.ACC_FINAL; import static org.objectweb.asm.Opcodes.ACC_PROTECTED; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; @@ -47,16 +48,26 @@ public class JdkApiExtractor { ); public static void main(String[] args) throws IOException { - if (args.length != 1) { - System.err.println("Exactly one argument is required: the output file path"); + boolean valid = args.length == 1 || (args.length == 2 && "--deprecations-only".equals(args[1])); + + if (valid == false) { + System.err.println("usage: [--deprecations-only]"); System.exit(1); } + boolean deprecationsOnly = args.length == 2 && args[1].equals("--deprecations-only"); + Map> accessibleImplementationsByClass = new TreeMap<>(); - Map> accessibleForOverwritesByClass = new TreeMap<>(); + Map> accessibleForOverridesByClass = new TreeMap<>(); + Map> deprecationsByClass = new TreeMap<>(); Utils.walkJdkModules((moduleName, moduleClasses, moduleExports) -> { - var visitor = new AccessibleClassVisitor(moduleExports, accessibleImplementationsByClass, accessibleForOverwritesByClass); + var visitor = new AccessibleClassVisitor( + moduleExports, + accessibleImplementationsByClass, + accessibleForOverridesByClass, + deprecationsByClass + ); for (var classFile : moduleClasses) { try { ClassReader cr = new ClassReader(Files.newInputStream(classFile)); @@ -67,14 +78,12 @@ public static void main(String[] args) throws IOException { } }); - Path path = Path.of(args[0]); - System.out.println("Writing accessible methods of " + Runtime.version() + " to " + path.toAbsolutePath()); + writeFile(Path.of(args[0]), deprecationsOnly ? deprecationsByClass : accessibleImplementationsByClass); + } - Files.write( - path, - () -> accessibleImplementationsByClass.entrySet().stream().flatMap(AccessibleMethod::toLines).iterator(), - StandardCharsets.UTF_8 - ); + private static void writeFile(Path path, Map> methods) throws IOException { + System.out.println("Writing result for " + Runtime.version() + " to " + path.toAbsolutePath()); + Files.write(path, () -> methods.entrySet().stream().flatMap(AccessibleMethod::toLines).iterator(), StandardCharsets.UTF_8); } record AccessibleMethod(String method, String descriptor, boolean isPublic, boolean isFinal, boolean isStatic) { @@ -105,25 +114,30 @@ static Stream toLines(Map.Entry> ent static class AccessibleClassVisitor extends ClassVisitor { private final Set moduleExports; private final Map> accessibleImplementationsByClass; - private final Map> accessibleForOverwritesByClass; + private final Map> accessibleForOverridesByClass; + private final Map> deprecationsByClass; private Set accessibleImplementations; - private Set accessibleForOverwrites; + private Set accessibleForOverrides; + private Set deprecations; private String className; private boolean isPublicClass; private boolean isFinalClass; + private boolean isDeprecatedClass; private boolean isExported; AccessibleClassVisitor( Set moduleExports, Map> accessibleImplementationsByClass, - Map> accessibleForOverwritesByClass + Map> accessibleForOverridesByClass, + Map> deprecationsByClass ) { super(ASM9); this.moduleExports = moduleExports; this.accessibleImplementationsByClass = accessibleImplementationsByClass; - this.accessibleForOverwritesByClass = accessibleForOverwritesByClass; + this.accessibleForOverridesByClass = accessibleForOverridesByClass; + this.deprecationsByClass = deprecationsByClass; } private Set getMethods(Map> methods, String clazz) { @@ -134,12 +148,12 @@ private Set getMethods(Map> meth public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (superName != null) { visitSuperClass(superName); - getMethods(accessibleForOverwritesByClass, name).addAll(getMethods(accessibleForOverwritesByClass, superName)); + getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, superName)); } if (interfaces != null && interfaces.length > 0) { for (var interfaceName : interfaces) { visitInterface(interfaceName); - getMethods(accessibleForOverwritesByClass, name).addAll(getMethods(accessibleForOverwritesByClass, interfaceName)); + getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, interfaceName)); } } super.visit(version, access, name, signature, superName, interfaces); @@ -147,8 +161,10 @@ public void visit(int version, int access, String name, String signature, String this.className = name; this.isPublicClass = (access & ACC_PUBLIC) != 0; this.isFinalClass = (access & ACC_FINAL) != 0; + this.isDeprecatedClass = (access & ACC_DEPRECATED) != 0; this.accessibleImplementations = getMethods(accessibleImplementationsByClass, name); - this.accessibleForOverwrites = getMethods(accessibleForOverwritesByClass, name); + this.accessibleForOverrides = getMethods(accessibleForOverridesByClass, name); + this.deprecations = getMethods(deprecationsByClass, name); } @Override @@ -157,8 +173,11 @@ public void visitEnd() { if (accessibleImplementations.isEmpty()) { accessibleImplementationsByClass.remove(className); } - if (accessibleForOverwrites.isEmpty()) { - accessibleForOverwritesByClass.remove(className); + if (accessibleForOverrides.isEmpty()) { + accessibleForOverridesByClass.remove(className); + } + if (deprecations.isEmpty()) { + deprecationsByClass.remove(className); } } @@ -196,6 +215,7 @@ public final MethodVisitor visitMethod(int access, String name, String descripto boolean isProtected = (access & ACC_PROTECTED) != 0; boolean isFinal = (access & ACC_FINAL) != 0; boolean isStatic = (access & ACC_STATIC) != 0; + boolean isDeprecated = (access & ACC_DEPRECATED) != 0; if ((isPublic || isProtected) == false) { return mv; } @@ -205,13 +225,19 @@ public final MethodVisitor visitMethod(int access, String name, String descripto // class is public and exported, for final classes skip non-public methods if (isPublic || isFinalClass == false) { accessibleImplementations.add(method); + // if not static, the method is accessible for overrides + if (isStatic == false) { + accessibleForOverrides.add(method); + } + if (isDeprecatedClass || isDeprecated) { + deprecations.add(method); + } } - // if not static, the method is accessible for overwrites - if (isStatic == false) { - accessibleForOverwrites.add(method); - } - } else if (accessibleForOverwrites.contains(method)) { + } else if (accessibleForOverrides.contains(method)) { accessibleImplementations.add(method); + if (isDeprecatedClass || isDeprecated) { + deprecations.add(method); + } } return mv; } From d147d9a86e5fb622f42d201562f4f82b21ffca85 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Thu, 14 Aug 2025 17:07:29 +0200 Subject: [PATCH 4/7] review feedback --- .../entitlement/tools/jdkapi/JdkApiExtractor.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java index 977afeb3343cc..3d07a04aab4e4 100644 --- a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.Comparator; import java.util.Map; import java.util.Set; @@ -147,12 +148,16 @@ private Set getMethods(Map> meth @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (superName != null) { - visitSuperClass(superName); + if(accessibleImplementationsByClass.containsKey(superName) == false) { + visitSuperClass(superName); + } getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, superName)); } if (interfaces != null && interfaces.length > 0) { for (var interfaceName : interfaces) { - visitInterface(interfaceName); + if(accessibleImplementationsByClass.containsKey(interfaceName) == false) { + visitInterface(interfaceName); + } getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, interfaceName)); } } @@ -171,13 +176,13 @@ public void visit(int version, int access, String name, String signature, String public void visitEnd() { super.visitEnd(); if (accessibleImplementations.isEmpty()) { - accessibleImplementationsByClass.remove(className); + accessibleImplementationsByClass.replace(className, Collections.emptySet()); } if (accessibleForOverrides.isEmpty()) { - accessibleForOverridesByClass.remove(className); + accessibleImplementationsByClass.replace(className, Collections.emptySet()); } if (deprecations.isEmpty()) { - deprecationsByClass.remove(className); + accessibleImplementationsByClass.replace(className, Collections.emptySet()); } } From b9093c20d2afbeba631ab4e34788859de907db22 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Thu, 14 Aug 2025 15:13:12 +0000 Subject: [PATCH 5/7] [CI] Auto commit changes from spotless --- .../entitlement/tools/jdkapi/JdkApiExtractor.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java index 3d07a04aab4e4..dcd2efaabd172 100644 --- a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -148,14 +148,14 @@ private Set getMethods(Map> meth @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (superName != null) { - if(accessibleImplementationsByClass.containsKey(superName) == false) { + if (accessibleImplementationsByClass.containsKey(superName) == false) { visitSuperClass(superName); } getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, superName)); } if (interfaces != null && interfaces.length > 0) { for (var interfaceName : interfaces) { - if(accessibleImplementationsByClass.containsKey(interfaceName) == false) { + if (accessibleImplementationsByClass.containsKey(interfaceName) == false) { visitInterface(interfaceName); } getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, interfaceName)); From 03aa38cd57c864051cefc5ff83db56fce9282966 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Thu, 14 Aug 2025 17:26:23 +0200 Subject: [PATCH 6/7] forbidden apis --- .../tools/jdkapi/JdkApiExtractor.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java index dcd2efaabd172..d6ed65d347387 100644 --- a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -9,6 +9,7 @@ package org.elasticsearch.entitlement.tools.jdkapi; +import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.entitlement.tools.Utils; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -49,13 +50,7 @@ public class JdkApiExtractor { ); public static void main(String[] args) throws IOException { - boolean valid = args.length == 1 || (args.length == 2 && "--deprecations-only".equals(args[1])); - - if (valid == false) { - System.err.println("usage: [--deprecations-only]"); - System.exit(1); - } - + validateArgs(args); boolean deprecationsOnly = args.length == 2 && args[1].equals("--deprecations-only"); Map> accessibleImplementationsByClass = new TreeMap<>(); @@ -82,6 +77,17 @@ public static void main(String[] args) throws IOException { writeFile(Path.of(args[0]), deprecationsOnly ? deprecationsByClass : accessibleImplementationsByClass); } + @SuppressForbidden(reason = "cli tool printing to standard err/out") + private static void validateArgs(String[] args) { + boolean valid = args.length == 1 || (args.length == 2 && "--deprecations-only".equals(args[1])); + + if (valid == false) { + System.err.println("usage: [--deprecations-only]"); + System.exit(1); + } + } + + @SuppressForbidden(reason = "cli tool printing to standard err/out") private static void writeFile(Path path, Map> methods) throws IOException { System.out.println("Writing result for " + Runtime.version() + " to " + path.toAbsolutePath()); Files.write(path, () -> methods.entrySet().stream().flatMap(AccessibleMethod::toLines).iterator(), StandardCharsets.UTF_8); @@ -186,6 +192,7 @@ public void visitEnd() { } } + @SuppressForbidden(reason = "cli tool printing to standard err/out") private void visitSuperClass(String superName) { try { ClassReader cr = new ClassReader(superName); @@ -195,6 +202,7 @@ private void visitSuperClass(String superName) { } } + @SuppressForbidden(reason = "cli tool printing to standard err/out") private void visitInterface(String interfaceName) { try { ClassReader cr = new ClassReader(interfaceName); From 226e54b4ba6d9aa83feb792133ab717b176a3595 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Thu, 28 Aug 2025 18:41:21 +0200 Subject: [PATCH 7/7] Review feedback --- .../tools/jdk-api-extractor/README.md | 2 +- .../tools/jdk-api-extractor/build.gradle | 10 ++-- .../tools/jdkapi/JdkApiExtractor.java | 47 +++++++++++-------- .../tools/public-callers-finder/build.gradle | 10 ++-- .../securitymanager-scanner/build.gradle | 10 ++-- 5 files changed, 40 insertions(+), 39 deletions(-) diff --git a/libs/entitlement/tools/jdk-api-extractor/README.md b/libs/entitlement/tools/jdk-api-extractor/README.md index e67b8511f1d3c..48e62d4a8981e 100644 --- a/libs/entitlement/tools/jdk-api-extractor/README.md +++ b/libs/entitlement/tools/jdk-api-extractor/README.md @@ -13,7 +13,7 @@ Usage example: diff libs/entitlement/tools/jdk-api-extractor/api-jdk24.tsv libs/entitlement/tools/jdk-api-extractor/api-jdk25.tsv ``` -To review the diff of deprecations, use `--deprecations-only` as 2nd argument. +To review the diff of deprecations (by means of `@Deprecated`), use `--deprecations-only` as 2nd argument. ```bash ./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=24 --args="deprecations-jdk24.tsv --deprecations-only" diff --git a/libs/entitlement/tools/jdk-api-extractor/build.gradle b/libs/entitlement/tools/jdk-api-extractor/build.gradle index bcbf31f86f720..21e26793a067d 100644 --- a/libs/entitlement/tools/jdk-api-extractor/build.gradle +++ b/libs/entitlement/tools/jdk-api-extractor/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.OS + plugins { id 'application' } @@ -40,12 +42,8 @@ application { ] } -// TODO Better integrate this into the existing tooling, -// usage of Druntime.java=24 doesn't work here at the moment out of the box -tasks.withType(JavaExec) { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java", "24")) - } +tasks.named("run").configure { + executable = "${buildParams.runtimeJavaHome.get()}/bin/java" + (OS.current() == OS.WINDOWS ? '.exe' : '') } repositories { diff --git a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java index d6ed65d347387..0e84fc9e15192 100644 --- a/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java +++ b/libs/entitlement/tools/jdk-api-extractor/src/main/java/org/elasticsearch/entitlement/tools/jdkapi/JdkApiExtractor.java @@ -65,6 +65,10 @@ public static void main(String[] args) throws IOException { deprecationsByClass ); for (var classFile : moduleClasses) { + // skip if class was already visited earlier due to a dependency on it + if (accessibleImplementationsByClass.containsKey(internalClassName(classFile, moduleName))) { + continue; + } try { ClassReader cr = new ClassReader(Files.newInputStream(classFile)); cr.accept(visitor, 0); @@ -77,6 +81,12 @@ public static void main(String[] args) throws IOException { writeFile(Path.of(args[0]), deprecationsOnly ? deprecationsByClass : accessibleImplementationsByClass); } + private static String internalClassName(Path clazz, String moduleName) { + Path modulePath = clazz.getFileSystem().getPath("modules", moduleName); + String relativePath = modulePath.relativize(clazz).toString(); + return relativePath.substring(0, relativePath.length() - ".class".length()); + } + @SuppressForbidden(reason = "cli tool printing to standard err/out") private static void validateArgs(String[] args) { boolean valid = args.length == 1 || (args.length == 2 && "--deprecations-only".equals(args[1])); @@ -147,51 +157,53 @@ static class AccessibleClassVisitor extends ClassVisitor { this.deprecationsByClass = deprecationsByClass; } - private Set getMethods(Map> methods, String clazz) { - return methods.computeIfAbsent(clazz, k -> new TreeSet<>(AccessibleMethod.COMPARATOR)); + private static Set newSortedSet() { + return new TreeSet<>(AccessibleMethod.COMPARATOR); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + final Set currentAccessibleForOverrides = newSortedSet(); if (superName != null) { if (accessibleImplementationsByClass.containsKey(superName) == false) { visitSuperClass(superName); } - getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, superName)); + currentAccessibleForOverrides.addAll(accessibleForOverridesByClass.getOrDefault(superName, Collections.emptySet())); } if (interfaces != null && interfaces.length > 0) { for (var interfaceName : interfaces) { if (accessibleImplementationsByClass.containsKey(interfaceName) == false) { visitInterface(interfaceName); } - getMethods(accessibleForOverridesByClass, name).addAll(getMethods(accessibleForOverridesByClass, interfaceName)); + currentAccessibleForOverrides.addAll(accessibleForOverridesByClass.getOrDefault(interfaceName, Collections.emptySet())); } } + // only initialize local state AFTER visiting all dependencies above! super.visit(version, access, name, signature, superName, interfaces); this.isExported = moduleExports.contains(getPackageName(name)); this.className = name; this.isPublicClass = (access & ACC_PUBLIC) != 0; this.isFinalClass = (access & ACC_FINAL) != 0; this.isDeprecatedClass = (access & ACC_DEPRECATED) != 0; - this.accessibleImplementations = getMethods(accessibleImplementationsByClass, name); - this.accessibleForOverrides = getMethods(accessibleForOverridesByClass, name); - this.deprecations = getMethods(deprecationsByClass, name); + this.accessibleForOverrides = currentAccessibleForOverrides; + this.accessibleImplementations = newSortedSet(); + this.deprecations = newSortedSet(); } @Override public void visitEnd() { super.visitEnd(); - if (accessibleImplementations.isEmpty()) { - accessibleImplementationsByClass.replace(className, Collections.emptySet()); - } - if (accessibleForOverrides.isEmpty()) { - accessibleImplementationsByClass.replace(className, Collections.emptySet()); - } - if (deprecations.isEmpty()) { - accessibleImplementationsByClass.replace(className, Collections.emptySet()); + if (accessibleImplementationsByClass.put(className, unmodifiableSet(accessibleImplementations)) != null + || accessibleForOverridesByClass.put(className, unmodifiableSet(accessibleForOverrides)) != null + || deprecationsByClass.put(className, unmodifiableSet(deprecations)) != null) { + throw new IllegalStateException("Class " + className + " was already visited!"); } } + private static Set unmodifiableSet(Set set) { + return set.isEmpty() ? Collections.emptySet() : Collections.unmodifiableSet(set); + } + @SuppressForbidden(reason = "cli tool printing to standard err/out") private void visitSuperClass(String superName) { try { @@ -209,14 +221,9 @@ private void visitInterface(String interfaceName) { cr.accept(this, 0); } catch (IOException e) { System.out.println("Failed to visit interface [" + interfaceName + "]:" + e.getMessage()); - } } - public String getClassName() { - return className; - } - private static String getPackageName(String className) { return ClassDesc.ofInternalName(className).packageName(); } diff --git a/libs/entitlement/tools/public-callers-finder/build.gradle b/libs/entitlement/tools/public-callers-finder/build.gradle index 33c53e577875b..6a350665eee54 100644 --- a/libs/entitlement/tools/public-callers-finder/build.gradle +++ b/libs/entitlement/tools/public-callers-finder/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.OS + plugins { id 'application' } @@ -40,12 +42,8 @@ application { ] } -// TODO Better integrate this into the existing tooling, -// usage of Druntime.java=24 doesn't work here at the moment out of the box -tasks.withType(JavaExec) { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java", "24")) - } +tasks.named("run").configure { + executable = "${buildParams.runtimeJavaHome.get()}/bin/java" + (OS.current() == OS.WINDOWS ? '.exe' : '') } repositories { diff --git a/libs/entitlement/tools/securitymanager-scanner/build.gradle b/libs/entitlement/tools/securitymanager-scanner/build.gradle index ac7be93bc4981..b28a064b91b98 100644 --- a/libs/entitlement/tools/securitymanager-scanner/build.gradle +++ b/libs/entitlement/tools/securitymanager-scanner/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.OS + plugins { id 'application' } @@ -40,12 +42,8 @@ application { ] } -// TODO Better integrate this into the existing tooling, -// usage of Druntime.java=24 doesn't work here at the moment out of the box -tasks.withType(JavaExec) { - javaLauncher = javaToolchains.launcherFor { - languageVersion = JavaLanguageVersion.of(System.getProperty("runtime.java", "21")) - } +tasks.named("run").configure { + executable = "${buildParams.runtimeJavaHome.get()}/bin/java" + (OS.current() == OS.WINDOWS ? '.exe' : '') } repositories {