Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

8317611: Add a tool like jdeprscan to find usage of restricted methods #19774

Closed
wants to merge 29 commits into from
Closed
Changes from 3 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
351e8f4
jnativescan tool
JornVernee May 29, 2024
719cd3e
remove references to --enable-native-access
JornVernee Jun 17, 2024
6729e12
jscan -> jnativescan
JornVernee Jun 17, 2024
83fbbc8
small format fix
JornVernee Jun 17, 2024
415b9b4
Handle array types + handle references through subclasses + implement
JornVernee Jun 17, 2024
9debed5
add test for array method refs
JornVernee Jun 18, 2024
3d0f4ff
add test for references through subclasses
JornVernee Jun 18, 2024
1f23bec
add more testing + cleanup code
JornVernee Jun 18, 2024
7531385
add missing newlinw
JornVernee Jun 18, 2024
01bcd0f
correct comment
JornVernee Jun 18, 2024
b9bd20f
correct one more comment
JornVernee Jun 18, 2024
c666f6a
add --print-native-access test
JornVernee Jun 18, 2024
4b560dc
add jnativescan to help flag test
JornVernee Jun 18, 2024
8a2ebae
use URI-based constructor of Path
JornVernee Jun 18, 2024
9e8dad0
Merge branch 'master' into jnativescan
JornVernee Jun 19, 2024
861965b
Update src/jdk.jdeps/share/classes/com/sun/tools/jnativescan/Main.java
JornVernee Jun 19, 2024
a1ef03a
add man page
JornVernee Jun 19, 2024
b546844
review comments
JornVernee Jun 19, 2024
4c6abdd
man page review comments
JornVernee Jun 20, 2024
06f53e3
update man page header to be consisten with the others
JornVernee Jun 20, 2024
75c9a6a
review comments Alan
JornVernee Jun 20, 2024
a472349
add extra test for missing root modules
JornVernee Jun 20, 2024
fda0568
Jan comments
JornVernee Jun 21, 2024
bb75a30
sort output for easier diffs
JornVernee Jun 21, 2024
a046f6f
Add support for module directories + class path directories
JornVernee Jun 21, 2024
40ca91e
de-dupe on path, not module name
JornVernee Jun 24, 2024
c597f24
use instance resolveAndBind + use junit in tests
JornVernee Jun 28, 2024
5afb356
ofInvokeInstruction
JornVernee Jul 1, 2024
1d1ff01
Merge branch 'master' into jnativescan
JornVernee Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -84,7 +84,7 @@ public Iterable<String> getSupportedPlatformNames() {

@Override
public PlatformDescription getPlatform(String platformName, String options) throws PlatformNotSupported {
if (Source.lookup(platformName) == null) {
if (!SUPPORTED_JAVA_PLATFORM_VERSIONS.contains(platformName)) {
throw new PlatformNotSupported();
}
return getPlatformTrusted(platformName);
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package com.sun.tools.jnativescan;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarFile;
import java.util.stream.Stream;
import java.util.zip.ZipFile;

sealed interface ClassFileSource {
String moduleName();
Path path();

Stream<byte[]> classFiles(Runtime.Version version) throws IOException;

record Module(ModuleReference reference) implements ClassFileSource {
@Override
public String moduleName() {
return reference.descriptor().name();
}

@Override
public Path path() {
URI location = reference.location().orElseThrow();
return Path.of(location);
}

@Override
public Stream<byte[]> classFiles(Runtime.Version version) throws IOException {
ModuleReader reader = reference().open();
return reader.list()
.filter(resourceName -> resourceName.endsWith(".class"))
.map(resourceName -> {
try (InputStream stream = reader.open(resourceName).orElseThrow()) {
return stream.readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}).onClose(() -> {
try {
reader.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}

record ClassPathJar(Path path) implements ClassFileSource {
@Override
public String moduleName() {
return "ALL-UNNAMED";
}

@Override
public Stream<byte[]> classFiles(Runtime.Version version) throws IOException {
JarFile jf = new JarFile(path().toFile(), false, ZipFile.OPEN_READ, version);
return jf.versionedStream()
.filter(je -> je.getName().endsWith(".class"))
.map(je -> {
try (InputStream stream = jf.getInputStream(je)){
return stream.readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}).onClose(() -> {
try {
jf.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}

record ClassPathDirectory(Path path) implements ClassFileSource {
@Override
public String moduleName() {
return "ALL-UNNAMED";
}

@Override
public Stream<byte[]> classFiles(Runtime.Version version) throws IOException {
return Files.walk(path)
.filter(file -> Files.isRegularFile(file) && file.toString().endsWith(".class"))
.map(file -> {
try (InputStream stream = Files.newInputStream(file)){
return stream.readAllBytes();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -35,31 +35,24 @@
import java.lang.classfile.ClassModel;
import java.lang.constant.ClassDesc;
import java.lang.module.ModuleDescriptor;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BiConsumer;
import java.util.jar.JarFile;
import java.util.zip.ZipFile;
import java.util.stream.Stream;

abstract class ClassResolver implements AutoCloseable {

static ClassResolver forScannedModules(List<ScannedModule> modules, Runtime.Version version) throws IOException {
List<JarFile> loaded = new ArrayList<>();
static ClassResolver forClassFileSources(List<ClassFileSource> sources, Runtime.Version version) throws IOException {
Map<ClassDesc, Info> classMap = new HashMap<>();
for (ScannedModule m : modules) {
JarFile jf = new JarFile(m.path().toFile(), false, ZipFile.OPEN_READ, version);
loaded.add(jf);
jf.versionedStream().filter(je -> je.getName().endsWith(".class")).forEach(je -> {
try {
ClassModel model = ClassFile.of().parse(jf.getInputStream(je).readAllBytes());
ClassDesc desc = model.thisClass().asSymbol();
classMap.put(desc, new Info(m.moduleName(), m.path(), model));
} catch (IOException e) {
throw new RuntimeException(e);
}
});
for (ClassFileSource source : sources) {
try (Stream<byte[]> classFiles = source.classFiles(version)) {
classFiles.forEach(bytes -> {
ClassModel model = ClassFile.of().parse(bytes);
ClassDesc desc = model.thisClass().asSymbol();
classMap.put(desc, new Info(source, model));
});
}
}
return new ScannedModuleClassResolver(loaded, classMap);
return new SimpleClassResolver(classMap);
}

static ClassResolver forSystemModules(Runtime.Version version) {
@@ -75,21 +68,19 @@ static ClassResolver forSystemModules(Runtime.Version version) {
return new SystemModuleClassResolver(fm);
}

record Info(String moduleName, Path jarPath, ClassModel model) {}
record Info(ClassFileSource source, ClassModel model) {}

public abstract void forEach(BiConsumer<ClassDesc, ClassResolver.Info> action);
public abstract Optional<ClassResolver.Info> lookup(ClassDesc desc);

@Override
public abstract void close() throws IOException;

private static class ScannedModuleClassResolver extends ClassResolver {
private static class SimpleClassResolver extends ClassResolver {

private final List<JarFile> jars;
private final Map<ClassDesc, ClassResolver.Info> classMap;

public ScannedModuleClassResolver(List<JarFile> jars, Map<ClassDesc, Info> classMap) {
this.jars = jars;
public SimpleClassResolver(Map<ClassDesc, Info> classMap) {
this.classMap = classMap;
}

@@ -102,11 +93,7 @@ public Optional<ClassResolver.Info> lookup(ClassDesc desc) {
}

@Override
public void close() throws IOException {
for (JarFile jarFile : jars) {
jarFile.close();
}
}
public void close() {}
}

private static class SystemModuleClassResolver extends ClassResolver {
@@ -159,7 +146,7 @@ public Optional<Info> lookup(ClassDesc desc) {
throw new JNativeScanFatalError("System class can not be found: " + qualName);
}
ClassModel model = ClassFile.of().parse(jfo.openInputStream().readAllBytes());
return new Info(moduleName, null, model);
return new Info(null, model);
} catch (IOException e) {
throw new RuntimeException(e);
}
Original file line number Diff line number Diff line change
@@ -30,14 +30,12 @@
import java.lang.module.Configuration;
import java.lang.module.ModuleFinder;
import java.lang.module.ResolvedModule;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipFile;

class JNativeScanTask {
@@ -60,8 +58,7 @@ public JNativeScanTask(PrintWriter out, List<Path> classPaths, List<Path> module
}

public void run() throws JNativeScanFatalError {
List<ScannedModule> modulesToScan = new ArrayList<>();
findAllClassPathJars().forEach(modulesToScan::add);
List<ClassFileSource> toScan = new ArrayList<>(findAllClassPathJars());

ModuleFinder moduleFinder = ModuleFinder.of(modulePaths.toArray(Path[]::new));
List<String> rootModules = cmdRootModules;
@@ -71,15 +68,12 @@ public void run() throws JNativeScanFatalError {
Configuration config = Configuration.resolveAndBind(moduleFinder, List.of(systemConfiguration()),
ModuleFinder.of(), rootModules);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the module path should work like other tools so the "moduleFinder" should be "after" finder, not the "before" finder. In other words, the modules that are observable on the application module path don't override the system modules. If you want you can use an instance method here, like this:

systemConfiguration().resovleAndBind(ModuleFinder.of(), moduleFinder, rootModules)

for (ResolvedModule m : config.modules()) {
URI location = m.reference().location().orElseThrow();
Path path = Path.of(location);
checkRegularJar(path);
modulesToScan.add(new ScannedModule(path, m.name()));
toScan.add(new ClassFileSource.Module(m.reference()));
}

Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> allRestrictedMethods;
try(ClassResolver classesToScan = ClassResolver.forScannedModules(modulesToScan, version);
ClassResolver systemClassResolver = ClassResolver.forSystemModules(version)) {
SortedMap<ClassFileSource, SortedMap<ClassDesc, List<RestrictedUse>>> allRestrictedMethods;
try(ClassResolver classesToScan = ClassResolver.forClassFileSources(toScan, version);
ClassResolver systemClassResolver = ClassResolver.forSystemModules(version)) {
NativeMethodFinder finder = NativeMethodFinder.create(classesToScan, systemClassResolver);
allRestrictedMethods = finder.findAll();
} catch (IOException e) {
@@ -92,30 +86,40 @@ public void run() throws JNativeScanFatalError {
}
}

// recursively look for all class path jars, starting at the root jars
// in this.classPaths, and recursively following all Class-Path manifest
// attributes
private Stream<ScannedModule> findAllClassPathJars() throws JNativeScanFatalError {
Stream.Builder<ScannedModule> builder = Stream.builder();
Deque<Path> classPathJars = new ArrayDeque<>(classPaths);
while (!classPathJars.isEmpty()) {
Path jar = classPathJars.poll();
checkRegularJar(jar);
String[] classPathAttribute = classPathAttribute(jar);
Path parentDir = jar.getParent();
for (String classPathEntry : classPathAttribute) {
Path otherJar = parentDir != null
? parentDir.resolve(classPathEntry)
: Path.of(classPathEntry);
if (Files.exists(otherJar)) {
// Class-Path attribute specifies that jars that
// are not found are simply ignored. Do the same here
classPathJars.offer(otherJar);
private List<ClassFileSource> findAllClassPathJars() throws JNativeScanFatalError {
List<ClassFileSource> result = new ArrayList<>();
for (Path path : classPaths) {
if (isJarFile(path)) {
Deque<Path> jarsToScan = new ArrayDeque<>();
jarsToScan.offer(path);

// recursively look for all class path jars, starting at the root jars
// in this.classPaths, and recursively following all Class-Path manifest
// attributes
while (!jarsToScan.isEmpty()) {
Path jar = jarsToScan.poll();
String[] classPathAttribute = classPathAttribute(jar);
Path parentDir = jar.getParent();
for (String classPathEntry : classPathAttribute) {
Path otherJar = parentDir != null
? parentDir.resolve(classPathEntry)
Copy link
Contributor

@AlanBateman AlanBateman Jun 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'lll need to create a follow on issue to re-examine this as the value of a Class-Path attribute are a sequence of relative URIs (with an optional "file" URI scheme) rather than file paths. Treating it as a file path may work in some cases but won't work once you encounter cases that use escaping.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, it seems that I didn't read the spec careful enough, and filtering valid URLs in the Class-Path attribute is actually a bit tricky. Let's handle this in a follow up.

: Path.of(classPathEntry);
if (Files.exists(otherJar)) {
// Class-Path attribute specifies that jars that
// are not found are simply ignored. Do the same here
jarsToScan.offer(otherJar);
}
}
result.add(new ClassFileSource.ClassPathJar(jar));
}
} else if (Files.isDirectory(path)) {
result.add(new ClassFileSource.ClassPathDirectory(path));
} else {
throw new JNativeScanFatalError(
"Path does not appear to be a jar file, or directory containing classes: " + path);
}
builder.add(new ScannedModule(jar, "ALL-UNNAMED"));
}
return builder.build();
return result;
}

private String[] classPathAttribute(Path jar) {
@@ -144,15 +148,15 @@ private List<String> allModuleNames(ModuleFinder finder) {
return finder.findAll().stream().map(mr -> mr.descriptor().name()).toList();
}

private void printNativeAccess(Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> allRestrictedMethods) {
private void printNativeAccess(SortedMap<ClassFileSource, SortedMap<ClassDesc, List<RestrictedUse>>> allRestrictedMethods) {
String nativeAccess = allRestrictedMethods.keySet().stream()
.map(ScannedModule::moduleName)
.map(ClassFileSource::moduleName)
.distinct()
.collect(Collectors.joining(","));
out.println(nativeAccess);
}

private void dumpAll(Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> allRestrictedMethods) {
private void dumpAll(SortedMap<ClassFileSource, SortedMap<ClassDesc, List<RestrictedUse>>> allRestrictedMethods) {
if (allRestrictedMethods.isEmpty()) {
out.println(" <no restricted methods>");
} else {
@@ -175,10 +179,8 @@ private void dumpAll(Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> all
}
}

private static void checkRegularJar(Path path) throws JNativeScanFatalError {
if (!(Files.exists(path) && Files.isRegularFile(path) && path.toString().endsWith(".jar"))) {
throw new JNativeScanFatalError("File does not exist, or does not appear to be a regular jar file: " + path);
}
private static boolean isJarFile(Path path) throws JNativeScanFatalError {
return Files.exists(path) && Files.isRegularFile(path) && path.toString().endsWith(".jar");
}

public enum Action {
Original file line number Diff line number Diff line change
@@ -31,9 +31,6 @@
import java.lang.classfile.Attributes;
import java.lang.classfile.ClassModel;
import java.lang.classfile.MethodModel;
import java.lang.classfile.constantpool.InterfaceMethodRefEntry;
import java.lang.classfile.constantpool.MemberRefEntry;
import java.lang.classfile.constantpool.MethodRefEntry;
import java.lang.classfile.instruction.InvokeInstruction;
import java.lang.constant.ClassDesc;
import java.lang.constant.MethodTypeDesc;
@@ -46,7 +43,7 @@ class NativeMethodFinder {
// see make/langtools/src/classes/build/tools/symbolgenerator/CreateSymbols.java
private static final String RESTRICTED_NAME = "Ljdk/internal/javac/Restricted+Annotation;";

private final Map<MethodRef, Boolean> CACHE = new HashMap<>();
private final Map<MethodRef, Boolean> cache = new HashMap<>();
private final ClassResolver classesToScan;
private final ClassResolver systemClassResolver;

@@ -59,16 +56,17 @@ public static NativeMethodFinder create(ClassResolver classesToScan, ClassResolv
return new NativeMethodFinder(classesToScan, systemClassResolver);
}

public Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> findAll() throws JNativeScanFatalError {
Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> restrictedMethods = new HashMap<>();
public SortedMap<ClassFileSource, SortedMap<ClassDesc, List<RestrictedUse>>> findAll() throws JNativeScanFatalError {
SortedMap<ClassFileSource, SortedMap<ClassDesc, List<RestrictedUse>>> restrictedMethods
= new TreeMap<>(Comparator.comparing(ClassFileSource::moduleName));
classesToScan.forEach((_, info) -> {
ClassModel classModel = info.model();
List<RestrictedUse> perClass = new ArrayList<>();
classModel.methods().forEach(methodModel -> {
if (methodModel.flags().has(AccessFlag.NATIVE)) {
perClass.add(new NativeMethodDecl(MethodRef.ofModel(methodModel)));
} else {
Set<MethodRef> perMethod = new HashSet<>();
SortedSet<MethodRef> perMethod = new TreeSet<>(Comparator.comparing(MethodRef::toString));
methodModel.code().ifPresent(code -> {
try {
code.forEach(e -> {
@@ -89,22 +87,21 @@ public Map<ScannedModule, Map<ClassDesc, List<RestrictedUse>>> findAll() throws
}
});
if (!perMethod.isEmpty()) {
perClass.add(new RestrictedMethodRefs(MethodRef.ofModel(methodModel),
Set.copyOf(perMethod)));
perClass.add(new RestrictedMethodRefs(MethodRef.ofModel(methodModel), perMethod));
}
}
});
if (!perClass.isEmpty()) {
ScannedModule scannedModule = new ScannedModule(info.jarPath(), info.moduleName());
restrictedMethods.computeIfAbsent(scannedModule, _ -> new HashMap<>())
restrictedMethods.computeIfAbsent(info.source(),
_ -> new TreeMap<>(Comparator.comparing(JNativeScanTask::qualName)))
.put(classModel.thisClass().asSymbol(), perClass);
}
});
return restrictedMethods;
}

private boolean isRestrictedMethod(MethodRef ref) throws JNativeScanFatalError {
return CACHE.computeIfAbsent(ref, methodRef -> {
return cache.computeIfAbsent(ref, methodRef -> {
if (methodRef.owner().isArray()) {
// no restricted methods in arrays atm, and we can't look them up since they have no class file
return false;
Original file line number Diff line number Diff line change
@@ -24,9 +24,9 @@
*/
package com.sun.tools.jnativescan;

import java.util.Set;
import java.util.SortedSet;

sealed interface RestrictedUse {
record RestrictedMethodRefs(MethodRef referent, Set<MethodRef> referees) implements RestrictedUse {}
record RestrictedMethodRefs(MethodRef referent, SortedSet<MethodRef> referees) implements RestrictedUse {}
record NativeMethodDecl(MethodRef decl) implements RestrictedUse {}
}

This file was deleted.

30 changes: 24 additions & 6 deletions test/langtools/tools/jnativescan/TestJNativeScan.java
Original file line number Diff line number Diff line change
@@ -155,25 +155,26 @@ public void testReleaseNotSupported() {
@Test
public void testFileDoesNotExist() {
assertFailure(jnativescan("--class-path", "non-existent.jar"))
.stderrShouldContain("File does not exist, or does not appear to be a regular jar file");
.stderrShouldContain("Path does not appear to be a jar file, or directory containing classes");
}

@Test
public void testModuleNotAJarFile() {
String modulePath = moduleRoot("org.myapp").toString() + File.pathSeparator + orgLib.toString();
assertFailure(jnativescan("--module-path", modulePath,
assertSuccess(jnativescan("--module-path", modulePath,
"--add-modules", "ALL-MODULE-PATH"))
.stderrShouldContain("File does not exist, or does not appear to be a regular jar file");
.stdoutShouldContain("lib.Lib")
.stdoutShouldContain("lib.Lib::m()void is a native method declaration")
.stdoutShouldContain("lib.Lib::doIt()void references restricted methods")
.stdoutShouldContain("java.lang.foreign.MemorySegment::reinterpret(long)MemorySegment");
}

@Test
public void testPrintNativeAccess() {
assertSuccess(jnativescan("--module-path", MODULE_PATH,
"-add-modules", "org.singlejar,org.myapp",
"--print-native-access"))
.stdoutShouldContain("org.singlejar")
.stdoutShouldContain("org.lib")
.stdoutShouldContain("org.service");
.stdoutShouldMatch("org.lib,org.service,org.singlejar");
}

@Test
@@ -212,4 +213,21 @@ public void testMissingRootModules() {
.stdoutShouldBeEmpty()
.stderrShouldContain("Missing required option(s) [add-modules]");
}

@Test
public void testClassPathDirectory() {
assertSuccess(jnativescan("--class-path", testClasses.toString()))
.stderrShouldBeEmpty()
.stdoutShouldContain("ALL-UNNAMED")
.stdoutShouldContain("UnnamedPackage")
.stdoutShouldContain("UnnamedPackage::m()void is a native method declaration")
.stdoutShouldContain("UnnamedPackage::main(String[])void references restricted methods")
.stdoutShouldContain("main.Main")
.stdoutShouldContain("main.Main::m()void is a native method declaration")
.stdoutShouldContain("main.Main::main(String[])void references restricted methods")
.stdoutShouldContain("lib.Lib")
.stdoutShouldContain("lib.Lib::m()void is a native method declaration")
.stdoutShouldContain("lib.Lib::doIt()void references restricted methods")
.stdoutShouldContain("java.lang.foreign.MemorySegment::reinterpret(long)MemorySegment");
}
}