From fccf83cb9549182e77f9ab839f7f8dba269702ed Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Tue, 21 Oct 2025 10:44:42 +0200 Subject: [PATCH 1/9] Made public jdk api extractor a common tool for easy use in public callers finder --- libs/entitlement/tools/common/build.gradle | 6 + .../tools/AccessibleJdkMethods.java | 220 ++++++++++++ .../entitlement/tools/Utils.java | 29 +- .../tools/jdk-api-extractor/build.gradle | 2 +- .../tools/jdkapi/JdkApiExtractor.java | 315 ++---------------- 5 files changed, 282 insertions(+), 290 deletions(-) create mode 100644 libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java diff --git a/libs/entitlement/tools/common/build.gradle b/libs/entitlement/tools/common/build.gradle index 89772b4132c5f..f1917ae547dfe 100644 --- a/libs/entitlement/tools/common/build.gradle +++ b/libs/entitlement/tools/common/build.gradle @@ -12,3 +12,9 @@ apply plugin: 'elasticsearch.build' tasks.named('forbiddenApisMain').configure { replaceSignatureFiles 'jdk-signatures' } + +dependencies { + implementation(project(':libs:core')) + implementation 'org.ow2.asm:asm:9.8' + implementation 'org.ow2.asm:asm-util:9.8' +} diff --git a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java new file mode 100644 index 0000000000000..c7e315420ab10 --- /dev/null +++ b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java @@ -0,0 +1,220 @@ +/* + * 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; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.io.IOException; +import java.lang.constant.ClassDesc; +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.function.Predicate; + +import static java.util.Collections.emptySet; + +public class AccessibleJdkMethods { + + private static final Set EXCLUDES = Set.of( + new AccessibleMethod.Descriptor("toString", "()Ljava/lang/String;", true, false), + new AccessibleMethod.Descriptor("hashCode", "()I", true, false), + new AccessibleMethod.Descriptor("equals", "(Ljava/lang/Object;)Z", true, false), + new AccessibleMethod.Descriptor("close", "()V", true, false) + ); + + public record AccessibleMethod(Descriptor descriptor, boolean isFinal, boolean isDeprecated) { + public record Descriptor(String method, String descriptor, boolean isPublic, boolean isStatic) { + public static final Comparator COMPARATOR = Comparator.comparing(Descriptor::method) + .thenComparing(Descriptor::descriptor) + .thenComparing(Descriptor::isStatic); + } + + public static final Comparator COMPARATOR = Comparator.comparing( + AccessibleMethod::descriptor, + Descriptor.COMPARATOR + ); + } + + public record ModuleClass(String module, String clazz) { + public static final Comparator COMPARATOR = Comparator.comparing(ModuleClass::module) + .thenComparing(ModuleClass::clazz); + } + + public static Map> loadAccessibleMethods(Predicate modulePredicate) throws IOException { + // 1st: map class names to module names (including later excluded modules) for lookup in 2nd step + final Map moduleNameByClass = Utils.loadClassToModuleMapping(); + final Map> exportsByModule = Utils.loadExportsByModule(); + final AccessibleMethodsVisitor visitor = new AccessibleMethodsVisitor(modulePredicate, moduleNameByClass, exportsByModule); + // 2nd: calculate accessible implementations of classes in included modules + Utils.walkJdkModules(modulePredicate, exportsByModule, (moduleName, moduleClasses, moduleExports) -> { + for (var classFile : moduleClasses) { + // visit class once (skips if class was already visited earlier due to a dependency on it) + visitor.visitOnce(new ModuleClass(moduleName, Utils.internalClassName(classFile, moduleName))); + } + }); + + return visitor.getAccessibleMethods(); + } + + private static class AccessibleMethodsVisitor extends ClassVisitor { + private final Map> inheritableAccessByClass = new TreeMap<>(ModuleClass.COMPARATOR); + private final Map> accessibleImplementationsByClass = new TreeMap<>(ModuleClass.COMPARATOR); + + private final Predicate modulePredicate; + private final Map moduleNameByClass; + private final Map> exportsByModule; + + private Set accessibleImplementations; + private Set inheritableAccess; + + private ModuleClass moduleClass; + private boolean isPublicClass; + private boolean isFinalClass; + private boolean isDeprecatedClass; + private boolean isExported; + + AccessibleMethodsVisitor( + Predicate modulePredicate, + Map moduleNameByClass, + Map> exportsByModule + ) { + super(Opcodes.ASM9); + this.modulePredicate = modulePredicate; + this.moduleNameByClass = moduleNameByClass; + this.exportsByModule = exportsByModule; + } + + private static Set newSortedSet() { + return new TreeSet<>(AccessibleMethod.COMPARATOR); + } + + Map> getAccessibleMethods() { + return Collections.unmodifiableMap(accessibleImplementationsByClass); + } + + void visitOnce(ModuleClass moduleClass) { + if (accessibleImplementationsByClass.containsKey(moduleClass)) { + return; + } + if (moduleClass.clazz.startsWith("com/sun/") && moduleClass.clazz.contains("/internal/")) { + // skip com.sun.*.internal classes as they are not part of the supported JDK API + // even if methods override some publicly visible API + return; + } + try { + ClassReader cr = new ClassReader(moduleClass.clazz); + cr.accept(this, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + final Set currentInheritedAccess = newSortedSet(); + if (superName != null) { + var superModuleClass = getModuleClass(superName); + visitOnce(superModuleClass); + currentInheritedAccess.addAll(inheritableAccessByClass.getOrDefault(superModuleClass, emptySet())); + } + if (interfaces != null && interfaces.length > 0) { + for (var interfaceName : interfaces) { + var interfaceModuleClass = getModuleClass(interfaceName); + visitOnce(interfaceModuleClass); + currentInheritedAccess.addAll(inheritableAccessByClass.getOrDefault(interfaceModuleClass, emptySet())); + } + } + // only initialize local state AFTER visiting all dependencies above! + super.visit(version, access, name, signature, superName, interfaces); + this.moduleClass = getModuleClass(name); + this.isExported = getModuleExports(moduleClass.module()).contains(getPackageName(name)); + this.isPublicClass = (access & Opcodes.ACC_PUBLIC) != 0; + this.isFinalClass = (access & Opcodes.ACC_FINAL) != 0; + this.isDeprecatedClass = (access & Opcodes.ACC_DEPRECATED) != 0; + this.inheritableAccess = currentInheritedAccess; + this.accessibleImplementations = newSortedSet(); + } + + private ModuleClass getModuleClass(String name) { + String module = moduleNameByClass.get(name); + if (module == null) { + throw new IllegalStateException("Unknown module for class: " + name); + } + return new ModuleClass(module, name); + } + + private Set getModuleExports(String module) { + Set exports = exportsByModule.get(module); + if (exports == null) { + throw new IllegalStateException("Unknown exports for module: " + module); + } + return exports; + } + + @Override + public void visitEnd() { + super.visitEnd(); + if (accessibleImplementationsByClass.put(moduleClass, unmodifiableSet(accessibleImplementations)) != null + || inheritableAccessByClass.put(moduleClass, unmodifiableSet(inheritableAccess)) != null) { + throw new IllegalStateException("Class " + moduleClass.clazz() + " was already visited!"); + } + } + + private static Set unmodifiableSet(Set set) { + return set.isEmpty() ? emptySet() : Collections.unmodifiableSet(set); + } + + 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 & Opcodes.ACC_PUBLIC) != 0; + boolean isProtected = (access & Opcodes.ACC_PROTECTED) != 0; + boolean isFinal = (access & Opcodes.ACC_FINAL) != 0; + boolean isStatic = (access & Opcodes.ACC_STATIC) != 0; + boolean isDeprecated = (access & Opcodes.ACC_DEPRECATED) != 0; + if ((isPublic || isProtected) == false) { + return mv; + } + + var methodDescriptor = new AccessibleMethod.Descriptor(name, descriptor, isPublic, isStatic); + var method = new AccessibleMethod(methodDescriptor, isFinal, isDeprecatedClass || isDeprecated); + if (isPublicClass && isExported && EXCLUDES.contains(methodDescriptor) == false) { + // class is public and exported, to be accessible outside the JDK the method must be either: + // - public or + // - protected if not a final class + if (isPublic || isFinalClass == false) { + if (modulePredicate.test(moduleClass.module)) { + accessibleImplementations.add(method); + } + // if public and not static, the method can be accessible on non-public and non-exported subclasses, + // but skip constructors + if (isPublic && isStatic == false && name.equals("") == false) { + inheritableAccess.add(method); + } + } + } else if (inheritableAccess.contains(method)) { + if (modulePredicate.test(moduleClass.module)) { + accessibleImplementations.add(method); + } + } + return mv; + } + } +} 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 0de0a8015b264..717a094bdb1c4 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 @@ -16,6 +16,7 @@ import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -48,7 +49,7 @@ public class Utils { && m.contains(".internal.") == false && m.contains(".incubator.") == false; - public static Map> findModuleExports() throws IOException { + public static Map> loadExportsByModule() throws IOException { var modulesExports = new HashMap>(); try (var stream = Files.walk(JRT_FS.getPath("modules"))) { stream.filter(p -> p.getFileName().toString().equals("module-info.class")).forEach(x -> { @@ -67,15 +68,29 @@ public static Map> findModuleExports() throws IOException { } }); } - return modulesExports; + return Collections.unmodifiableMap(modulesExports); } + public static Map loadClassToModuleMapping() throws IOException{ + Map moduleNameByClass = new HashMap<>(); + Utils.walkJdkModules(m -> true, Collections.emptyMap(), (moduleName, moduleClasses, moduleExports) -> { + for (var classFile : moduleClasses) { + String prev = moduleNameByClass.put(internalClassName(classFile, moduleName), moduleName); + if (prev != null) { + throw new IllegalStateException("Class " + classFile + " is in both modules " + prev + " and " + moduleName); + } + } + }); + return Collections.unmodifiableMap(moduleNameByClass); + } + + public interface JdkModuleConsumer { void accept(String moduleName, List moduleClasses, Set moduleExports); } public static void walkJdkModules(JdkModuleConsumer c) throws IOException { - walkJdkModules(DEFAULT_MODULE_PREDICATE, Utils.findModuleExports(), c); + walkJdkModules(DEFAULT_MODULE_PREDICATE, Utils.loadExportsByModule(), c); } public static void walkJdkModules(Predicate modulePredicate, Map> exportsByModule, JdkModuleConsumer c) @@ -88,10 +103,16 @@ public static void walkJdkModules(Predicate modulePredicate, Map EXCLUDES = Set.of( - new AccessibleMethod("toString", "()Ljava/lang/String;", true, false, false), - new AccessibleMethod("toString", "()Ljava/lang/String;", true, true, false), - new AccessibleMethod("hashCode", "()I", true, false, false), - new AccessibleMethod("hashCode", "()I", true, true, false), - new AccessibleMethod("equals", "(Ljava/lang/Object;)Z", true, false, false), - new AccessibleMethod("equals", "(Ljava/lang/Object;)Z", true, true, false), - new AccessibleMethod("close", "()V", true, false, false), - new AccessibleMethod("close", "()V", true, true, false) - ); + private static final String SEPARATOR = "\t"; private static String DEPRECATIONS_ONLY = "--deprecations-only"; private static String INCLUDE_INCUBATOR = "--include-incubator"; @@ -62,68 +37,14 @@ public class JdkApiExtractor { public static void main(String[] args) throws IOException { validateArgs(args); - boolean deprecationsOnly = optionalArgs(args).anyMatch(DEPRECATIONS_ONLY::equals); - - final Map moduleNameByClass = new HashMap<>(); - final Map> accessibleImplementationsByClass = new TreeMap<>(ModuleClass.COMPARATOR); - final Map> inheritableAccessByClass = new TreeMap<>(ModuleClass.COMPARATOR); - final Map> deprecationsByClass = new TreeMap<>(ModuleClass.COMPARATOR); - - final Map> exportsByModule = Utils.findModuleExports(); - // 1st: map class names to module names (including later excluded modules) for lookup in 2nd step - Utils.walkJdkModules(m -> true, exportsByModule, (moduleName, moduleClasses, moduleExports) -> { - for (var classFile : moduleClasses) { - String prev = moduleNameByClass.put(internalClassName(classFile, moduleName), moduleName); - if (prev != null) { - throw new IllegalStateException("Class " + classFile + " is in both modules " + prev + " and " + moduleName); - } - } - }); - - var visitor = new AccessibleClassVisitor( - moduleNameByClass, - exportsByModule, - accessibleImplementationsByClass, - inheritableAccessByClass, - deprecationsByClass - ); Predicate modulePredicate = Utils.DEFAULT_MODULE_PREDICATE.or( m -> optionalArgs(args).anyMatch(INCLUDE_INCUBATOR::equals) && m.contains(".incubator.") ); - // 2nd: calculate accessible implementations of classes in included modules - Utils.walkJdkModules(modulePredicate, exportsByModule, (moduleName, moduleClasses, moduleExports) -> { - for (var classFile : moduleClasses) { - // skip if class was already visited earlier due to a dependency on it - String className = internalClassName(classFile, moduleName); - if (accessibleImplementationsByClass.containsKey(new ModuleClass(moduleName, className))) { - continue; - } - try { - ClassReader cr = new ClassReader(Files.newInputStream(classFile)); - cr.accept(visitor, 0); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - }); - - // finally, skip some implementations we're not interested in - Predicate>> predicate = entry -> { - if (entry.getKey().clazz.startsWith("com/sun/") && entry.getKey().clazz.contains("/internal/")) { - // skip com.sun.*.internal classes as they are not part of the supported JDK API - // even if methods override some publicly visible API - return false; - } - // skip classes that are not part of included modules, but checked due to dependencies - return modulePredicate.test(entry.getKey().module); - }; - writeFile(Path.of(args[0]), deprecationsOnly ? deprecationsByClass : accessibleImplementationsByClass, predicate); - } - - 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()); + writeFile( + Path.of(args[0]), + AccessibleJdkMethods.loadAccessibleMethods(modulePredicate), + optionalArgs(args).anyMatch(DEPRECATIONS_ONLY::equals) + ); } private static Stream optionalArgs(String[] args) { @@ -157,209 +78,33 @@ private static boolean isWritableOutputPath(String pathStr) { } } + private static CharSequence writeLine(ModuleClass moduleClass, AccessibleMethod method) { + return String.join( + SEPARATOR, + moduleClass.module(), + "", // compatibility with public-callers-finder + "", // compatibility with public-callers-finder + moduleClass.clazz(), + method.descriptor().method(), + method.descriptor().descriptor(), + method.descriptor().isPublic() ? "PUBLIC" : "PROTECTED", + method.descriptor().isStatic() ? "STATIC" : "", + method.isFinal() ? "FINAL" : "" + ); + } + @SuppressForbidden(reason = "cli tool printing to standard err/out") - private static void writeFile( - Path path, - Map> methods, - Predicate>> predicate - ) throws IOException { + private static void writeFile(Path path, Map> methods, boolean deprecationsOnly) throws IOException { System.out.println("Writing result for " + Runtime.version() + " to " + path.toAbsolutePath()); + Predicate predicate = deprecationsOnly ? AccessibleMethod::isDeprecated : m -> true; Files.write( path, - () -> methods.entrySet().stream().filter(predicate).flatMap(AccessibleMethod::toLines).iterator(), + () -> methods.entrySet() + .stream() + .flatMap(t -> t.getValue().stream().filter(predicate).map(method -> writeLine(t.getKey(), method))) + .iterator(), StandardCharsets.UTF_8 ); } - record ModuleClass(String module, String clazz) { - private static final Comparator COMPARATOR = Comparator.comparing(ModuleClass::module) - .thenComparing(ModuleClass::clazz); - } - - 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(ModuleClass moduleClass) { - return String.join( - SEPARATOR, - moduleClass.module, - "", // compatibility with public-callers-finder - "", // compatibility with public-callers-finder - moduleClass.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 Map moduleNameByClass; - private final Map> exportsByModule; - private final Map> accessibleImplementationsByClass; - private final Map> inheritableAccessByClass; - private final Map> deprecationsByClass; - - private Set accessibleImplementations; - private Set inheritableAccess; - private Set deprecations; - - private ModuleClass moduleClass; - private boolean isPublicClass; - private boolean isFinalClass; - private boolean isDeprecatedClass; - private boolean isExported; - - AccessibleClassVisitor( - Map moduleNameByClass, - Map> exportsByModule, - Map> accessibleImplementationsByClass, - Map> inheritableAccessByClass, - Map> deprecationsByClass - ) { - super(ASM9); - this.moduleNameByClass = moduleNameByClass; - this.exportsByModule = exportsByModule; - this.accessibleImplementationsByClass = accessibleImplementationsByClass; - this.inheritableAccessByClass = inheritableAccessByClass; - this.deprecationsByClass = deprecationsByClass; - } - - 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 currentInheritedAccess = newSortedSet(); - if (superName != null) { - var superModuleClass = getModuleClass(superName); - if (accessibleImplementationsByClass.containsKey(superModuleClass) == false) { - visitSuperClass(superName); - } - currentInheritedAccess.addAll(inheritableAccessByClass.getOrDefault(superModuleClass, emptySet())); - } - if (interfaces != null && interfaces.length > 0) { - for (var interfaceName : interfaces) { - var interfaceModuleClass = getModuleClass(interfaceName); - if (accessibleImplementationsByClass.containsKey(interfaceModuleClass) == false) { - visitInterface(interfaceName); - } - currentInheritedAccess.addAll(inheritableAccessByClass.getOrDefault(interfaceModuleClass, emptySet())); - } - } - // only initialize local state AFTER visiting all dependencies above! - super.visit(version, access, name, signature, superName, interfaces); - this.moduleClass = getModuleClass(name); - this.isExported = getModuleExports(moduleClass.module).contains(getPackageName(name)); - this.isPublicClass = (access & ACC_PUBLIC) != 0; - this.isFinalClass = (access & ACC_FINAL) != 0; - this.isDeprecatedClass = (access & ACC_DEPRECATED) != 0; - this.inheritableAccess = currentInheritedAccess; - this.accessibleImplementations = newSortedSet(); - this.deprecations = newSortedSet(); - } - - private ModuleClass getModuleClass(String name) { - String module = moduleNameByClass.get(name); - if (module == null) { - throw new IllegalStateException("Unknown module for class: " + name); - } - return new ModuleClass(module, name); - } - - private Set getModuleExports(String module) { - Set exports = exportsByModule.get(module); - if (exports == null) { - throw new IllegalStateException("Unknown exports for module: " + module); - } - return exports; - } - - @Override - public void visitEnd() { - super.visitEnd(); - if (accessibleImplementationsByClass.put(moduleClass, unmodifiableSet(accessibleImplementations)) != null - || inheritableAccessByClass.put(moduleClass, unmodifiableSet(inheritableAccess)) != null - || deprecationsByClass.put(moduleClass, unmodifiableSet(deprecations)) != null) { - throw new IllegalStateException("Class " + moduleClass.clazz + " was already visited!"); - } - } - - private static Set unmodifiableSet(Set set) { - return set.isEmpty() ? emptySet() : Collections.unmodifiableSet(set); - } - - @SuppressForbidden(reason = "cli tool printing to standard err/out") - 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()); - } - } - - @SuppressForbidden(reason = "cli tool printing to standard err/out") - 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()); - } - } - - 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; - boolean isDeprecated = (access & ACC_DEPRECATED) != 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, to be accessible outside the JDK the method must be either: - // - public or - // - protected if not a final class - if (isPublic || isFinalClass == false) { - accessibleImplementations.add(method); - // if public and not static, the method can be accessible on non-public and non-exported subclasses, - // but skip constructors - if (isPublic && isStatic == false && name.equals("") == false) { - inheritableAccess.add(method); - } - if (isDeprecatedClass || isDeprecated) { - deprecations.add(method); - } - } - } else if (inheritableAccess.contains(method)) { - accessibleImplementations.add(method); - if (isDeprecatedClass || isDeprecated) { - deprecations.add(method); - } - } - return mv; - } - } } From 56773b8ddd3b0acf47e00bb2c1e57dd0eacab63a Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Mon, 27 Oct 2025 11:39:58 +0100 Subject: [PATCH 2/9] fix readme --- libs/entitlement/tools/public-callers-finder/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/entitlement/tools/public-callers-finder/README.md b/libs/entitlement/tools/public-callers-finder/README.md index b6d5d3cb02986..3b3fa12d07ace 100644 --- a/libs/entitlement/tools/public-callers-finder/README.md +++ b/libs/entitlement/tools/public-callers-finder/README.md @@ -35,7 +35,7 @@ If `-Druntime.java` is not provided, the bundled JDK is used. Examples: ```bash -./gradlew :libs:entitlement:tools:public-callers-finder:run --args="sensitive-methods.tsv true" --transitive --check-instrumentation" +./gradlew :libs:entitlement:tools:public-callers-finder:run --args="sensitive-methods.tsv --transitive --check-instrumentation" ``` The tool writes the following `TAB`-separated columns to standard out: From de7c2b8b188133a433c48434c22508a9c341d5e0 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Mon, 27 Oct 2025 14:37:38 +0100 Subject: [PATCH 3/9] Exclude some internal, low-level classes from transitive searches for public callers. Relates to ES-13117 --- .../FindUsagesClassVisitor.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java index 9124897e2672f..df14027bf3e9b 100644 --- a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java +++ b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java @@ -18,14 +18,25 @@ import java.lang.constant.ClassDesc; import java.lang.reflect.AccessFlag; import java.util.EnumSet; +import java.util.Map; import java.util.Set; +import static java.util.Collections.emptySet; import static org.objectweb.asm.Opcodes.ACC_PROTECTED; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ASM9; class FindUsagesClassVisitor extends ClassVisitor { + /** + * Internal classes that should be skipped for further transitive analysis. + */ + private static Map> SKIPS = Map.of( + // heavily used internal low-level APIs used to write bytecode by MethodHandles and similar + new MethodDescriptor("java/nio/file/Files", "write", "(Ljava/nio/file/Path;[B[Ljava/nio/file/OpenOption;)Ljava/nio/file/Path;"), + Set.of("jdk/internal/foreign/abi/BindingSpecializer", "jdk/internal/util/ClassFileDumper") + ); + private int classAccess; private boolean accessibleViaInterfaces; @@ -84,6 +95,9 @@ public void visitSource(String source, String debug) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + if (SKIPS.getOrDefault(methodToFind, emptySet()).contains(className)) { + return null; + } return new FindUsagesMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions), name, descriptor, access); } From 30415c6467042e4e76399ab1798eb3fcb8909822 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Mon, 27 Oct 2025 16:04:16 +0100 Subject: [PATCH 4/9] Use AccessibleJdkMethods to consistently calculate external visibility. Relates to ES-13117 --- .../tools/AccessibleJdkMethods.java | 6 +- .../entitlement/tools/ExternalAccess.java | 9 +- .../entitlement/tools/Utils.java | 7 +- .../tools/jdkapi/JdkApiExtractor.java | 27 +--- .../tools/public-callers-finder/README.md | 4 +- .../FindUsagesClassVisitor.java | 66 +++----- .../tools/publiccallersfinder/Main.java | 143 ++++++++++-------- .../scanner/SecurityCheckClassVisitor.java | 3 +- 8 files changed, 123 insertions(+), 142 deletions(-) diff --git a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java index c7e315420ab10..d445fbe05ba8c 100644 --- a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java +++ b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java @@ -9,6 +9,7 @@ package org.elasticsearch.entitlement.tools; +import org.elasticsearch.core.Tuple; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; @@ -23,6 +24,7 @@ import java.util.TreeMap; import java.util.TreeSet; import java.util.function.Predicate; +import java.util.stream.Stream; import static java.util.Collections.emptySet; @@ -53,7 +55,7 @@ public record ModuleClass(String module, String clazz) { .thenComparing(ModuleClass::clazz); } - public static Map> loadAccessibleMethods(Predicate modulePredicate) throws IOException { + public static Stream> loadAccessibleMethods(Predicate modulePredicate) throws IOException { // 1st: map class names to module names (including later excluded modules) for lookup in 2nd step final Map moduleNameByClass = Utils.loadClassToModuleMapping(); final Map> exportsByModule = Utils.loadExportsByModule(); @@ -66,7 +68,7 @@ public static Map> loadAccessibleMethods(Pred } }); - return visitor.getAccessibleMethods(); + return visitor.getAccessibleMethods().entrySet().stream().flatMap(e -> e.getValue().stream().map(m -> Tuple.tuple(e.getKey(), m))); } private static class AccessibleMethodsVisitor extends ClassVisitor { diff --git a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/ExternalAccess.java b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/ExternalAccess.java index e36f760605cba..d340b4b625b7b 100644 --- a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/ExternalAccess.java +++ b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/ExternalAccess.java @@ -24,12 +24,7 @@ public static String toString(EnumSet externalAccesses) { return externalAccesses.stream().map(Enum::toString).collect(Collectors.joining(DELIMITER)); } - public static EnumSet fromPermissions( - boolean packageExported, - boolean publicClass, - boolean publicMethod, - boolean protectedMethod - ) { + public static EnumSet fromPermissions(boolean publicAccessible, boolean publicMethod, boolean protectedMethod) { if (publicMethod && protectedMethod) { throw new IllegalArgumentException(); } @@ -41,7 +36,7 @@ public static EnumSet fromPermissions( externalAccesses.add(ExternalAccess.PROTECTED_METHOD); } - if (packageExported && publicClass) { + if (publicAccessible) { externalAccesses.add(ExternalAccess.PUBLIC_CLASS); } return externalAccesses; 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 717a094bdb1c4..bea3430102519 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 @@ -49,6 +49,10 @@ public class Utils { && m.contains(".internal.") == false && m.contains(".incubator.") == false; + public static final Predicate modulePredicate(boolean includeIncubator) { + return includeIncubator == false ? DEFAULT_MODULE_PREDICATE : DEFAULT_MODULE_PREDICATE.or(m -> m.contains(".incubator.")); + } + public static Map> loadExportsByModule() throws IOException { var modulesExports = new HashMap>(); try (var stream = Files.walk(JRT_FS.getPath("modules"))) { @@ -71,7 +75,7 @@ public static Map> loadExportsByModule() throws IOException return Collections.unmodifiableMap(modulesExports); } - public static Map loadClassToModuleMapping() throws IOException{ + public static Map loadClassToModuleMapping() throws IOException { Map moduleNameByClass = new HashMap<>(); Utils.walkJdkModules(m -> true, Collections.emptyMap(), (moduleName, moduleClasses, moduleExports) -> { for (var classFile : moduleClasses) { @@ -84,7 +88,6 @@ public static Map loadClassToModuleMapping() throws IOException{ return Collections.unmodifiableMap(moduleNameByClass); } - public interface JdkModuleConsumer { void accept(String moduleName, List moduleClasses, Set moduleExports); } 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 fba15ccf5e45e..e5c38738558fe 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 @@ -10,6 +10,7 @@ package org.elasticsearch.entitlement.tools.jdkapi; import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.core.Tuple; import org.elasticsearch.entitlement.tools.AccessibleJdkMethods; import org.elasticsearch.entitlement.tools.AccessibleJdkMethods.AccessibleMethod; import org.elasticsearch.entitlement.tools.AccessibleJdkMethods.ModuleClass; @@ -21,7 +22,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; -import java.util.Map; import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -37,14 +37,9 @@ public class JdkApiExtractor { public static void main(String[] args) throws IOException { validateArgs(args); - Predicate modulePredicate = Utils.DEFAULT_MODULE_PREDICATE.or( - m -> optionalArgs(args).anyMatch(INCLUDE_INCUBATOR::equals) && m.contains(".incubator.") - ); - writeFile( - Path.of(args[0]), - AccessibleJdkMethods.loadAccessibleMethods(modulePredicate), - optionalArgs(args).anyMatch(DEPRECATIONS_ONLY::equals) - ); + boolean includeIncubator = optionalArgs(args).anyMatch(INCLUDE_INCUBATOR::equals); + boolean deprecationsOnly = optionalArgs(args).anyMatch(DEPRECATIONS_ONLY::equals); + writeFile(Path.of(args[0]), AccessibleJdkMethods.loadAccessibleMethods(Utils.modulePredicate(includeIncubator)), deprecationsOnly); } private static Stream optionalArgs(String[] args) { @@ -94,17 +89,11 @@ private static CharSequence writeLine(ModuleClass moduleClass, AccessibleMethod } @SuppressForbidden(reason = "cli tool printing to standard err/out") - private static void writeFile(Path path, Map> methods, boolean deprecationsOnly) throws IOException { + private static void writeFile(Path path, Stream> methods, boolean deprecationsOnly) + throws IOException { System.out.println("Writing result for " + Runtime.version() + " to " + path.toAbsolutePath()); - Predicate predicate = deprecationsOnly ? AccessibleMethod::isDeprecated : m -> true; - Files.write( - path, - () -> methods.entrySet() - .stream() - .flatMap(t -> t.getValue().stream().filter(predicate).map(method -> writeLine(t.getKey(), method))) - .iterator(), - StandardCharsets.UTF_8 - ); + Predicate> predicate = deprecationsOnly ? t -> t.v2().isDeprecated() : t -> true; + Files.write(path, () -> methods.filter(predicate).map(t -> writeLine(t.v1(), t.v2())).iterator(), StandardCharsets.UTF_8); } } diff --git a/libs/entitlement/tools/public-callers-finder/README.md b/libs/entitlement/tools/public-callers-finder/README.md index 3b3fa12d07ace..00c327ba0652b 100644 --- a/libs/entitlement/tools/public-callers-finder/README.md +++ b/libs/entitlement/tools/public-callers-finder/README.md @@ -15,7 +15,7 @@ it treats calls to `super` in `S.m` as regular calls (e.g. `example() -> S.m() - In order to run the tool, use: ```shell -./gradlew :libs:entitlement:tools:public-callers-finder:run [-Druntime.java=25] --args=" [--transitive] [--check-instrumentation]" +./gradlew :libs:entitlement:tools:public-callers-finder:run [-Druntime.java=25] --args=" [--transitive] [--check-instrumentation] [--include-incubator]" ``` - `input-file` is a `TAB`-separated TSV file containing the following columns: @@ -31,6 +31,8 @@ In order to run the tool, use: - optional: `--check-instrumentation` to check if methods are instrumented for entitlements. +- optional: `--include-incubator` to include incubator modules (e.g. `jdk.incubator.vector`). + If `-Druntime.java` is not provided, the bundled JDK is used. Examples: diff --git a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java index df14027bf3e9b..e26e486316263 100644 --- a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java +++ b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java @@ -13,13 +13,11 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; -import org.objectweb.asm.Type; -import java.lang.constant.ClassDesc; -import java.lang.reflect.AccessFlag; import java.util.EnumSet; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import static java.util.Collections.emptySet; import static org.objectweb.asm.Opcodes.ACC_PROTECTED; @@ -37,27 +35,28 @@ class FindUsagesClassVisitor extends ClassVisitor { Set.of("jdk/internal/foreign/abi/BindingSpecializer", "jdk/internal/util/ClassFileDumper") ); - private int classAccess; - private boolean accessibleViaInterfaces; - record MethodDescriptor(String className, String methodName, String methodDescriptor) {} - record EntryPoint(String moduleName, String source, int line, MethodDescriptor method, EnumSet access) {} + record EntryPoint(String moduleName, String source, int line, MethodDescriptor method, EnumSet access) { } interface CallerConsumer { void accept(String source, int line, MethodDescriptor method, EnumSet access); } - private final Set moduleExports; private final MethodDescriptor methodToFind; + private final Predicate isPublicAccessible; private final CallerConsumer callers; private String className; private String source; - protected FindUsagesClassVisitor(Set moduleExports, MethodDescriptor methodToFind, CallerConsumer callers) { + protected FindUsagesClassVisitor( + MethodDescriptor methodToFind, + Predicate isPublicAccessible, + CallerConsumer callers + ) { super(ASM9); - this.moduleExports = moduleExports; this.methodToFind = methodToFind; + this.isPublicAccessible = isPublicAccessible; this.callers = callers; } @@ -65,26 +64,6 @@ protected FindUsagesClassVisitor(Set moduleExports, MethodDescriptor met public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { super.visit(version, access, name, signature, superName, interfaces); this.className = name; - this.classAccess = access; - if (interfaces.length > 0) { - this.accessibleViaInterfaces = findAccessibility(interfaces, moduleExports); - } - } - - private static boolean findAccessibility(String[] interfaces, Set moduleExports) { - var accessibleViaInterfaces = false; - for (var interfaceName : interfaces) { - if (moduleExports.contains(getPackageName(interfaceName))) { - var interfaceType = Type.getObjectType(interfaceName); - try { - var clazz = Class.forName(interfaceType.getClassName()); - if (clazz.accessFlags().contains(AccessFlag.PUBLIC)) { - accessibleViaInterfaces = true; - } - } catch (ClassNotFoundException ignored) {} - } - } - return accessibleViaInterfaces; } @Override @@ -101,10 +80,6 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str return new FindUsagesMethodVisitor(super.visitMethod(access, name, descriptor, signature, exceptions), name, descriptor, access); } - private static String getPackageName(String className) { - return ClassDesc.ofInternalName(className).packageName(); - } - private class FindUsagesMethodVisitor extends MethodVisitor { private final String methodName; @@ -123,18 +98,17 @@ protected FindUsagesMethodVisitor(MethodVisitor mv, String methodName, String me public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); - if (methodToFind.className.equals(owner)) { - if (methodToFind.methodName.equals(name)) { - if (methodToFind.methodDescriptor == null || methodToFind.methodDescriptor.equals(descriptor)) { - EnumSet externalAccess = ExternalAccess.fromPermissions( - moduleExports.contains(getPackageName(className)), - accessibleViaInterfaces || (classAccess & ACC_PUBLIC) != 0, - (methodAccess & ACC_PUBLIC) != 0, - (methodAccess & ACC_PROTECTED) != 0 - ); - callers.accept(source, line, new MethodDescriptor(className, methodName, methodDescriptor), externalAccess); - } - } + if (methodToFind.className.equals(owner) + && methodToFind.methodName.equals(name) + && (methodToFind.methodDescriptor == null || methodToFind.methodDescriptor.equals(descriptor))) { + + MethodDescriptor method = new MethodDescriptor(className, methodName, methodDescriptor); + EnumSet externalAccess = ExternalAccess.fromPermissions( + isPublicAccessible.test(method), + (methodAccess & ACC_PUBLIC) != 0, + (methodAccess & ACC_PROTECTED) != 0 + ); + callers.accept(source, line, method, externalAccess); } } diff --git a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/Main.java b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/Main.java index f8a2edf0c8ac2..365d149793c2d 100644 --- a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/Main.java +++ b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/Main.java @@ -10,8 +10,11 @@ package org.elasticsearch.entitlement.tools.publiccallersfinder; import org.elasticsearch.core.SuppressForbidden; +import org.elasticsearch.entitlement.tools.AccessibleJdkMethods; import org.elasticsearch.entitlement.tools.ExternalAccess; import org.elasticsearch.entitlement.tools.Utils; +import org.elasticsearch.entitlement.tools.publiccallersfinder.FindUsagesClassVisitor.EntryPoint; +import org.elasticsearch.entitlement.tools.publiccallersfinder.FindUsagesClassVisitor.MethodDescriptor; import org.objectweb.asm.ClassReader; import java.io.IOException; @@ -25,111 +28,119 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Collections.emptyMap; +import static java.util.function.Predicate.not; + public class Main { private static final String SEPARATOR = "\t"; private static String TRANSITIVE = "--transitive"; private static String CHECK_INSTRUMENTATION = "--check-instrumentation"; - private static Set OPTIONAL_ARGS = Set.of(TRANSITIVE, CHECK_INSTRUMENTATION); + private static String INCLUDE_INCUBATOR = "--include-incubator"; + + private static Set OPTIONAL_ARGS = Set.of(TRANSITIVE, CHECK_INSTRUMENTATION, INCLUDE_INCUBATOR); - private static final Set INSTRUMENTED_METHODS = new HashSet<>(); + private static final Set INSTRUMENTED_METHODS = new HashSet<>(); + private static final Set ACCESSIBLE_JDK_METHODS = new HashSet<>(); - record CallChain(FindUsagesClassVisitor.EntryPoint entryPoint, CallChain next) {} + record CallChain(EntryPoint entryPoint, CallChain next) { + boolean isPublic() { + return ExternalAccess.isExternallyAccessible(entryPoint.access()); + } + + CallChain prepend(MethodDescriptor method, EnumSet access, String module, String source, int line) { + return new CallChain(new EntryPoint(module, source, line, method, access), this); + } + + static CallChain firstLevel(MethodDescriptor method, EnumSet access, String module, String source, int line) { + return new CallChain(new EntryPoint(module, source, line, method, access), null); + } + } interface UsageConsumer { void usageFound(CallChain originalEntryPoint, CallChain newMethod); } + private static void visitClasses(FindUsagesClassVisitor visitor, List classes) { + for (var clazz : classes) { + try { + ClassReader cr = new ClassReader(Files.newInputStream(clazz)); + cr.accept(visitor, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + private static void findTransitiveUsages( Collection firstLevelCallers, List classesToScan, - Set moduleExports, boolean bubbleUpFromPublic, UsageConsumer usageConsumer ) { for (var caller : firstLevelCallers) { var methodsToCheck = new ArrayDeque<>(Set.of(caller)); - var methodsSeen = new HashSet(); + var methodsSeen = new HashSet(); while (methodsToCheck.isEmpty() == false) { var methodToCheck = methodsToCheck.removeFirst(); var entryPoint = methodToCheck.entryPoint(); - var visitor2 = new FindUsagesClassVisitor(moduleExports, entryPoint.method(), (source, line, method, access) -> { - var newMethod = new CallChain( - new FindUsagesClassVisitor.EntryPoint(entryPoint.moduleName(), source, line, method, access), - methodToCheck - ); - - var notSeenBefore = methodsSeen.add(newMethod.entryPoint()); - if (notSeenBefore) { - if (ExternalAccess.isExternallyAccessible(access)) { - usageConsumer.usageFound(caller.next(), newMethod); - } - if (access.contains(ExternalAccess.PUBLIC_METHOD) == false || bubbleUpFromPublic) { - methodsToCheck.add(newMethod); + var visitor2 = new FindUsagesClassVisitor( + entryPoint.method(), + ACCESSIBLE_JDK_METHODS::contains, + (source, line, method, access) -> { + var newMethod = methodToCheck.prepend(method, access, entryPoint.moduleName(), source, line); + var notSeenBefore = methodsSeen.add(newMethod.entryPoint()); + if (notSeenBefore) { + if (newMethod.isPublic()) { + usageConsumer.usageFound(caller.next(), newMethod); + } + if (bubbleUpFromPublic || newMethod.isPublic() == false) { + methodsToCheck.add(newMethod); + } } } - }); - - for (var classFile : classesToScan) { - try { - ClassReader cr = new ClassReader(Files.newInputStream(classFile)); - cr.accept(visitor2, 0); - } catch (IOException e) { - throw new RuntimeException(e); - } - } + ); + visitClasses(visitor2, classesToScan); } } } private static void identifyTopLevelEntryPoints( - FindUsagesClassVisitor.MethodDescriptor methodToFind, + Predicate modulePredicate, + MethodDescriptor methodToFind, String methodToFindModule, EnumSet methodToFindAccess, boolean bubbleUpFromPublic ) throws IOException { - Utils.walkJdkModules((moduleName, moduleClasses, moduleExports) -> { + CallChain firstLevel = CallChain.firstLevel(methodToFind, methodToFindAccess, methodToFindModule, "", 0); + Utils.walkJdkModules(modulePredicate, emptyMap(), (moduleName, moduleClasses, ignore) -> { var originalCallers = new ArrayList(); var visitor = new FindUsagesClassVisitor( - moduleExports, methodToFind, - (source, line, method, access) -> originalCallers.add( - new CallChain( - new FindUsagesClassVisitor.EntryPoint(moduleName, source, line, method, access), - new CallChain( - new FindUsagesClassVisitor.EntryPoint(methodToFindModule, "", 0, methodToFind, methodToFindAccess), - null - ) - ) - ) + ACCESSIBLE_JDK_METHODS::contains, + (source, line, method, access) -> originalCallers.add(firstLevel.prepend(method, access, moduleName, source, line)) ); + visitClasses(visitor, moduleClasses); - for (var classFile : moduleClasses) { - try { - ClassReader cr = new ClassReader(Files.newInputStream(classFile)); - cr.accept(visitor, 0); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - originalCallers.stream().filter(c -> ExternalAccess.isExternallyAccessible(c.entryPoint().access())).forEach(c -> { + originalCallers.stream().filter(CallChain::isPublic).forEach(c -> { var originalCaller = c.next(); printRow(getEntryPointColumns(c.entryPoint().moduleName(), c.entryPoint()), getOriginalEntryPointColumns(originalCaller)); }); - var firstLevelCallers = bubbleUpFromPublic ? originalCallers : originalCallers.stream().filter(Main::isNotFullyPublic).toList(); + var firstLevelCallers = bubbleUpFromPublic + ? originalCallers + : originalCallers.stream().filter(not(CallChain::isPublic)).toList(); if (firstLevelCallers.isEmpty() == false) { findTransitiveUsages( firstLevelCallers, moduleClasses, - moduleExports, bubbleUpFromPublic, (originalEntryPoint, newMethod) -> printRow( getEntryPointColumns(moduleName, newMethod.entryPoint()), @@ -140,11 +151,6 @@ private static void identifyTopLevelEntryPoints( }); } - private static boolean isNotFullyPublic(CallChain c) { - return (c.entryPoint().access().contains(ExternalAccess.PUBLIC_CLASS) - && c.entryPoint().access().contains(ExternalAccess.PUBLIC_METHOD)) == false; - } - @SuppressForbidden(reason = "This tool prints the CSV to stdout") private static void printRow(CharSequence[] entryPointColumns, CharSequence[] originalEntryPointColumns) { String row = String.join( @@ -154,7 +160,7 @@ private static void printRow(CharSequence[] entryPointColumns, CharSequence[] or System.out.println(row); } - private static CharSequence[] getEntryPointColumns(String moduleName, FindUsagesClassVisitor.EntryPoint e) { + private static CharSequence[] getEntryPointColumns(String moduleName, EntryPoint e) { if (INSTRUMENTED_METHODS.isEmpty()) { return new CharSequence[] { moduleName, @@ -186,8 +192,7 @@ private static CharSequence[] getOriginalEntryPointColumns(CallChain originalCal } interface MethodDescriptorConsumer { - void accept(FindUsagesClassVisitor.MethodDescriptor methodDescriptor, String moduleName, EnumSet access) - throws IOException; + void accept(MethodDescriptor methodDescriptor, String moduleName, EnumSet access) throws IOException; } private static void parseCsv(Path csvPath, MethodDescriptorConsumer methodConsumer) throws IOException { @@ -199,7 +204,7 @@ private static void parseCsv(Path csvPath, MethodDescriptorConsumer methodConsum var methodName = tokens[4]; var methodDescriptor = tokens[5]; var access = ExternalAccess.fromString(tokens[6]); - methodConsumer.accept(new FindUsagesClassVisitor.MethodDescriptor(className, methodName, methodDescriptor), moduleName, access); + methodConsumer.accept(new MethodDescriptor(className, methodName, methodDescriptor), moduleName, access); } } @@ -213,7 +218,7 @@ private static void loadInstrumentedMethods(Path dump) throws IOException { if (parts.length != 3) { throw new IllegalStateException("Invalid line in entitlements dump: " + Arrays.toString(parts)); } - INSTRUMENTED_METHODS.add(new FindUsagesClassVisitor.MethodDescriptor(parts[0], parts[1], parts[2])); + INSTRUMENTED_METHODS.add(new MethodDescriptor(parts[0], parts[1], parts[2])); } } @@ -240,10 +245,22 @@ public static void main(String[] args) throws IOException { var csvFilePath = Path.of(args[0]); boolean bubbleUpFromPublic = optionalArgs(args).anyMatch(TRANSITIVE::equals); boolean checkInstrumentation = optionalArgs(args).anyMatch(CHECK_INSTRUMENTATION::equals); + boolean includeIncubator = optionalArgs(args).anyMatch(INCLUDE_INCUBATOR::equals); + + AccessibleJdkMethods.loadAccessibleMethods(Utils.DEFAULT_MODULE_PREDICATE) + .forEach( + t -> ACCESSIBLE_JDK_METHODS.add( + new MethodDescriptor(t.v1().clazz(), t.v2().descriptor().method(), t.v2().descriptor().descriptor()) + ) + ); if (checkInstrumentation && System.getProperty("es.entitlements.dump") != null) { loadInstrumentedMethods(Path.of(System.getProperty("es.entitlements.dump"))); } - parseCsv(csvFilePath, (method, module, access) -> identifyTopLevelEntryPoints(method, module, access, bubbleUpFromPublic)); + Predicate modulePredicate = Utils.modulePredicate(includeIncubator); + parseCsv( + csvFilePath, + (method, module, access) -> identifyTopLevelEntryPoints(modulePredicate, method, module, access, bubbleUpFromPublic) + ); } } diff --git a/libs/entitlement/tools/securitymanager-scanner/src/main/java/org/elasticsearch/entitlement/tools/securitymanager/scanner/SecurityCheckClassVisitor.java b/libs/entitlement/tools/securitymanager-scanner/src/main/java/org/elasticsearch/entitlement/tools/securitymanager/scanner/SecurityCheckClassVisitor.java index 55a9ded797d6f..a0f986b5cc190 100644 --- a/libs/entitlement/tools/securitymanager-scanner/src/main/java/org/elasticsearch/entitlement/tools/securitymanager/scanner/SecurityCheckClassVisitor.java +++ b/libs/entitlement/tools/securitymanager-scanner/src/main/java/org/elasticsearch/entitlement/tools/securitymanager/scanner/SecurityCheckClassVisitor.java @@ -206,8 +206,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri if (SECURITY_MANAGER_INTERNAL_NAME.equals(owner)) { EnumSet externalAccesses = ExternalAccess.fromPermissions( - moduleExports.contains(getPackageName(className)), - (classAccess & ACC_PUBLIC) != 0, + moduleExports.contains(getPackageName(className)) && (classAccess & ACC_PUBLIC) != 0, (methodAccess & ACC_PUBLIC) != 0, (methodAccess & ACC_PROTECTED) != 0 ); From e3678ba2dc6139f9676cc7d57fc1b0228165db64 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Mon, 27 Oct 2025 16:11:03 +0100 Subject: [PATCH 5/9] spotless --- .../tools/publiccallersfinder/FindUsagesClassVisitor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java index e26e486316263..6e5824d20759e 100644 --- a/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java +++ b/libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java @@ -37,7 +37,7 @@ class FindUsagesClassVisitor extends ClassVisitor { record MethodDescriptor(String className, String methodName, String methodDescriptor) {} - record EntryPoint(String moduleName, String source, int line, MethodDescriptor method, EnumSet access) { } + record EntryPoint(String moduleName, String source, int line, MethodDescriptor method, EnumSet access) {} interface CallerConsumer { void accept(String source, int line, MethodDescriptor method, EnumSet access); From d3b43463dc01f6430800f69dfdc003e1b835d9dc Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Tue, 28 Oct 2025 15:01:03 +0100 Subject: [PATCH 6/9] rename --- .../entitlement/tools/AccessibleJdkMethods.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java index d445fbe05ba8c..225e70ecfc714 100644 --- a/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java +++ b/libs/entitlement/tools/common/src/main/java/org/elasticsearch/entitlement/tools/AccessibleJdkMethods.java @@ -128,20 +128,20 @@ void visitOnce(ModuleClass moduleClass) { public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { final Set currentInheritedAccess = newSortedSet(); if (superName != null) { - var superModuleClass = getModuleClass(superName); + var superModuleClass = getModuleClassFromName(superName); visitOnce(superModuleClass); currentInheritedAccess.addAll(inheritableAccessByClass.getOrDefault(superModuleClass, emptySet())); } if (interfaces != null && interfaces.length > 0) { for (var interfaceName : interfaces) { - var interfaceModuleClass = getModuleClass(interfaceName); + var interfaceModuleClass = getModuleClassFromName(interfaceName); visitOnce(interfaceModuleClass); currentInheritedAccess.addAll(inheritableAccessByClass.getOrDefault(interfaceModuleClass, emptySet())); } } // only initialize local state AFTER visiting all dependencies above! super.visit(version, access, name, signature, superName, interfaces); - this.moduleClass = getModuleClass(name); + this.moduleClass = getModuleClassFromName(name); this.isExported = getModuleExports(moduleClass.module()).contains(getPackageName(name)); this.isPublicClass = (access & Opcodes.ACC_PUBLIC) != 0; this.isFinalClass = (access & Opcodes.ACC_FINAL) != 0; @@ -150,7 +150,7 @@ public void visit(int version, int access, String name, String signature, String this.accessibleImplementations = newSortedSet(); } - private ModuleClass getModuleClass(String name) { + private ModuleClass getModuleClassFromName(String name) { String module = moduleNameByClass.get(name); if (module == null) { throw new IllegalStateException("Unknown module for class: " + name); From 78dcd4dcc0f84e578d58c3724a4f16849ca266de Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 29 Oct 2025 12:17:51 +0100 Subject: [PATCH 7/9] fix licence check --- libs/entitlement/tools/common/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/entitlement/tools/common/build.gradle b/libs/entitlement/tools/common/build.gradle index 6c541c318273d..c22337a74580e 100644 --- a/libs/entitlement/tools/common/build.gradle +++ b/libs/entitlement/tools/common/build.gradle @@ -18,3 +18,7 @@ dependencies { implementation 'org.ow2.asm:asm:9.9' implementation 'org.ow2.asm:asm-util:9.9' } + +tasks.named("dependencyLicenses").configure { + mapping from: /asm-.*/, to: 'asm' +} From 1d552219d6a0327634504f90b9cf3e53be661b81 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 29 Oct 2025 12:53:16 +0100 Subject: [PATCH 8/9] fix license check --- .../tools/common/licenses/asm-LICENSE.txt | 26 +++++++++++++++++++ .../tools/common/licenses/asm-NOTICE.txt | 1 + 2 files changed, 27 insertions(+) create mode 100644 libs/entitlement/tools/common/licenses/asm-LICENSE.txt create mode 100644 libs/entitlement/tools/common/licenses/asm-NOTICE.txt diff --git a/libs/entitlement/tools/common/licenses/asm-LICENSE.txt b/libs/entitlement/tools/common/licenses/asm-LICENSE.txt new file mode 100644 index 0000000000000..afb064f2f2666 --- /dev/null +++ b/libs/entitlement/tools/common/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/common/licenses/asm-NOTICE.txt b/libs/entitlement/tools/common/licenses/asm-NOTICE.txt new file mode 100644 index 0000000000000..8d1c8b69c3fce --- /dev/null +++ b/libs/entitlement/tools/common/licenses/asm-NOTICE.txt @@ -0,0 +1 @@ + From e68465f482592546f264965a9a49b5d25fe48a75 Mon Sep 17 00:00:00 2001 From: Moritz Mack Date: Wed, 29 Oct 2025 13:49:45 +0100 Subject: [PATCH 9/9] fix thirdPartyAudit --- libs/entitlement/tools/common/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/entitlement/tools/common/build.gradle b/libs/entitlement/tools/common/build.gradle index c22337a74580e..abb2fa79a9c4b 100644 --- a/libs/entitlement/tools/common/build.gradle +++ b/libs/entitlement/tools/common/build.gradle @@ -22,3 +22,7 @@ dependencies { tasks.named("dependencyLicenses").configure { mapping from: /asm-.*/, to: 'asm' } + +tasks.named("thirdPartyAudit").configure { + ignoreMissingClasses() +}