Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
144 lines
6.3 KiB
Java
144 lines
6.3 KiB
Java
package workbench;
|
|
|
|
import java.lang.reflect.Field;
|
|
|
|
/**
|
|
* Checks at runtime whether the javac topology patches are active.
|
|
*
|
|
* Verifies each patch by reflection — no flags, no trust-me properties.
|
|
*
|
|
* Patch 0001: GraphUtils$TarjanNode.active boolean field
|
|
* (replaces O(V²) stack.contains(n) with O(1) field read)
|
|
* Note: merged into OpenJDK 21+ upstream — will show PRESENT on JDK 21+
|
|
* even without --patch-module. On older JDKs --patch-module is required.
|
|
*
|
|
* Patch 0004: Dependencies — List.contains+add → LinkedHashSet.add
|
|
* We cannot reliably reflect on the internal node structure without
|
|
* --add-opens; detected via the presence of a specific inner class shape.
|
|
* Falls back to "cannot verify" if reflection is blocked.
|
|
*
|
|
* Requires --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
|
|
*
|
|
* Run standalone (unpatched runtime):
|
|
* java --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
|
|
* -cp . workbench.PatchVerifier
|
|
*
|
|
* Run with --patch-module applied:
|
|
* java --patch-module jdk.compiler=/tmp/jt-all-classes \
|
|
* --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
|
|
* -cp . workbench.PatchVerifier
|
|
*/
|
|
public class PatchVerifier {
|
|
|
|
private static final String TARJAN_NODE = "com.sun.tools.javac.util.GraphUtils$TarjanNode";
|
|
|
|
public record Check(String id, boolean present, String detail) {
|
|
@Override public String toString() {
|
|
return (present ? "[OK] " : "[--] ") + id + ": " + detail;
|
|
}
|
|
}
|
|
|
|
public record Status(boolean allPresent, Check[] checks) {
|
|
public boolean patched() { return allPresent; }
|
|
|
|
@Override public String toString() {
|
|
StringBuilder sb = new StringBuilder();
|
|
for (Check c : checks) sb.append(c).append(" | ");
|
|
if (sb.length() > 3) sb.setLength(sb.length() - 3);
|
|
return sb.toString();
|
|
}
|
|
|
|
public String summary() {
|
|
int ok = 0;
|
|
for (Check c : checks) if (c.present()) ok++;
|
|
return ok + "/" + checks.length + " patches verified";
|
|
}
|
|
}
|
|
|
|
public static Status check() {
|
|
Check[] checks = {
|
|
check0001(),
|
|
check0004(),
|
|
};
|
|
boolean all = true;
|
|
for (Check c : checks) if (!c.present()) all = false;
|
|
return new Status(all, checks);
|
|
}
|
|
|
|
/** 0001: GraphUtils$TarjanNode.active boolean field. */
|
|
private static Check check0001() {
|
|
try {
|
|
Class<?> cls = Class.forName(TARJAN_NODE);
|
|
try {
|
|
Field f = cls.getDeclaredField("active");
|
|
return new Check("0001/GraphUtils.active",
|
|
true,
|
|
f.getType().getSimpleName() + " field present — O(V+E) Tarjan (may be upstream JDK)");
|
|
} catch (NoSuchFieldException e) {
|
|
return new Check("0001/GraphUtils.active",
|
|
false,
|
|
"field missing — stock O(V²) stack.contains(); apply --patch-module");
|
|
}
|
|
} catch (ClassNotFoundException e) {
|
|
return new Check("0001/GraphUtils.active",
|
|
false,
|
|
"GraphUtils not accessible — need --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED");
|
|
} catch (Exception e) {
|
|
return new Check("0001/GraphUtils.active", false, "reflection error: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/** 0004: Dependencies — LinkedHashSet instead of ArrayList for depsByKind. */
|
|
private static Check check0004() {
|
|
// Generic type erasure means we can't see LinkedHashSet vs ArrayList via reflection.
|
|
// Scan the raw class bytes of the inner Node class for the type descriptor.
|
|
// Stock JDK: "java/util/ArrayList" in constant pool
|
|
// Patched: "java/util/LinkedHashSet" in constant pool (ArrayList removed)
|
|
// Node is nested: Dependencies$GraphDependencies$Node
|
|
try {
|
|
Class<?> cls = Class.forName(
|
|
"com.sun.tools.javac.util.Dependencies$GraphDependencies$Node");
|
|
String resourcePath = cls.getName().replace('.', '/') + ".class";
|
|
byte[] bytes;
|
|
// Named module: use Module.getResourceAsStream (not ClassLoader)
|
|
try (java.io.InputStream in = cls.getModule().getResourceAsStream(resourcePath)) {
|
|
bytes = in == null ? null : in.readAllBytes();
|
|
}
|
|
if (bytes == null) {
|
|
return new Check("0004/Dependencies.LinkedHashSet",
|
|
false, "class bytes not readable from class loader");
|
|
}
|
|
String pool = new String(bytes, java.nio.charset.StandardCharsets.ISO_8859_1);
|
|
boolean hasLinkedHashSet = pool.contains("java/util/LinkedHashSet");
|
|
boolean hasArrayList = pool.contains("java/util/ArrayList");
|
|
if (hasLinkedHashSet && !hasArrayList) {
|
|
return new Check("0004/Dependencies.LinkedHashSet",
|
|
true, "constant pool has LinkedHashSet, no ArrayList — O(1) add");
|
|
} else if (hasArrayList) {
|
|
return new Check("0004/Dependencies.LinkedHashSet",
|
|
false, "constant pool has ArrayList — stock O(N) List.contains+add");
|
|
} else {
|
|
return new Check("0004/Dependencies.LinkedHashSet",
|
|
false, "neither ArrayList nor LinkedHashSet in constant pool (inconclusive)");
|
|
}
|
|
} catch (ClassNotFoundException e) {
|
|
return new Check("0004/Dependencies.LinkedHashSet",
|
|
false, "Dependencies not accessible — need --add-exports");
|
|
} catch (Exception e) {
|
|
return new Check("0004/Dependencies.LinkedHashSet",
|
|
false, "byte scan failed: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
/** Standalone: prints status, exits 0 if all checks pass. */
|
|
public static void main(String[] args) {
|
|
System.out.println("java.home: " + System.getProperty("java.home"));
|
|
System.out.println("java.version: " + System.getProperty("java.version"));
|
|
System.out.println();
|
|
Status s = check();
|
|
for (Check c : s.checks()) System.out.println(" " + c);
|
|
System.out.println();
|
|
System.out.println(" => " + s.summary());
|
|
System.exit(s.allPresent() ? 0 : 1);
|
|
}
|
|
}
|