Skip to content

Commit e9f5181

Browse files
committed
Merge pull request #95 from policeman-tools/features/internalRuntimeRewrite
Rename internalRuntimeForbidden and make heuristics reliable
2 parents 3a49369 + 3ca52e4 commit e9f5181

27 files changed

+319
-128
lines changed

build.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -462,15 +462,15 @@
462462
</target>
463463

464464
<target name="-check-myself" depends="compile,compile-tools,compile-test,-install-forbiddenapi-task">
465-
<forbiddenapis internalRuntimeForbidden="true" failOnUnsupportedJava="false">
465+
<forbiddenapis failOnUnsupportedJava="false">
466466
<classpath refid="path.all"/>
467467
<fileset dir="build/main"/>
468468
<fileset dir="build/tools"/>
469469
<fileset dir="build/test"/>
470470
<signatures>
471471
<bundled name="jdk-unsafe-${jdk.version}"/>
472472
<bundled name="jdk-deprecated-${jdk.version}"/>
473-
<bundled name="jdk-internal-${jdk.version}"/>
473+
<bundled name="jdk-non-portable"/>
474474
<bundled name="jdk-system-out"/>
475475
<bundled name="jdk-reflection"/>
476476
</signatures>

src/main/docs/ant-task.html

+2-1
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ <h2>Parameters</h2>
120120
<td>internalRuntimeForbidden</td>
121121
<td><code>boolean</code></td>
122122
<td><code>false</code></td>
123-
<td>Forbids calls to classes from the internal java runtime (like <code>sun.misc.Unsafe</code>).</td>
123+
<td>Forbids calls to non-portable runtime APIs (like <code>sun.misc.Unsafe</code>). <em>Please note:</em> This enables <code>"jdk-non-portable"</code> bundled signatures for backwards compatibility.<br>
124+
<strong>Deprecated.</strong> Use <a href="bundled-signatures.html">bundled signatures</a> <code>"jdk-non-portable"</code> or <code>"jdk-internal"</code> instead.</td>
124125
</tr>
125126

126127
<tr>

src/main/docs/bundled-signatures.html

+12-4
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,24 @@ <h1>Bundled Signatures Documentation</h1>
3434
<li><strong><tt>jdk-deprecated-*</tt>:</strong> This disallows all deprecated
3535
methods from the JDK (for Java <tt>*</tt> = 1.6, 1.7, 1.8; Maven / Gradle automatically add the compile Java version).</li>
3636

37+
<li><strong><tt>jdk-internal-*</tt>:</strong> Lists all internal packages of the JDK as of <code>Security.getProperty(&quot;package.access&quot;)</code>.
38+
Calling those methods will always trigger security manager and is completely forbidden from Java 9 on
39+
(for Java <tt>*</tt> = 1.6, 1.7, 1.8; Maven / Gradle automatically add the compile Java version, <em>since forbiddenapis v2.1</em>).</li>
40+
41+
<li><strong><tt>jdk-non-portable</tt>:</strong> Signatures of all non-portable (like <code>com.sun.management.HotSpotDiagnosticMXBean</code>)
42+
or internal runtime APIs (like <code>sun.misc.Unsafe</code>). This is a superset of <tt>jdk-internal</tt>.<br>
43+
<em>Internally this is implemented using heuristics:</em> Any reference to an API that is part of the Java runtime (<tt>rt.jar</tt>, extensions,
44+
Java 9 <tt>java.*</tt> / <tt>jdk.*</tt> core modules) and is <strong>not</strong> part of the Java SE specification packages
45+
(mainly <tt>java</tt>, <tt>javax</tt>, but also <tt>org.ietf.jgss</tt>, <tt>org.omg</tt>, <tt>org.w3c.dom</tt>, and <tt>org.xml.sax</tt>) is forbidden
46+
(any java version, no specific JDK version, <em>since forbiddenapis v2.1 / replaces deprecated and wrong-named task
47+
setting <tt>internalRuntimeForbidden</tt></em>).</li>
48+
3749
<li><strong><tt>jdk-system-out</tt>:</strong> On server-side applications or libraries used by other programs, printing to
3850
<tt>System.out</tt> or <tt>System.err</tt> is discouraged and should be avoided (any java version, no specific JDK version).</li>
3951

4052
<li><strong><tt>jdk-reflection</tt>:</strong> Reflection usage to work around access flags fails with SecurityManagers
4153
and likely will not work anymore on runtime classes in Java 9 (any java version, no specific JDK version, <em>since forbiddenapis v2.1</em>).</li>
4254

43-
<li><strong><tt>jdk-internal</tt>:</strong> Lists all internal packages of the JDK as of <code>Security.getProperty(&quot;package.access&quot;)</code>.
44-
Calling those methods will always trigger security manager and is completely forbidden from Java 9 on
45-
(for Java <tt>*</tt> = 1.6, 1.7, 1.8; Maven / Gradle automatically add the compile Java version, <em>since forbiddenapis v2.1</em>).</li>
46-
4755
<li><strong><tt>commons-io-unsafe-*</tt>:</strong> If your application uses the famous <i>Apache Common-IO</i> library,
4856
this adds signatures of all methods that depend on default charset
4957
(for versions <tt>*</tt> = 1.0, 1.1, 1.2, 1.3, 1.4, 2.0, 2.1, 2.2, 2.3, 2.4).</li>

src/main/java/de/thetaphi/forbiddenapis/AsmUtils.java

+52-9
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
* limitations under the License.
1717
*/
1818

19-
import java.util.Arrays;
19+
import java.net.URISyntaxException;
20+
import java.net.URL;
2021
import java.util.Locale;
2122
import java.util.regex.Pattern;
2223

@@ -26,24 +27,44 @@ public final class AsmUtils {
2627
private AsmUtils() {}
2728

2829
private static final String REGEX_META_CHARS = ".^$+{}[]|()\\";
29-
private static final Pattern INTERNAL_PACKAGE_PATTERN;
30-
static {
30+
31+
/** Package prefixes of documented Java API (extracted from Javadocs of Java 8). */
32+
private static final Pattern PORTABLE_RUNTIME_PACKAGE_PATTERN = makePkgPrefixPattern("java", "javax", "org.ietf.jgss", "org.omg", "org.w3c.dom", "org.xml.sax");
33+
34+
/** Pattern that matches all module names, which are shipped by default in Java.
35+
* (see: {@code http://openjdk.java.net/projects/jigsaw/spec/sotms/}):
36+
* The remaining platform modules will share the 'java.' name prefix and are likely to include,
37+
* e.g., java.sql for database connectivity, java.xml for XML processing, and java.logging for
38+
* logging. Modules that are not defined in the Java SE 9 Platform Specification but instead
39+
* specific to the JDK will, by convention, share the 'jdk.' name prefix.
40+
*/
41+
private static final Pattern RUNTIME_MODULES_PATTERN = makePkgPrefixPattern("java", "jdk");
42+
43+
private static Pattern makePkgPrefixPattern(String... prefixes) {
3144
final StringBuilder sb = new StringBuilder();
3245
boolean first = true;
33-
for (final String pkg : Arrays.asList("sun.", "oracle.", "com.sun.", "com.oracle.", "jdk.", "sunw.")) {
34-
sb.append(first ? '(' : '|').append(Pattern.quote(pkg));
46+
for (final String p : prefixes) {
47+
sb.append(first ? '(' : '|').append(Pattern.quote(p));
3548
first = false;
3649
}
37-
INTERNAL_PACKAGE_PATTERN = Pattern.compile(sb.append(").*").toString());
50+
sb.append(")").append(Pattern.quote(".")).append(".*");
51+
return Pattern.compile(sb.toString());
3852
}
3953

4054
private static boolean isRegexMeta(char c) {
4155
return REGEX_META_CHARS.indexOf(c) != -1;
4256
}
4357

44-
/** Returns true, if the given binary class name (dotted) is likely a internal class (like sun.misc.Unsafe) */
45-
public static boolean isInternalClass(String className) {
46-
return INTERNAL_PACKAGE_PATTERN.matcher(className).matches();
58+
/** Returns true, if the given binary class name (dotted) is part of the documented and portable Java APIs. */
59+
public static boolean isPortableRuntimeClass(String className) {
60+
return PORTABLE_RUNTIME_PACKAGE_PATTERN.matcher(className).matches();
61+
}
62+
63+
/** Returns true, if the given Java 9 module name is part of the runtime (no custom 3rd party module).
64+
* @param module the module name or {@code null}, if in unnamed module
65+
*/
66+
public static boolean isRuntimeModule(String module) {
67+
return module != null && RUNTIME_MODULES_PATTERN.matcher(module).matches();
4768
}
4869

4970
/** Converts a binary class name (dotted) to the JVM internal one (slashed). Only accepts valid class names, no arrays. */
@@ -103,5 +124,27 @@ public static Pattern glob2Pattern(String... globs) {
103124
}
104125
return Pattern.compile(regex.toString(), 0);
105126
}
127+
128+
/** Returns the module name from a {@code jrt:/} URL; returns null if no module given or wrong URL type. */
129+
public static String getModuleName(URL jrtUrl) {
130+
if (!"jrt".equalsIgnoreCase(jrtUrl.getProtocol())) {
131+
return null;
132+
}
133+
try {
134+
// use URI class to also decode path and remove escapes:
135+
String mod = jrtUrl.toURI().getPath();
136+
if (mod != null && mod.length() >= 1) {
137+
mod = mod.substring(1);
138+
int p = mod.indexOf('/');
139+
if (p >= 0) {
140+
mod = mod.substring(0, p);
141+
}
142+
return mod.isEmpty() ? null : mod;
143+
}
144+
return null;
145+
} catch (URISyntaxException use) {
146+
return null;
147+
}
148+
}
106149

107150
}

src/main/java/de/thetaphi/forbiddenapis/Checker.java

+55-32
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,9 @@
5454
/**
5555
* Forbidden APIs checker class.
5656
*/
57-
public final class Checker implements RelatedClassLookup {
57+
public final class Checker implements RelatedClassLookup, Constants {
5858

5959
public static enum Option {
60-
INTERNAL_RUNTIME_FORBIDDEN,
6160
FAIL_ON_MISSING_CLASSES,
6261
FAIL_ON_VIOLATION,
6362
FAIL_ON_UNRESOLVABLE_SIGNATURES
@@ -66,27 +65,28 @@ public static enum Option {
6665
public final boolean isSupportedJDK;
6766

6867
private final long start;
69-
68+
private final NavigableSet<String> runtimePaths;
69+
7070
final Logger logger;
7171

72-
final NavigableSet<String> runtimePaths;
73-
7472
final ClassLoader loader;
75-
final java.lang.reflect.Method method_Class_getModule, method_Module_getResourceAsStream;
73+
final java.lang.reflect.Method method_Class_getModule, method_Module_getResourceAsStream, method_Module_getName;
7674
final EnumSet<Option> options;
7775

7876
// key is the binary name (dotted):
7977
final Map<String,ClassSignature> classesToCheck = new HashMap<String,ClassSignature>();
8078
// key is the binary name (dotted):
8179
final Map<String,ClassSignature> classpathClassCache = new HashMap<String,ClassSignature>();
8280

81+
// if enabled, the bundled signature to enable heuristics for detection of non-portable runtime calls is used:
82+
private boolean forbidNonPortableRuntime = false;
8383
// key is the internal name (slashed), followed by \000 and the field name:
8484
final Map<String,String> forbiddenFields = new HashMap<String,String>();
8585
// key is the internal name (slashed), followed by \000 and the method signature:
8686
final Map<String,String> forbiddenMethods = new HashMap<String,String>();
8787
// key is the internal name (slashed):
8888
final Map<String,String> forbiddenClasses = new HashMap<String,String>();
89-
// key is pattern to binary class name:
89+
// set of patterns of forbidden classes:
9090
final Set<ClassPatternRule> forbiddenClassPatterns = new LinkedHashSet<ClassPatternRule>();
9191
// descriptors (not internal names) of all annotations that suppress:
9292
final Set<String> suppressAnnotations = new LinkedHashSet<String>();
@@ -129,19 +129,22 @@ public Checker(Logger logger, ClassLoader loader, EnumSet<Option> options) {
129129

130130
boolean isSupportedJDK = false;
131131

132-
// first try Java 9 mdoule system (Jigsaw)
132+
// first try Java 9 module system (Jigsaw)
133133
// Please note: This code is not guaranteed to work with final Java 9 version. This is just for testing!
134-
java.lang.reflect.Method method_Class_getModule, method_Module_getResourceAsStream;
134+
java.lang.reflect.Method method_Class_getModule, method_Module_getResourceAsStream, method_Module_getName;
135135
try {
136136
method_Class_getModule = Class.class.getMethod("getModule");
137137
method_Module_getResourceAsStream = method_Class_getModule
138138
.getReturnType().getMethod("getResourceAsStream", String.class);
139+
method_Module_getName = method_Class_getModule
140+
.getReturnType().getMethod("getName");
139141
isSupportedJDK = true;
140142
} catch (NoSuchMethodException e) {
141-
method_Class_getModule = method_Module_getResourceAsStream = null;
143+
method_Class_getModule = method_Module_getResourceAsStream = method_Module_getName = null;
142144
}
143145
this.method_Class_getModule = method_Class_getModule;
144146
this.method_Module_getResourceAsStream = method_Module_getResourceAsStream;
147+
this.method_Module_getName = method_Module_getName;
145148

146149
final NavigableSet<String> runtimePaths = new TreeSet<String>();
147150

@@ -222,17 +225,30 @@ public Checker(Logger logger, ClassLoader loader, EnumSet<Option> options) {
222225
* This code is not guaranteed to work with final Java 9 version.
223226
* This is just for testing!
224227
**/
225-
private InputStream getBytecodeFromJigsaw(String classname) {
226-
if (method_Class_getModule == null || method_Module_getResourceAsStream == null) {
228+
private ClassSignature loadClassFromJigsaw(String classname) throws IOException {
229+
if (method_Class_getModule == null || method_Module_getResourceAsStream == null || method_Module_getName == null) {
227230
return null; // not Java 9 JIGSAW
228231
}
232+
233+
final InputStream in;
234+
final String moduleName;
229235
try {
230236
final Class<?> clazz = Class.forName(classname, false, loader);
231237
final Object module = method_Class_getModule.invoke(clazz);
232-
return (InputStream) method_Module_getResourceAsStream.invoke(module, AsmUtils.getClassResourceName(classname));
238+
moduleName = (String) method_Module_getName.invoke(module);
239+
in = (InputStream) method_Module_getResourceAsStream.invoke(module, AsmUtils.getClassResourceName(classname));
240+
if (in == null) {
241+
return null;
242+
}
233243
} catch (Exception e) {
234244
return null; // not found
235245
}
246+
247+
try {
248+
return new ClassSignature(new ClassReader(in), AsmUtils.isRuntimeModule(moduleName), false);
249+
} finally {
250+
in.close();
251+
}
236252
}
237253

238254
private boolean isRuntimePath(URL url) throws IOException {
@@ -260,7 +276,7 @@ private boolean isRuntimeClass(URLConnection conn) throws IOException {
260276
// all 'jrt:' URLs refer to a module in the Java 9+ runtime (see http://openjdk.java.net/jeps/220)
261277
// This may still be different with module system. We support both variants for now.
262278
// Please note: This code is not guaranteed to work with final Java 9 version. This is just for testing!
263-
return true;
279+
return AsmUtils.isRuntimeModule(AsmUtils.getModuleName(url));
264280
}
265281
return false;
266282
}
@@ -287,14 +303,9 @@ private ClassSignature getClassFromClassLoader(final String clazz) throws ClassN
287303
}
288304
return c;
289305
} else {
290-
final InputStream in = getBytecodeFromJigsaw(clazz);
291-
if (in != null) {
292-
try {
293-
// we mark it as runtime class because it was derived from the module system:
294-
classpathClassCache.put(clazz, c = new ClassSignature(new ClassReader(in), true, false));
295-
} finally {
296-
in.close();
297-
}
306+
final ClassSignature jigsawCl = loadClassFromJigsaw(clazz);
307+
if (jigsawCl != null) {
308+
classpathClassCache.put(clazz, c = jigsawCl);
298309
return c;
299310
}
300311
}
@@ -336,7 +347,8 @@ public ClassSignature lookupRelatedClass(String internalName) {
336347

337348
/** Adds the method signature to the list of disallowed methods. The Signature is checked against the given ClassLoader. */
338349
private void addSignature(final String line, final String defaultMessage, final UnresolvableReporting report) throws ParseException,IOException {
339-
final String clazz, field, signature, message;
350+
final String clazz, field, signature;
351+
String message = null;
340352
final Method method;
341353
int p = line.indexOf('@');
342354
if (p >= 0) {
@@ -371,15 +383,17 @@ private void addSignature(final String line, final String defaultMessage, final
371383
method = null;
372384
field = null;
373385
}
386+
if (message != null && message.isEmpty()) {
387+
message = null;
388+
}
374389
// create printout message:
375-
final String printout = (message != null && message.length() > 0) ?
376-
(signature + " [" + message + "]") : signature;
390+
final String printout = (message != null) ? (signature + " [" + message + "]") : signature;
377391
// check class & method/field signature, if it is really existent (in classpath), but we don't really load the class into JVM:
378392
if (AsmUtils.isGlob(clazz)) {
379393
if (method != null || field != null) {
380394
throw new ParseException(String.format(Locale.ENGLISH, "Class level glob pattern cannot be combined with methods/fields: %s", signature));
381395
}
382-
forbiddenClassPatterns.add(new ClassPatternRule(clazz, printout));
396+
forbiddenClassPatterns.add(new ClassPatternRule(clazz, message));
383397
} else {
384398
final ClassSignature c;
385399
try {
@@ -419,14 +433,19 @@ private void addSignature(final String line, final String defaultMessage, final
419433
}
420434

421435
/** Reads a list of bundled API signatures from classpath. */
422-
public void parseBundledSignatures(String name, String jdkTargetVersion) throws IOException,ParseException {
423-
parseBundledSignatures(name, jdkTargetVersion, true);
436+
public void addBundledSignatures(String name, String jdkTargetVersion) throws IOException,ParseException {
437+
addBundledSignatures(name, jdkTargetVersion, true);
424438
}
425439

426-
private void parseBundledSignatures(String name, String jdkTargetVersion, boolean logging) throws IOException,ParseException {
440+
private void addBundledSignatures(String name, String jdkTargetVersion, boolean logging) throws IOException,ParseException {
427441
if (!name.matches("[A-Za-z0-9\\-\\.]+")) {
428442
throw new ParseException("Invalid bundled signature reference: " + name);
429443
}
444+
if (BS_JDK_NONPORTABLE.equals(name)) {
445+
if (logging) logger.info("Reading bundled API signatures: " + name);
446+
forbidNonPortableRuntime = true;
447+
return;
448+
}
430449
// use Checker.class hardcoded (not getClass) so we have a fixed package name:
431450
InputStream in = Checker.class.getResourceAsStream("signatures/" + name + ".txt");
432451
// automatically expand the compiler version in here (for jdk-* signatures without version):
@@ -487,7 +506,7 @@ private void parseSignaturesFile(Reader reader, boolean isBundled) throws IOExce
487506
if (line.startsWith("@")) {
488507
if (isBundled && line.startsWith(BUNDLED_PREFIX)) {
489508
final String name = line.substring(BUNDLED_PREFIX.length()).trim();
490-
parseBundledSignatures(name, null, false);
509+
addBundledSignatures(name, null, false);
491510
} else if (line.startsWith(DEFAULT_MESSAGE_PREFIX)) {
492511
defaultMessage = line.substring(DEFAULT_MESSAGE_PREFIX.length()).trim();
493512
if (defaultMessage.length() == 0) defaultMessage = null;
@@ -549,7 +568,11 @@ public void addClassesToCheck(File basedir, String... relativeNames) throws IOEx
549568
}
550569

551570
public boolean hasNoSignatures() {
552-
return forbiddenMethods.isEmpty() && forbiddenFields.isEmpty() && forbiddenClasses.isEmpty() && forbiddenClassPatterns.isEmpty() && (!options.contains(Option.INTERNAL_RUNTIME_FORBIDDEN));
571+
return 0 == forbiddenMethods.size() +
572+
forbiddenFields.size() +
573+
forbiddenClasses.size() +
574+
forbiddenClassPatterns.size() +
575+
(forbidNonPortableRuntime ? 1 : 0);
553576
}
554577

555578
/** Adds the given annotation class for suppressing errors. */
@@ -565,7 +588,7 @@ public void addSuppressAnnotation(String annoName) {
565588
/** Parses a class and checks for valid method invocations */
566589
private int checkClass(final ClassReader reader, Pattern suppressAnnotationsPattern) {
567590
final String className = Type.getObjectType(reader.getClassName()).getClassName();
568-
final ClassScanner scanner = new ClassScanner(this, forbiddenClasses, forbiddenClassPatterns, forbiddenMethods, forbiddenFields, suppressAnnotationsPattern, options.contains(Option.INTERNAL_RUNTIME_FORBIDDEN));
591+
final ClassScanner scanner = new ClassScanner(this, forbiddenClasses, forbiddenClassPatterns, forbiddenMethods, forbiddenFields, suppressAnnotationsPattern, forbidNonPortableRuntime);
569592
reader.accept(scanner, ClassReader.SKIP_FRAMES);
570593
final List<ForbiddenViolation> violations = scanner.getSortedViolations();
571594
final Pattern splitter = Pattern.compile(Pattern.quote(ForbiddenViolation.SEPARATOR));

0 commit comments

Comments
 (0)