Skip to content

Commit 5fe1395

Browse files
moscheKubik42
authored andcommitted
Extend public-callers-finder to check if methods are instrumented with entitlements. (elastic#136022)
If `--check-instrumentation` is given, the public-callers-finder will load the dump of instrumented methods and check that against the public callers the tool found. An additional column is added to the output, that is either COVERED if the method is already instrumented or MISSING if not. Relates to ES-11757
1 parent 40ee31b commit 5fe1395

File tree

9 files changed

+325
-81
lines changed

9 files changed

+325
-81
lines changed

libs/entitlement/build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ apply plugin: 'elasticsearch.build'
1313
apply plugin: 'elasticsearch.publish'
1414
apply plugin: 'elasticsearch.embedded-providers'
1515
apply plugin: 'elasticsearch.mrjar'
16+
apply plugin: 'elasticsearch.internal-test-artifact'
1617

1718
embeddedProviders {
1819
impl 'entitlement', project(':libs:entitlement:asm-provider')
@@ -27,6 +28,11 @@ dependencies {
2728
exclude group: 'org.elasticsearch', module: 'entitlement'
2829
}
2930

31+
testImplementation 'org.ow2.asm:asm:9.8'
32+
testImplementation 'org.ow2.asm:asm-util:9.8'
33+
testImplementation 'org.ow2.asm:asm-tree:9.8'
34+
testImplementation 'org.ow2.asm:asm-analysis:9.8'
35+
3036
// guarding for intellij
3137
if (sourceSets.findByName("main23")) {
3238
main23CompileOnly project(path: ':libs:entitlement:bridge', configuration: 'java23')
@@ -36,3 +42,9 @@ dependencies {
3642
tasks.withType(CheckForbiddenApisTask).configureEach {
3743
replaceSignatureFiles 'jdk-signatures'
3844
}
45+
46+
// invoke using ./gradlew :libs:entitlement:dumpInstrumentedMethods --args="<output file>"
47+
tasks.register('dumpInstrumentedMethods', JavaExec) {
48+
classpath = sourceSets.test.runtimeClasspath
49+
mainClass = 'org.elasticsearch.entitlement.initialization.DynamicInstrumentationUtils'
50+
}

libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ static void initialize(Instrumentation inst, Class<?> checkerInterface, boolean
123123
}
124124
}
125125

126-
private static Map<MethodKey, CheckMethod> getMethodsToInstrument(Class<?> checkerInterface) throws ClassNotFoundException,
126+
static Map<MethodKey, CheckMethod> getMethodsToInstrument(Class<?> checkerInterface) throws ClassNotFoundException,
127127
NoSuchMethodException {
128128
Map<MethodKey, CheckMethod> checkMethods = new HashMap<>(INSTRUMENTATION_SERVICE.lookupMethods(checkerInterface));
129129
Stream.of(fileSystemProviderChecks(), fileStoreChecks(), pathChecks(), selectorProviderChecks())
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.initialization;
11+
12+
import org.elasticsearch.entitlement.initialization.DynamicInstrumentationUtils.Descriptor;
13+
import org.elasticsearch.test.ESTestCase;
14+
import org.hamcrest.Description;
15+
import org.hamcrest.Matcher;
16+
import org.hamcrest.TypeSafeMatcher;
17+
18+
import java.util.List;
19+
20+
import static org.elasticsearch.test.LambdaMatchers.transformedMatch;
21+
import static org.hamcrest.Matchers.anyOf;
22+
import static org.hamcrest.Matchers.everyItem;
23+
import static org.hamcrest.Matchers.is;
24+
import static org.hamcrest.Matchers.startsWith;
25+
26+
public class DynamicInstrumentationTests extends ESTestCase {
27+
28+
public void testInstrumentedMethodsExist() throws Exception {
29+
List<Descriptor> descriptors = DynamicInstrumentationUtils.loadInstrumentedMethodDescriptors();
30+
assertThat(
31+
descriptors,
32+
everyItem(
33+
anyOf(
34+
isKnownDescriptor(),
35+
// not visible
36+
transformedMatch(Descriptor::className, startsWith("jdk/vm/ci/services/")),
37+
// removed in JDK 24
38+
is(
39+
new Descriptor(
40+
"sun/net/www/protocol/http/HttpURLConnection",
41+
"openConnectionCheckRedirects",
42+
List.of("java/net/URLConnection"),
43+
null
44+
)
45+
)
46+
)
47+
)
48+
);
49+
}
50+
51+
static Matcher<Descriptor> isKnownDescriptor() {
52+
return new TypeSafeMatcher<>() {
53+
@Override
54+
protected boolean matchesSafely(Descriptor item) {
55+
return item.methodDescriptor() != null;
56+
}
57+
58+
@Override
59+
public void describeTo(Description description) {
60+
description.appendText("valid method descriptor");
61+
}
62+
63+
@Override
64+
protected void describeMismatchSafely(Descriptor item, Description mismatchDescription) {
65+
mismatchDescription.appendText("was ").appendValue(item.toString());
66+
}
67+
};
68+
}
69+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.entitlement.initialization;
11+
12+
import org.elasticsearch.common.logging.LogConfigurator;
13+
import org.elasticsearch.entitlement.bridge.EntitlementChecker;
14+
import org.elasticsearch.entitlement.instrumentation.CheckMethod;
15+
import org.elasticsearch.entitlement.instrumentation.MethodKey;
16+
import org.objectweb.asm.ClassReader;
17+
import org.objectweb.asm.ClassVisitor;
18+
import org.objectweb.asm.MethodVisitor;
19+
import org.objectweb.asm.Opcodes;
20+
import org.objectweb.asm.Type;
21+
22+
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
24+
import java.nio.file.Files;
25+
import java.nio.file.Path;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.stream.Stream;
29+
30+
import static java.util.Objects.requireNonNull;
31+
32+
public class DynamicInstrumentationUtils {
33+
34+
/**
35+
* This dumps the list of instrumented methods to a file specified by the first argument
36+
* or alternatively the system property `es.entitlements.dump`.
37+
*/
38+
public static void main(String[] args) throws Exception {
39+
LogConfigurator.loadLog4jPlugins();
40+
LogConfigurator.configureESLogging();
41+
42+
var path = requireNonNull(args.length > 0 ? args[0] : System.getProperty("es.entitlements.dump"), "destination for dump required");
43+
var descriptors = loadInstrumentedMethodDescriptors();
44+
Files.write(
45+
Path.of(path),
46+
() -> descriptors.stream().filter(d -> d.methodDescriptor != null).map(Descriptor::toLine).iterator(),
47+
StandardCharsets.UTF_8
48+
);
49+
}
50+
51+
static List<Descriptor> loadInstrumentedMethodDescriptors() throws Exception {
52+
Map<MethodKey, CheckMethod> methodsToInstrument = DynamicInstrumentation.getMethodsToInstrument(
53+
EntitlementCheckerUtils.getVersionSpecificCheckerClass(EntitlementChecker.class, Runtime.version().feature())
54+
);
55+
return methodsToInstrument.keySet().stream().map(DynamicInstrumentationUtils::lookupDescriptor).toList();
56+
}
57+
58+
private static Descriptor lookupDescriptor(MethodKey key) {
59+
final String[] foundDescriptor = { null };
60+
try {
61+
ClassReader reader = new ClassReader(key.className());
62+
reader.accept(new ClassVisitor(Opcodes.ASM9) {
63+
@Override
64+
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
65+
if (name.equals(key.methodName()) == false) {
66+
return null;
67+
}
68+
List<String> argTypes = Stream.of(Type.getArgumentTypes(descriptor)).map(Type::getInternalName).toList();
69+
if (argTypes.equals(key.parameterTypes())) {
70+
foundDescriptor[0] = descriptor;
71+
}
72+
return null;
73+
}
74+
}, 0);
75+
} catch (IOException e) {
76+
// nothing to do
77+
}
78+
return new Descriptor(key.className(), key.methodName(), key.parameterTypes(), foundDescriptor[0]);
79+
80+
}
81+
82+
record Descriptor(String className, String methodName, List<String> parameterTypes, String methodDescriptor) {
83+
84+
private static final String SEPARATOR = "\t";
85+
86+
CharSequence toLine() {
87+
return String.join(SEPARATOR, className, methodName, methodDescriptor);
88+
}
89+
90+
}
91+
}

libs/entitlement/tools/jdk-api-extractor/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,17 @@ cat libs/entitlement/tools/jdk-api-extractor/api.diff | grep '^+[^+]' | sed 's/^
3232

3333
# review new additions next for critical ones that should require entitlements
3434
# once done, remove all lines that are not considered critical and run the public-callers-finder to report
35-
# the transitive public surface for these additions
36-
./gradlew :libs:entitlement:tools:public-callers-finder:run -Druntime.java=25 --args="api-jdk25-additions.tsv true"
35+
# the transitive public surface for these additions to identify methods that are node already covered by entitlements
36+
./gradlew :libs:entitlement:tools:public-callers-finder:run -Druntime.java=25 --args="api-jdk25-additions.tsv --transitive --check-instrumentation"
3737
```
3838

3939
### Optional arguments:
4040

4141
- `--deprecations-only`: reports public deprecations (by means of `@Deprecated`)
4242
- `--include-incubator`: include incubator modules (e.g. `jdk.incubator.vector`)
4343

44+
If `-Druntime.java` is not provided, the bundled JDK is used.
45+
4446
```bash
4547
./gradlew :libs:entitlement:tools:jdk-api-extractor:run -Druntime.java=24 --args="deprecations-jdk24.tsv --deprecations-only"
4648
```

libs/entitlement/tools/public-callers-finder/README.md

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,30 @@ it treats calls to `super` in `S.m` as regular calls (e.g. `example() -> S.m() -
1515

1616
In order to run the tool, use:
1717
```shell
18-
./gradlew :libs:entitlement:tools:public-callers-finder:run -Druntime.java=25 --args="<input-file> [<bubble-up-from-public>]"
18+
./gradlew :libs:entitlement:tools:public-callers-finder:run [-Druntime.java=25] --args="<input-file> [--transitive] [--check-instrumentation]"
1919
```
20-
Where `input-file` is a CSV file (columns separated by `TAB`) that contains the following columns:
21-
1. Module name
22-
2. unused
23-
3. unused
24-
4. Fully qualified class name (ASM style, with `/` separators)
25-
5. Method name
26-
6. Method descriptor (ASM signature)
27-
7. Visibility (PUBLIC/PUBLIC-METHOD/PRIVATE)
2820

29-
And `bubble-up-from-public` is a boolean (`true|false`) indicating if the code should stop at the first public method (`false`: default) or continue to find usages recursively even after reaching the "public surface".
21+
- `input-file` is a `TAB`-separated TSV file containing the following columns:
22+
1. Module name
23+
2. unused
24+
3. unused
25+
4. Fully qualified class name (ASM style, with `/` separators)
26+
5. Method name
27+
6. Method descriptor (ASM signature)
28+
7. Visibility (PUBLIC/PUBLIC-METHOD/PRIVATE)
29+
30+
- optional: `--transitive` to not stop at the first public method, but continue to find the transitive public surface.
3031

31-
The output of the tool is another CSV file, with one line for each entry-point, columns separated by `TAB`
32+
- optional: `--check-instrumentation` to check if methods are instrumented for entitlements.
33+
34+
If `-Druntime.java` is not provided, the bundled JDK is used.
35+
36+
Examples:
37+
```bash
38+
./gradlew :libs:entitlement:tools:public-callers-finder:run --args="sensitive-methods.tsv true" --transitive --check-instrumentation"
39+
```
40+
41+
The tool writes the following `TAB`-separated columns to standard out:
3242
3343
1. Module name
3444
2. File name (from source root)
@@ -37,12 +47,13 @@ The output of the tool is another CSV file, with one line for each entry-point,
3747
5. Method name
3848
6. Method descriptor (ASM signature)
3949
7. Visibility (PUBLIC/PUBLIC-METHOD/PRIVATE)
40-
8. Original caller Module name
41-
9. Original caller Class name (ASM style, with `/` separators)
42-
10. Original caller Method name
43-
11. Original caller Visibility
50+
8. If using `--check-instrumentation`: `COVERED` if method is instrumented with entitlement checks, `MISSING` otherwise
51+
9. Original caller Module name
52+
10. Original caller Class name (ASM style, with `/` separators)
53+
11. Original caller Method name
54+
12. Original caller Visibility
4455
45-
Examples:
56+
Example output:
4657
```
4758
java.base DeleteOnExitHook.java 50 java/io/DeleteOnExitHook$1 run ()V PUBLIC java.base java/io/File delete PUBLIC
4859
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

libs/entitlement/tools/public-callers-finder/build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,24 @@ application {
4242
]
4343
}
4444

45+
configurations {
46+
dumpMethods
47+
}
48+
49+
def instrumentedMethodsDump = project.layout.buildDirectory.file('instrumented-methods.dump').get().asFile
50+
4551
tasks.named("run").configure {
52+
dependsOn 'dumpInstrumentedMethods'
53+
systemProperty 'es.entitlements.dump', instrumentedMethodsDump
4654
executable = "${buildParams.runtimeJavaHome.get()}/bin/java" + (OS.current() == OS.WINDOWS ? '.exe' : '')
4755
}
4856

57+
tasks.register('dumpInstrumentedMethods', JavaExec) {
58+
classpath = configurations.dumpMethods
59+
mainClass = 'org.elasticsearch.entitlement.initialization.DynamicInstrumentationUtils'
60+
systemProperty 'es.entitlements.dump', instrumentedMethodsDump
61+
}
62+
4963
repositories {
5064
mavenCentral()
5165
}
@@ -55,6 +69,8 @@ dependencies {
5569
implementation 'org.ow2.asm:asm:9.8'
5670
implementation 'org.ow2.asm:asm-util:9.8'
5771
implementation(project(':libs:entitlement:tools:common'))
72+
73+
dumpMethods testArtifact(project(':libs:entitlement'))
5874
}
5975

6076
tasks.named('forbiddenApisMain').configure {

libs/entitlement/tools/public-callers-finder/src/main/java/org/elasticsearch/entitlement/tools/publiccallersfinder/FindUsagesClassVisitor.java

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,10 @@ class FindUsagesClassVisitor extends ClassVisitor {
3131

3232
record MethodDescriptor(String className, String methodName, String methodDescriptor) {}
3333

34-
record EntryPoint(
35-
String moduleName,
36-
String source,
37-
int line,
38-
String className,
39-
String methodName,
40-
String methodDescriptor,
41-
EnumSet<ExternalAccess> access
42-
) {}
34+
record EntryPoint(String moduleName, String source, int line, MethodDescriptor method, EnumSet<ExternalAccess> access) {}
4335

4436
interface CallerConsumer {
45-
void accept(String source, int line, String className, String methodName, String methodDescriptor, EnumSet<ExternalAccess> access);
37+
void accept(String source, int line, MethodDescriptor method, EnumSet<ExternalAccess> access);
4638
}
4739

4840
private final Set<String> moduleExports;
@@ -126,7 +118,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String descri
126118
(methodAccess & ACC_PUBLIC) != 0,
127119
(methodAccess & ACC_PROTECTED) != 0
128120
);
129-
callers.accept(source, line, className, methodName, methodDescriptor, externalAccess);
121+
callers.accept(source, line, new MethodDescriptor(className, methodName, methodDescriptor), externalAccess);
130122
}
131123
}
132124
}

0 commit comments

Comments
 (0)