-
Notifications
You must be signed in to change notification settings - Fork 5.7k
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
Changes from 3 commits
351e8f4
719cd3e
6729e12
83fbbc8
415b9b4
9debed5
3d0f4ff
1f23bec
7531385
01bcd0f
b9bd20f
c666f6a
4b560dc
8a2ebae
9e8dad0
861965b
a1ef03a
b546844
4c6abdd
06f53e3
75c9a6a
a472349
fda0568
bb75a30
a046f6f
40ca91e
c597f24
5afb356
1d1ff01
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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); | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
This file was deleted.
There was a problem hiding this comment.
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)