Entitlements tools: public callers finder (#116257)

* WIP: Tool to find all public caller from a starting list of (JDK) methods.

* Add public-callers-finder tool, extract common stuff to common module

* Adjustments to visibility/functions and classes and modules to print out

* Spotless

* Missing gradle configuration

* Add details in README as requested in PR

* Update ASM version

* Including protected methods
This commit is contained in:
Lorenzo Dematté 2024-12-13 16:24:54 +01:00 committed by GitHub
parent 1bad1cf6b2
commit 5411b93d49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 603 additions and 50 deletions

View file

@ -0,0 +1,68 @@
/*
* 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 java.util.Arrays;
import java.util.EnumSet;
import java.util.stream.Collectors;
public enum ExternalAccess {
PUBLIC_CLASS,
PUBLIC_METHOD,
PROTECTED_METHOD;
private static final String DELIMITER = ":";
public static String toString(EnumSet<ExternalAccess> externalAccesses) {
return externalAccesses.stream().map(Enum::toString).collect(Collectors.joining(DELIMITER));
}
public static EnumSet<ExternalAccess> fromPermissions(
boolean packageExported,
boolean publicClass,
boolean publicMethod,
boolean protectedMethod
) {
if (publicMethod && protectedMethod) {
throw new IllegalArgumentException();
}
EnumSet<ExternalAccess> externalAccesses = EnumSet.noneOf(ExternalAccess.class);
if (publicMethod) {
externalAccesses.add(ExternalAccess.PUBLIC_METHOD);
} else if (protectedMethod) {
externalAccesses.add(ExternalAccess.PROTECTED_METHOD);
}
if (packageExported && publicClass) {
externalAccesses.add(ExternalAccess.PUBLIC_CLASS);
}
return externalAccesses;
}
public static boolean isExternallyAccessible(EnumSet<ExternalAccess> access) {
return access.contains(ExternalAccess.PUBLIC_CLASS)
&& (access.contains(ExternalAccess.PUBLIC_METHOD) || access.contains(ExternalAccess.PROTECTED_METHOD));
}
public static EnumSet<ExternalAccess> fromString(String accessAsString) {
if ("PUBLIC".equals(accessAsString)) {
return EnumSet.of(ExternalAccess.PUBLIC_CLASS, ExternalAccess.PUBLIC_METHOD);
}
if ("PUBLIC-METHOD".equals(accessAsString)) {
return EnumSet.of(ExternalAccess.PUBLIC_METHOD);
}
if ("PRIVATE".equals(accessAsString)) {
return EnumSet.noneOf(ExternalAccess.class);
}
return EnumSet.copyOf(Arrays.stream(accessAsString.split(DELIMITER)).map(ExternalAccess::valueOf).toList());
}
}

View file

@ -11,16 +11,28 @@ package org.elasticsearch.entitlement.tools;
import java.io.IOException; import java.io.IOException;
import java.lang.module.ModuleDescriptor; import java.lang.module.ModuleDescriptor;
import java.net.URI;
import java.nio.file.FileSystem; import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class Utils { public class Utils {
public static Map<String, Set<String>> findModuleExports(FileSystem fs) throws IOException { private static final Set<String> EXCLUDED_MODULES = Set.of(
"java.desktop",
"jdk.jartool",
"jdk.jdi",
"java.security.jgss",
"jdk.jshell"
);
private static Map<String, Set<String>> findModuleExports(FileSystem fs) throws IOException {
var modulesExports = new HashMap<String, Set<String>>(); var modulesExports = new HashMap<String, Set<String>>();
try (var stream = Files.walk(fs.getPath("modules"))) { try (var stream = Files.walk(fs.getPath("modules"))) {
stream.filter(p -> p.getFileName().toString().equals("module-info.class")).forEach(x -> { stream.filter(p -> p.getFileName().toString().equals("module-info.class")).forEach(x -> {
@ -42,4 +54,27 @@ public class Utils {
return modulesExports; return modulesExports;
} }
public interface JdkModuleConsumer {
void accept(String moduleName, List<Path> moduleClasses, Set<String> moduleExports);
}
public static void walkJdkModules(JdkModuleConsumer c) throws IOException {
FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
var moduleExports = Utils.findModuleExports(fs);
try (var stream = Files.walk(fs.getPath("modules"))) {
var modules = stream.filter(x -> x.toString().endsWith(".class"))
.collect(Collectors.groupingBy(x -> x.subpath(1, 2).toString()));
for (var kv : modules.entrySet()) {
var moduleName = kv.getKey();
if (Utils.EXCLUDED_MODULES.contains(moduleName) == false) {
var thisModuleExports = moduleExports.get(moduleName);
c.accept(moduleName, kv.getValue(), thisModuleExports);
}
}
}
}
} }

View file

@ -0,0 +1,50 @@
This tool scans the JDK on which it is running. It takes a list of methods (compatible with the output of the `securitymanager-scanner` tool), and looks for the "public surface" of these methods (i.e. any class/method accessible from regular Java code that calls into the original list, directly or transitively).
It acts basically as a recursive "Find Usages" in Intellij, stopping at the first fully accessible point (public method on a public class).
The tool scans every method in every class inside the same java module; e.g.
if you have a private method `File#normalizedList`, it will scan `java.base` to find
public methods like `File#list(String)`, `File#list(FilenameFilter, String)` and
`File#listFiles(File)`.
The tool considers implemented interfaces (directly); e.g. if we're looking at a
method `C.m`, where `C implements I`, it will look for calls to `I.m`. It will
also consider (indirectly) calls to `S.m` (where `S` is a supertype of `C`), as
it treats calls to `super` in `S.m` as regular calls (e.g. `example() -> S.m() -> C.m()`).
In order to run the tool, use:
```shell
./gradlew :libs:entitlement:tools:public-callers-finder:run <input-file> [<bubble-up-from-public>]
```
Where `input-file` is a CSV file (columns separated by `TAB`) that contains the following columns:
Module name
1. unused
2. unused
3. unused
4. Fully qualified class name (ASM style, with `/` separators)
5. Method name
6. Method descriptor (ASM signature)
7. Visibility (PUBLIC/PUBLIC-METHOD/PRIVATE)
And `bubble-up-from-public` is a boolean (`true|false`) indicating if the code should stop at the first public method (`false`: default, recommended) or continue to find usages recursively even after reaching the "public surface".
The output of the tool is another CSV file, with one line for each entry-point, columns separated by `TAB`
1. Module name
2. File name (from source root)
3. Line number
4. Fully qualified class name (ASM style, with `/` separators)
5. Method name
6. Method descriptor (ASM signature)
7. Visibility (PUBLIC/PUBLIC-METHOD/PRIVATE)
8. Original caller Module name
9. Original caller Class name (ASM style, with `/` separators)
10. Original caller Method name
11. Original caller Visibility
Examples:
```
java.base DeleteOnExitHook.java 50 java/io/DeleteOnExitHook$1 run ()V PUBLIC java.base java/io/File delete PUBLIC
java.base ZipFile.java 254 java/util/zip/ZipFile <init> (Ljava/io/File;ILjava/nio/charset/Charset;)V PUBLIC java.base java/io/File delete PUBLIC
java.logging FileHandler.java 279 java/util/logging/FileHandler <init> ()V PUBLIC java.base java/io/File delete PUBLIC
```

View file

@ -0,0 +1,61 @@
plugins {
id 'application'
}
apply plugin: 'elasticsearch.build'
apply plugin: 'elasticsearch.publish'
tasks.named("dependencyLicenses").configure {
mapping from: /asm-.*/, to: 'asm'
}
group = 'org.elasticsearch.entitlement.tools'
ext {
javaMainClass = "org.elasticsearch.entitlement.tools.publiccallersfinder.Main"
}
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'
]
}
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(project(':libs:entitlement:tools:common'))
}
tasks.named('forbiddenApisMain').configure {
replaceSignatureFiles 'jdk-signatures'
}
tasks.named("thirdPartyAudit").configure {
ignoreMissingClasses()
}

View file

@ -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.

View file

@ -0,0 +1,141 @@
/*
* 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.publiccallersfinder;
import org.elasticsearch.entitlement.tools.ExternalAccess;
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.Set;
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 {
private int classAccess;
private boolean accessibleViaInterfaces;
record MethodDescriptor(String className, String methodName, String methodDescriptor) {}
record EntryPoint(
String moduleName,
String source,
int line,
String className,
String methodName,
String methodDescriptor,
EnumSet<ExternalAccess> access
) {}
interface CallerConsumer {
void accept(String source, int line, String className, String methodName, String methodDescriptor, EnumSet<ExternalAccess> access);
}
private final Set<String> moduleExports;
private final MethodDescriptor methodToFind;
private final CallerConsumer callers;
private String className;
private String source;
protected FindUsagesClassVisitor(Set<String> moduleExports, MethodDescriptor methodToFind, CallerConsumer callers) {
super(ASM9);
this.moduleExports = moduleExports;
this.methodToFind = methodToFind;
this.callers = callers;
}
@Override
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<String> 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
public void visitSource(String source, String debug) {
super.visitSource(source, debug);
this.source = source;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
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;
private int line;
private final String methodDescriptor;
private final int methodAccess;
protected FindUsagesMethodVisitor(MethodVisitor mv, String methodName, String methodDescriptor, int methodAccess) {
super(ASM9, mv);
this.methodName = methodName;
this.methodDescriptor = methodDescriptor;
this.methodAccess = methodAccess;
}
@Override
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 = ExternalAccess.fromPermissions(
moduleExports.contains(getPackageName(className)),
accessibleViaInterfaces || (classAccess & ACC_PUBLIC) != 0,
(methodAccess & ACC_PUBLIC) != 0,
(methodAccess & ACC_PROTECTED) != 0
);
callers.accept(source, line, className, methodName, methodDescriptor, externalAccess);
}
}
}
}
@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
this.line = line;
}
}
}

View file

@ -0,0 +1,197 @@
/*
* 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.publiccallersfinder;
import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.tools.ExternalAccess;
import org.elasticsearch.entitlement.tools.Utils;
import org.objectweb.asm.ClassReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class Main {
private static final String SEPARATOR = "\t";
record CallChain(FindUsagesClassVisitor.EntryPoint entryPoint, CallChain next) {}
interface UsageConsumer {
void usageFound(CallChain originalEntryPoint, CallChain newMethod);
}
private static void findTransitiveUsages(
Collection<CallChain> firstLevelCallers,
List<Path> classesToScan,
Set<String> moduleExports,
boolean bubbleUpFromPublic,
UsageConsumer usageConsumer
) {
for (var caller : firstLevelCallers) {
var methodsToCheck = new ArrayDeque<>(Set.of(caller));
var methodsSeen = new HashSet<FindUsagesClassVisitor.EntryPoint>();
while (methodsToCheck.isEmpty() == false) {
var methodToCheck = methodsToCheck.removeFirst();
var m = methodToCheck.entryPoint();
var visitor2 = new FindUsagesClassVisitor(
moduleExports,
new FindUsagesClassVisitor.MethodDescriptor(m.className(), m.methodName(), m.methodDescriptor()),
(source, line, className, methodName, methodDescriptor, access) -> {
var newMethod = new CallChain(
new FindUsagesClassVisitor.EntryPoint(
m.moduleName(),
source,
line,
className,
methodName,
methodDescriptor,
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);
}
}
}
);
for (var classFile : classesToScan) {
try {
ClassReader cr = new ClassReader(Files.newInputStream(classFile));
cr.accept(visitor2, 0);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
private static void identifyTopLevelEntryPoints(
FindUsagesClassVisitor.MethodDescriptor methodToFind,
String methodToFindModule,
EnumSet<ExternalAccess> methodToFindAccess,
boolean bubbleUpFromPublic
) throws IOException {
Utils.walkJdkModules((moduleName, moduleClasses, moduleExports) -> {
var originalCallers = new ArrayList<CallChain>();
var visitor = new FindUsagesClassVisitor(
moduleExports,
methodToFind,
(source, line, className, methodName, methodDescriptor, access) -> originalCallers.add(
new CallChain(
new FindUsagesClassVisitor.EntryPoint(moduleName, source, line, className, methodName, methodDescriptor, access),
new CallChain(
new FindUsagesClassVisitor.EntryPoint(
methodToFindModule,
"",
0,
methodToFind.className(),
methodToFind.methodName(),
methodToFind.methodDescriptor(),
methodToFindAccess
),
null
)
)
)
);
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 -> {
var originalCaller = c.next();
printRow(getEntryPointString(c.entryPoint().moduleName(), c.entryPoint()), getOriginalEntryPointString(originalCaller));
});
var firstLevelCallers = bubbleUpFromPublic ? originalCallers : originalCallers.stream().filter(Main::isNotFullyPublic).toList();
if (firstLevelCallers.isEmpty() == false) {
findTransitiveUsages(
firstLevelCallers,
moduleClasses,
moduleExports,
bubbleUpFromPublic,
(originalEntryPoint, newMethod) -> printRow(
getEntryPointString(moduleName, newMethod.entryPoint()),
getOriginalEntryPointString(originalEntryPoint)
)
);
}
});
}
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(String entryPointString, String originalEntryPoint) {
System.out.println(entryPointString + SEPARATOR + originalEntryPoint);
}
private static String getEntryPointString(String moduleName, FindUsagesClassVisitor.EntryPoint e) {
return moduleName + SEPARATOR + e.source() + SEPARATOR + e.line() + SEPARATOR + e.className() + SEPARATOR + e.methodName()
+ SEPARATOR + e.methodDescriptor() + SEPARATOR + ExternalAccess.toString(e.access());
}
private static String getOriginalEntryPointString(CallChain originalCallChain) {
return originalCallChain.entryPoint().moduleName() + SEPARATOR + originalCallChain.entryPoint().className() + SEPARATOR
+ originalCallChain.entryPoint().methodName() + SEPARATOR + ExternalAccess.toString(originalCallChain.entryPoint().access());
}
interface MethodDescriptorConsumer {
void accept(FindUsagesClassVisitor.MethodDescriptor methodDescriptor, String moduleName, EnumSet<ExternalAccess> access)
throws IOException;
}
private static void parseCsv(Path csvPath, MethodDescriptorConsumer methodConsumer) throws IOException {
var lines = Files.readAllLines(csvPath);
for (var l : lines) {
var tokens = l.split(SEPARATOR);
var moduleName = tokens[0];
var className = tokens[3];
var methodName = tokens[4];
var methodDescriptor = tokens[5];
var access = ExternalAccess.fromString(tokens[6]);
methodConsumer.accept(new FindUsagesClassVisitor.MethodDescriptor(className, methodName, methodDescriptor), moduleName, access);
}
}
public static void main(String[] args) throws IOException {
var csvFilePath = Path.of(args[0]);
boolean bubbleUpFromPublic = args.length >= 2 && Boolean.parseBoolean(args[1]);
parseCsv(csvFilePath, (method, module, access) -> identifyTopLevelEntryPoints(method, module, access, bubbleUpFromPublic));
}
}

View file

@ -10,39 +10,28 @@
package org.elasticsearch.entitlement.tools.securitymanager.scanner; package org.elasticsearch.entitlement.tools.securitymanager.scanner;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.tools.ExternalAccess;
import org.elasticsearch.entitlement.tools.Utils; import org.elasticsearch.entitlement.tools.Utils;
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassReader;
import java.io.IOException; import java.io.IOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files; import java.nio.file.Files;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Set;
public class Main { public class Main {
static final Set<String> excludedModules = Set.of("java.desktop");
private static void identifySMChecksEntryPoints() throws IOException { private static void identifySMChecksEntryPoints() throws IOException {
FileSystem fs = FileSystems.getFileSystem(URI.create("jrt:/"));
var moduleExports = Utils.findModuleExports(fs);
var callers = new HashMap<String, List<SecurityCheckClassVisitor.CallerInfo>>(); var callers = new HashMap<String, List<SecurityCheckClassVisitor.CallerInfo>>();
var visitor = new SecurityCheckClassVisitor(callers); var visitor = new SecurityCheckClassVisitor(callers);
try (var stream = Files.walk(fs.getPath("modules"))) { Utils.walkJdkModules((moduleName, moduleClasses, moduleExports) -> {
stream.filter(x -> x.toString().endsWith(".class")).forEach(x -> { for (var classFile : moduleClasses) {
var moduleName = x.subpath(1, 2).toString();
if (excludedModules.contains(moduleName) == false) {
try { try {
ClassReader cr = new ClassReader(Files.newInputStream(x)); ClassReader cr = new ClassReader(Files.newInputStream(classFile));
visitor.setCurrentModule(moduleName, moduleExports.get(moduleName)); visitor.setCurrentModule(moduleName, moduleExports);
var path = x.getNameCount() > 3 ? x.subpath(2, x.getNameCount() - 1).toString() : ""; var path = classFile.getNameCount() > 3 ? classFile.subpath(2, classFile.getNameCount() - 1).toString() : "";
visitor.setCurrentSourcePath(path); visitor.setCurrentSourcePath(path);
cr.accept(visitor, 0); cr.accept(visitor, 0);
} catch (IOException e) { } catch (IOException e) {
@ -50,7 +39,6 @@ public class Main {
} }
} }
}); });
}
printToStdout(callers); printToStdout(callers);
} }
@ -68,16 +56,8 @@ public class Main {
private static String toString(String calleeName, SecurityCheckClassVisitor.CallerInfo callerInfo) { private static String toString(String calleeName, SecurityCheckClassVisitor.CallerInfo callerInfo) {
var s = callerInfo.moduleName() + SEPARATOR + callerInfo.source() + SEPARATOR + callerInfo.line() + SEPARATOR + callerInfo var s = callerInfo.moduleName() + SEPARATOR + callerInfo.source() + SEPARATOR + callerInfo.line() + SEPARATOR + callerInfo
.className() + SEPARATOR + callerInfo.methodName() + SEPARATOR + callerInfo.methodDescriptor() + SEPARATOR; .className() + SEPARATOR + callerInfo.methodName() + SEPARATOR + callerInfo.methodDescriptor() + SEPARATOR + ExternalAccess
.toString(callerInfo.externalAccess());
if (callerInfo.externalAccess().contains(SecurityCheckClassVisitor.ExternalAccess.METHOD)
&& callerInfo.externalAccess().contains(SecurityCheckClassVisitor.ExternalAccess.CLASS)) {
s += "PUBLIC";
} else if (callerInfo.externalAccess().contains(SecurityCheckClassVisitor.ExternalAccess.METHOD)) {
s += "PUBLIC-METHOD";
} else {
s += "PRIVATE";
}
if (callerInfo.runtimePermissionType() != null) { if (callerInfo.runtimePermissionType() != null) {
s += SEPARATOR + callerInfo.runtimePermissionType(); s += SEPARATOR + callerInfo.runtimePermissionType();

View file

@ -10,6 +10,7 @@
package org.elasticsearch.entitlement.tools.securitymanager.scanner; package org.elasticsearch.entitlement.tools.securitymanager.scanner;
import org.elasticsearch.core.SuppressForbidden; import org.elasticsearch.core.SuppressForbidden;
import org.elasticsearch.entitlement.tools.ExternalAccess;
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label; import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.MethodVisitor;
@ -27,6 +28,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC; import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ASM9; import static org.objectweb.asm.Opcodes.ASM9;
import static org.objectweb.asm.Opcodes.GETSTATIC; import static org.objectweb.asm.Opcodes.GETSTATIC;
@ -42,11 +44,6 @@ class SecurityCheckClassVisitor extends ClassVisitor {
static final String SECURITY_MANAGER_INTERNAL_NAME = "java/lang/SecurityManager"; static final String SECURITY_MANAGER_INTERNAL_NAME = "java/lang/SecurityManager";
static final Set<String> excludedClasses = Set.of(SECURITY_MANAGER_INTERNAL_NAME); static final Set<String> excludedClasses = Set.of(SECURITY_MANAGER_INTERNAL_NAME);
enum ExternalAccess {
CLASS,
METHOD
}
record CallerInfo( record CallerInfo(
String moduleName, String moduleName,
String source, String source,
@ -208,15 +205,12 @@ class SecurityCheckClassVisitor extends ClassVisitor {
|| opcode == INVOKEDYNAMIC) { || opcode == INVOKEDYNAMIC) {
if (SECURITY_MANAGER_INTERNAL_NAME.equals(owner)) { if (SECURITY_MANAGER_INTERNAL_NAME.equals(owner)) {
EnumSet<ExternalAccess> externalAccesses = EnumSet.noneOf(ExternalAccess.class); EnumSet<ExternalAccess> externalAccesses = ExternalAccess.fromPermissions(
if (moduleExports.contains(getPackageName(className))) { moduleExports.contains(getPackageName(className)),
if ((methodAccess & ACC_PUBLIC) != 0) { (classAccess & ACC_PUBLIC) != 0,
externalAccesses.add(ExternalAccess.METHOD); (methodAccess & ACC_PUBLIC) != 0,
} (methodAccess & ACC_PROTECTED) != 0
if ((classAccess & ACC_PUBLIC) != 0) { );
externalAccesses.add(ExternalAccess.CLASS);
}
}
if (name.equals("checkPermission")) { if (name.equals("checkPermission")) {
var callers = callerInfoByMethod.computeIfAbsent(name, ignored -> new ArrayList<>()); var callers = callerInfoByMethod.computeIfAbsent(name, ignored -> new ArrayList<>());