java-topology/test/jdk/tools/jimage/VerifyJimage.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
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.
2026-03-26 17:11:57 -04:00

332 lines
14 KiB
Java

/*
* Copyright (c) 2014, 2026, 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.
*
* 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.
*/
import jdk.internal.jimage.BasicImageReader;
import jtreg.SkippedException;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URI;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import static java.util.stream.Collectors.joining;
/*
* @test id=load
* @summary Load all classes defined in JRT file system.
* @library /test/lib
* @modules java.base/jdk.internal.jimage
* @run main/othervm --add-modules ALL-SYSTEM VerifyJimage
*/
/*
* @test id=compare
* @summary Compare an exploded directory of module classes with the system jimage.
* @library /test/lib
* @modules java.base/jdk.internal.jimage
* @run main/othervm --add-modules ALL-SYSTEM -Djdk.test.threads=10 VerifyJimage ../../jdk/modules
*/
public abstract class VerifyJimage implements Runnable {
private static final String MODULE_INFO = "module-info.class";
public static void main(String... args) throws Exception {
// Best practice is to read "test.jdk" in preference to "java.home".
String testJdk = System.getProperty("test.jdk", System.getProperty("java.home"));
Path jdkRoot = Path.of(testJdk);
Path bootimagePath = jdkRoot.resolve("lib", "modules");
if (Files.notExists(bootimagePath)) {
throw new SkippedException("No boot image: " + bootimagePath);
}
FileSystem jrtFs = FileSystems.getFileSystem(URI.create("jrt:/"));
Path modulesRoot = jrtFs.getPath("/").resolve("modules");
List<String> modules;
try (Stream<Path> moduleDirs = Files.list(modulesRoot)) {
modules = moduleDirs.map(Path::getFileName).map(Object::toString).toList();
}
VerifyJimage verifier;
if (args.length == 0) {
verifier = new ClassLoadingVerifier(modules, modulesRoot);
} else {
Path pathArg = Path.of(args[0].replace("/", FileSystems.getDefault().getSeparator()));
// The path argument may be relative.
Path rootDir = jdkRoot.resolve(pathArg);
if (!Files.isDirectory(rootDir)) {
throw new SkippedException("No modules directory found: " + rootDir);
}
int maxThreads = Integer.getInteger("jdk.test.threads", 1);
verifier = new DirectoryContentVerifier(modules, rootDir, maxThreads, bootimagePath);
}
verifier.verify();
}
final List<String> modules;
// Count of items which have passed verification.
final AtomicInteger verifiedCount = new AtomicInteger(0);
// Error messages for verification failures.
final Deque<String> failed = new ConcurrentLinkedDeque<>();
private VerifyJimage(List<String> modules) {
this.modules = modules;
}
void verify() {
long start = System.nanoTime();
run();
long end = System.nanoTime();
System.out.format("Verified %d entries: %d ms, %d errors%n",
verifiedCount.get(),
TimeUnit.NANOSECONDS.toMillis(end - start),
failed.size());
if (!failed.isEmpty()) {
failed.forEach(System.err::println);
throw new AssertionError("Test failed");
}
}
private static final class DirectoryContentVerifier extends VerifyJimage {
private final Path rootDir;
private final ExecutorService pool;
private final Path jimagePath;
DirectoryContentVerifier(List<String> modules, Path rootDir, int maxThreads, Path jimagePath) {
super(modules);
this.rootDir = rootDir;
this.pool = Executors.newFixedThreadPool(maxThreads);
this.jimagePath = jimagePath;
}
@Override
public void run() {
System.out.println("Comparing jimage with: " + rootDir);
try (BasicImageReader jimage = BasicImageReader.open(jimagePath)) {
for (String modName : modules) {
Path modDir = rootDir.resolve(modName);
if (!Files.isDirectory(modDir)) {
failed.add("Missing module directory: " + modDir);
} else {
pool.execute(new ModuleResourceComparator(rootDir, modName, jimage));
}
}
pool.close();
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
/**
* Verifies the contents of the current runtime jimage file by comparing
* entries with the on-disk resources in a given directory.
*/
private class ModuleResourceComparator implements Runnable {
private final Path rootDir;
private final String moduleName;
private final BasicImageReader jimage;
private final String moduleInfoName;
// Entries we expect to find in the jimage module.
private final Set<String> moduleEntries;
private final Set<String> handledEntries = new HashSet<>();
public ModuleResourceComparator(Path rootDir, String moduleName, BasicImageReader jimage) {
this.rootDir = rootDir;
this.moduleName = moduleName;
this.jimage = jimage;
String moduleEntryPrefix = "/" + moduleName + "/";
this.moduleInfoName = moduleEntryPrefix + MODULE_INFO;
this.moduleEntries =
Arrays.stream(jimage.getEntryNames())
.filter(n -> n.startsWith(moduleEntryPrefix))
.filter(n -> !isJimageOnly(n))
.collect(Collectors.toSet());
}
@Override
public void run() {
try (Stream<Path> files = Files.walk(rootDir.resolve(moduleName))) {
files.filter(this::shouldVerify).forEach(this::compareEntry);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
moduleEntries.stream()
.filter(n -> !handledEntries.contains(n))
.sorted()
.forEach(n -> failed.add("Untested jimage entry: " + n));
}
void compareEntry(Path path) {
String entryName = getEntryName(path);
if (!moduleEntries.contains(entryName)) {
// Corresponds to an on-disk file which is not expected to
// be present in the jimage. This is normal and is skipped.
return;
}
// Mark valid entries as "handled" to track if we've seen them
// (even if we don't test their content).
if (!handledEntries.add(entryName)) {
failed.add("Duplicate entry name: " + entryName);
return;
}
if (isExpectedToDiffer(entryName)) {
return;
}
try {
int mismatch = Arrays.mismatch(
Files.readAllBytes(path),
jimage.getResource(entryName));
if (mismatch == -1) {
verifiedCount.incrementAndGet();
} else {
failed.add("Content diff (byte offset " + mismatch + "): " + entryName);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Predicate for files which correspond to entries in the jimage.
*
* <p>This should be a narrow test with minimal chance of
* false-negative matching, primarily focusing on excluding build
* artifacts.
*/
boolean shouldVerify(Path path) {
// Use the entry name because we know it uses the '/' separator.
String entryName = getEntryName(path);
return Files.isRegularFile(path)
&& !entryName.contains("/_the.")
&& !entryName.contains("/_element_lists.");
}
/**
* Predicate for the limited subset of entries which are expected to
* exist in the file system, but are not expected to have the same
* content as the associated jimage entry. This is to handle files
* which are modified/patched by jlink plugins.
*
* <p>This should be a narrow test with minimal chance of
* false-positive matching.
*/
private boolean isExpectedToDiffer(String entryName) {
return entryName.equals(moduleInfoName)
|| (entryName.startsWith("/java.base/java/lang/invoke/") && entryName.endsWith("$Holder.class"))
|| entryName.equals("/java.base/jdk/internal/module/SystemModulesMap.class");
}
/**
* Predicate for the limited subset of entries which are not expected
* to exist in the file system, such as those created synthetically
* by jlink plugins.
*
* <p>This should be a narrow test with minimal chance of
* false-positive matching.
*/
private boolean isJimageOnly(String entryName) {
return entryName.startsWith("/java.base/jdk/internal/module/SystemModules$")
|| entryName.startsWith("/java.base/java/lang/invoke/BoundMethodHandle$Species_");
}
private String getEntryName(Path path) {
return StreamSupport.stream(rootDir.relativize(path).spliterator(), false)
.map(Object::toString).collect(joining("/", "/", ""));
}
}
}
/**
* Verifies the contents of the current runtime jimage file by attempting to
* load every available class based on the content of the JRT file system.
*/
static final class ClassLoadingVerifier extends VerifyJimage {
private static final String CLASS_SUFFIX = ".class";
private final Path modulesRoot;
ClassLoadingVerifier(List<String> modules, Path modulesRoot) {
super(modules);
this.modulesRoot = modulesRoot;
}
@Override
public void run() {
ClassLoader loader = ClassLoader.getSystemClassLoader();
for (String modName : modules) {
Path modDir = modulesRoot.resolve(modName);
try (Stream<Path> files = Files.walk(modDir)) {
files.map(modDir::relativize)
.filter(ClassLoadingVerifier::isClassFile)
.map(ClassLoadingVerifier::toClassName)
.forEach(cn -> loadClass(cn, loader));
} catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
private void loadClass(String cn, ClassLoader loader) {
try {
Class.forName(cn, false, loader);
verifiedCount.incrementAndGet();
} catch (VerifyError ve) {
System.err.println("VerifyError for " + cn);
failed.add("Class: " + cn + " not verified: " + ve.getMessage());
} catch (ClassNotFoundException e) {
failed.add("Class: " + cn + " not found");
}
}
/**
* Maps a module-relative JRT path of a class file to its corresponding
* fully-qualified class name.
*/
private static String toClassName(Path path) {
// JRT uses '/' as the separator, and relative paths don't start with '/'.
String s = path.toString();
return s.substring(0, s.length() - CLASS_SUFFIX.length()).replace('/', '.');
}
/** Whether a module-relative JRT file system path is a class file. */
private static boolean isClassFile(Path path) {
String classFileName = path.getFileName().toString();
return classFileName.endsWith(CLASS_SUFFIX)
&& !classFileName.equals(MODULE_INFO);
}
}
}