java-topology/test/jdk/build/AbsPathsInImage.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

340 lines
14 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* Copyright (c) 2019, 2025, 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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static java.util.Comparator.comparing;
/*
* @test
* @bug 8226346
* @summary Check all output files for absolute path fragments
* @requires !vm.debug
* @comment ASAN keeps the 'unwanted' paths in the binaries because of its build options
* @requires !vm.asan
* @run main AbsPathsInImage
*/
public class AbsPathsInImage {
// Set this property on command line to scan an alternate dir or file:
// JTREG=JAVA_OPTIONS=-Djdk.test.build.AbsPathInImage.dir=/path/to/dir
public static final String DIR_PROPERTY = "jdk.test.build.AbsPathsInImage.dir";
private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows");
private static final boolean IS_LINUX = System.getProperty("os.name").toLowerCase().contains("linux");
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static List<byte[]> searchPatterns = new ArrayList<>();
private static List<int[]> prefixTables = new ArrayList<>();
private boolean matchFound = false;
record Match(int begin, int end) { }
public static void main(String[] args) throws Exception {
String jdkPathString = System.getProperty("test.jdk");
Path jdkHome = Paths.get(jdkPathString);
Path dirToScan = jdkHome;
String overrideDir = System.getProperty(DIR_PROPERTY);
if (overrideDir != null) {
dirToScan = Paths.get(overrideDir);
}
String buildWorkspaceRoot = null;
String buildOutputRoot = null;
String testImageDirString = System.getenv("TEST_IMAGE_DIR");
if (testImageDirString != null) {
Path testImageDir = Paths.get(testImageDirString);
Path buildInfoPropertiesFile = testImageDir.resolve("build-info.properties");
System.out.println("Getting patterns from " + buildInfoPropertiesFile.toString());
Properties buildInfoProperties = new Properties();
try (InputStream inStream = Files.newInputStream(buildInfoPropertiesFile)) {
buildInfoProperties.load(inStream);
}
buildWorkspaceRoot = buildInfoProperties.getProperty("build.workspace.root");
buildOutputRoot = buildInfoProperties.getProperty("build.output.root");
} else {
System.out.println("Getting patterns from local environment");
// Try to resolve the workspace root based on the jtreg test root dir
String testRootDirString = System.getProperty("test.root");
if (testRootDirString != null) {
Path testRootDir = Paths.get(testRootDirString);
// Remove /test/jdk suffix
buildWorkspaceRoot = testRootDir.getParent().getParent().toString();
}
// Remove /jdk
Path buildOutputRootPath = jdkHome.getParent();
if (buildOutputRootPath.endsWith("images")) {
buildOutputRootPath = buildOutputRootPath.getParent();
}
buildOutputRoot = buildOutputRootPath.toString();
}
if (buildWorkspaceRoot == null) {
throw new Error("Could not find workspace root, test cannot run");
}
if (buildOutputRoot == null) {
throw new Error("Could not find build output root, test cannot run");
}
// Validate the root paths
if (!Paths.get(buildWorkspaceRoot).isAbsolute()) {
throw new Error("Workspace root is not an absolute path: " + buildWorkspaceRoot);
}
if (!Paths.get(buildOutputRoot).isAbsolute()) {
throw new Error("Output root is not an absolute path: " + buildOutputRoot);
}
expandPatterns(buildWorkspaceRoot);
expandPatterns(buildOutputRoot);
createPrefixTables();
System.out.println("Looking for:");
for (byte[] searchPattern : searchPatterns) {
System.out.println(new String(searchPattern));
}
System.out.println();
AbsPathsInImage absPathsInImage = new AbsPathsInImage();
absPathsInImage.scanFiles(dirToScan);
if (absPathsInImage.matchFound) {
throw new Exception("Test failed");
}
}
/**
* Add path pattern to list of patterns to search for. Create all possible
* variants depending on platform.
*/
private static void expandPatterns(String pattern) {
if (IS_WINDOWS) {
String forward = pattern.replace('\\', '/');
String back = pattern.replace('/', '\\');
if (pattern.charAt(1) == ':') {
String forwardUpper = String.valueOf(pattern.charAt(0)).toUpperCase() + forward.substring(1);
String forwardLower = String.valueOf(pattern.charAt(0)).toLowerCase() + forward.substring(1);
String backUpper = String.valueOf(pattern.charAt(0)).toUpperCase() + back.substring(1);
String backLower = String.valueOf(pattern.charAt(0)).toLowerCase() + back.substring(1);
searchPatterns.add(forwardUpper.getBytes());
searchPatterns.add(forwardLower.getBytes());
searchPatterns.add(backUpper.getBytes());
searchPatterns.add(backLower.getBytes());
} else {
searchPatterns.add(forward.getBytes());
searchPatterns.add(back.getBytes());
}
} else {
searchPatterns.add(pattern.getBytes());
}
}
/**
* The failure function for KMP. Returns the correct index in the pattern to jump
* back to when encountering a mismatched character. Used in both
* createPrefixTables (pre-processing) and scanBytes (matching).
*/
private static int getPrefixIndex(int patternIdx, int state, byte match) {
if (state == 0) {
return 0;
}
byte[] searchPattern = searchPatterns.get(patternIdx);
int[] prefixTable = prefixTables.get(patternIdx);
int i = prefixTable[state - 1];
while (i > 0 && searchPattern[i] != match) {
i = prefixTable[i - 1];
}
return searchPattern[i] == match ? i + 1 : i;
}
/**
* Pre-processing string patterns for KnuthMorrisPratt (KMP) search algorithm.
* Lookup tables of longest prefixes at each given index are created for each
* search pattern string. These tables are later used in scanBytes during matching
* as lookups for failure state transitions.
*/
private static void createPrefixTables() {
for (int patternIdx = 0; patternIdx < searchPatterns.size(); patternIdx++) {
int patternLen = searchPatterns.get(patternIdx).length;
int[] prefixTable = new int[patternLen];
prefixTables.add(prefixTable);
for (int i = 1; i < patternLen; i++) {
prefixTable[i] = getPrefixIndex(patternIdx, i, searchPatterns.get(patternIdx)[i]);
}
}
}
private void scanFiles(Path root) throws IOException {
Files.walkFileTree(root, new SimpleFileVisitor<>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
String dirName = dir.toString();
if (dirName.endsWith(".dSYM")) {
return FileVisitResult.SKIP_SUBTREE;
}
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String fileName = file.toString();
if (Files.isSymbolicLink(file)) {
return super.visitFile(file, attrs);
} else if ((fileName.endsWith(".debuginfo") && !IS_LINUX) || fileName.endsWith(".pdb")) {
// Do nothing
} else if (fileName.endsWith(".zip")) {
scanZipFile(file);
} else {
scanFile(file);
}
return super.visitFile(file, attrs);
}
});
}
private void scanFile(Path file) throws IOException {
List<Match> matches;
try (InputStream inputStream = Files.newInputStream(file)) {
matches = scanBytes(inputStream);
}
// test succeeds
if (matches.size() == 0) {
return;
}
// test fails; pay penalty and re-scan file for debug output
try (InputStream inputStream = Files.newInputStream(file)) {
printDebugOutput(inputStream, matches, file + ":");
}
}
private void scanZipFile(Path zipFile) throws IOException {
List<List<Match>> entryMatches = new ArrayList<>();
boolean found = false;
ZipEntry zipEntry;
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(zipFile))) {
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
List<Match> matches = scanBytes(zipInputStream);
if (matches.size() > 0) {
entryMatches.add(matches);
found = true;
} else {
entryMatches.add(null);
}
}
}
// test succeeds
if (!found) {
return;
}
// test fails
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(zipFile))) {
int i = 0;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
List<Match> matches = entryMatches.get(i);
i++;
if (matches != null) {
printDebugOutput(zipInputStream, matches, zipFile + ", " + zipEntry.getName() + ":");
}
}
}
}
/**
* Scans each byte until encounters a match with one of searchPatterns. Uses KMP to
* perform matches. Keep track of current matched index (states) for each search
* pattern. At each given byte, update states accordingly (increment if match or
* failure function transition if mismatch). Returns a list of Match objects.
*/
private List<Match> scanBytes(InputStream input) throws IOException {
List<Match> matches = new ArrayList<>();
byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
int[] states = new int[searchPatterns.size()];
int fileIdx = 0;
int bytesRead, patternLen;
while ((bytesRead = input.read(buf)) != -1) {
for (int bufIdx = 0; bufIdx < bytesRead; bufIdx++, fileIdx++) {
byte datum = buf[bufIdx];
for (int i = 0; i < searchPatterns.size(); i++) {
patternLen = searchPatterns.get(i).length;
if (datum != searchPatterns.get(i)[states[i]]) {
states[i] = getPrefixIndex(i, states[i], datum);
} else if (++states[i] == patternLen) {
// technically at last match, state should reset according to failure function
// but in original test, matching didn't search same string for multiple matches
states[i] = 0;
matches.add(new Match(fileIdx - patternLen + 1, fileIdx));
break;
}
}
}
}
return matches;
}
/**
* In original test, failed test output would backtrack to last non-ascii byte on
* matched pattern. This is incompatible with the new buffered approach (and a
* proper solution requires a 2nd dynamic buffer). Instead, on failed test case,
* files are scanned a 2nd time to print debug output. Failed runs will pay
* additional performance/space penalty, but passing runs are faster.
*/
private void printDebugOutput(InputStream input, List<Match> matches, final String HEADER) throws IOException{
matchFound = true;
System.out.println(HEADER);
matches.sort(comparing(Match::begin));
ByteArrayOutputStream output = new ByteArrayOutputStream();
byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
int matchIdx = 0;
int fileIdx = 0;
int bytesRead;
while (matchIdx < matches.size() && (bytesRead = input.read(buf)) != -1) {
for (int i = 0; matchIdx < matches.size() && i < bytesRead; i++, fileIdx++) {
byte datum = buf[i];
if (datum >= 32 && datum <= 126) {
output.write(datum);
} else if (fileIdx < matches.get(matchIdx).begin()) {
output.reset();
} else if (fileIdx > matches.get(matchIdx).end()) {
System.out.println(output.toString());
output.reset();
// This imperfect as incorrect in edge cases with patterns containing non-ascii?
// but high-accuracy not priority + output still legible and useful
for (; matchIdx < matches.size() && matches.get(matchIdx).end() < fileIdx; matchIdx++);
} else {
output.write(datum);
}
}
}
System.out.println();
}
}