8353185: Introduce the concept of upgradeable files in context of JEP 493

Reviewed-by: clanger, ihse, alanb
This commit is contained in:
Severin Gehwolf 2025-04-15 10:16:31 +00:00
parent d7676c39b6
commit 4e24dc003c
5 changed files with 254 additions and 4 deletions

View File

@ -0,0 +1,32 @@
#
# Copyright (c) 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. 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.
#
################################################################################
# Instruct SetupJavaCompilation for the jdk.jlink module to include
# upgrade_files_<module-name>.conf files
COPY += .conf
################################################################################

View File

@ -45,6 +45,7 @@ import java.util.HexFormat;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -76,6 +77,7 @@ public class JRTArchive implements Archive {
private final Map<String, ResourceDiff> resDiff; private final Map<String, ResourceDiff> resDiff;
private final boolean errorOnModifiedFile; private final boolean errorOnModifiedFile;
private final TaskHelper taskHelper; private final TaskHelper taskHelper;
private final Set<String> upgradeableFiles;
/** /**
* JRTArchive constructor * JRTArchive constructor
@ -86,12 +88,15 @@ public class JRTArchive implements Archive {
* install aborts the link. * install aborts the link.
* @param perModDiff The lib/modules (a.k.a jimage) diff for this module, * @param perModDiff The lib/modules (a.k.a jimage) diff for this module,
* possibly an empty list if there are no differences. * possibly an empty list if there are no differences.
* @param taskHelper The task helper instance.
* @param upgradeableFiles The set of files that are allowed for upgrades.
*/ */
JRTArchive(String module, JRTArchive(String module,
Path path, Path path,
boolean errorOnModifiedFile, boolean errorOnModifiedFile,
List<ResourceDiff> perModDiff, List<ResourceDiff> perModDiff,
TaskHelper taskHelper) { TaskHelper taskHelper,
Set<String> upgradeableFiles) {
this.module = module; this.module = module;
this.path = path; this.path = path;
this.ref = ModuleFinder.ofSystem() this.ref = ModuleFinder.ofSystem()
@ -105,6 +110,7 @@ public class JRTArchive implements Archive {
this.resDiff = Objects.requireNonNull(perModDiff).stream() this.resDiff = Objects.requireNonNull(perModDiff).stream()
.collect(Collectors.toMap(ResourceDiff::getName, Function.identity())); .collect(Collectors.toMap(ResourceDiff::getName, Function.identity()));
this.taskHelper = taskHelper; this.taskHelper = taskHelper;
this.upgradeableFiles = upgradeableFiles;
} }
@Override @Override
@ -217,7 +223,8 @@ public class JRTArchive implements Archive {
// Read from the base JDK image. // Read from the base JDK image.
Path path = BASE.resolve(m.resPath); Path path = BASE.resolve(m.resPath);
if (shaSumMismatch(path, m.hashOrTarget, m.symlink)) { if (!isUpgradeableFile(m.resPath) &&
shaSumMismatch(path, m.hashOrTarget, m.symlink)) {
if (errorOnModifiedFile) { if (errorOnModifiedFile) {
String msg = taskHelper.getMessage("err.runtime.link.modified.file", path.toString()); String msg = taskHelper.getMessage("err.runtime.link.modified.file", path.toString());
IOException cause = new IOException(msg); IOException cause = new IOException(msg);
@ -239,6 +246,17 @@ public class JRTArchive implements Archive {
} }
} }
/**
* Certain files in a module are considered upgradeable. That is,
* their hash sums aren't checked.
*
* @param resPath The resource path of the file to check for upgradeability.
* @return {@code true} if the file is upgradeable. {@code false} otherwise.
*/
private boolean isUpgradeableFile(String resPath) {
return upgradeableFiles.contains(resPath);
}
static boolean shaSumMismatch(Path res, String expectedSha, boolean isSymlink) { static boolean shaSumMismatch(Path res, String expectedSha, boolean isSymlink) {
if (isSymlink) { if (isSymlink) {
return false; return false;

View File

@ -28,7 +28,10 @@ package jdk.tools.jlink.internal;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Scanner;
import java.util.Set;
import jdk.tools.jlink.internal.runtimelink.ResourceDiff; import jdk.tools.jlink.internal.runtimelink.ResourceDiff;
@ -42,6 +45,9 @@ public class LinkableRuntimeImage {
public static final String RESPATH_PATTERN = "jdk/tools/jlink/internal/runtimelink/fs_%s_files"; public static final String RESPATH_PATTERN = "jdk/tools/jlink/internal/runtimelink/fs_%s_files";
// The diff files per module for supporting linking from the run-time image // The diff files per module for supporting linking from the run-time image
public static final String DIFF_PATTERN = "jdk/tools/jlink/internal/runtimelink/diff_%s"; public static final String DIFF_PATTERN = "jdk/tools/jlink/internal/runtimelink/diff_%s";
// meta data for upgradable files
private static final String UPGRADEABLE_FILES_PATTERN = "jdk/tools/jlink/internal/runtimelink/upgrade_files_%s.conf";
private static final Module JDK_JLINK_MOD = LinkableRuntimeImage.class.getModule();
/** /**
* In order to be able to show whether or not a runtime is capable of * In order to be able to show whether or not a runtime is capable of
@ -62,7 +68,38 @@ public class LinkableRuntimeImage {
private static InputStream getDiffInputStream(String module) throws IOException { private static InputStream getDiffInputStream(String module) throws IOException {
String resourceName = String.format(DIFF_PATTERN, module); String resourceName = String.format(DIFF_PATTERN, module);
return LinkableRuntimeImage.class.getModule().getResourceAsStream(resourceName); return JDK_JLINK_MOD.getResourceAsStream(resourceName);
}
private static Set<String> upgradeableFiles(String module) {
String resourceName = String.format(UPGRADEABLE_FILES_PATTERN, module);
InputStream filesIn = null;
try {
filesIn = JDK_JLINK_MOD.getResourceAsStream(resourceName);
} catch (IOException e) {
throw new AssertionError("Unexpected IO error getting res stream");
}
if (filesIn == null) {
// no upgradeable files
return Set.of();
}
Set<String> upgradeableFiles = new HashSet<>();
final InputStream in = filesIn;
try (in;
Scanner scanner = new Scanner(filesIn)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (line.trim().startsWith("#")) {
// Skip comments
continue;
}
upgradeableFiles.add(scanner.nextLine());
}
} catch (IOException e) {
throw new AssertionError("Failure to retrieve upgradeable files for " +
"module " + module, e);
}
return upgradeableFiles;
} }
public static Archive newArchive(String module, public static Archive newArchive(String module,
@ -81,7 +118,13 @@ public class LinkableRuntimeImage {
throw new AssertionError("Failure to retrieve resource diff for " + throw new AssertionError("Failure to retrieve resource diff for " +
"module " + module, e); "module " + module, e);
} }
return new JRTArchive(module, path, !ignoreModifiedRuntime, perModuleDiff, taskHelper); Set<String> upgradeableFiles = upgradeableFiles(module);
return new JRTArchive(module,
path,
!ignoreModifiedRuntime,
perModuleDiff,
taskHelper,
upgradeableFiles);
} }

View File

@ -0,0 +1,4 @@
# Configuration for resource paths of files allowed to be
# upgraded (in java.base)
lib/tzdb.dat
lib/security/cacerts

View File

@ -0,0 +1,153 @@
/*
* Copyright (c) 2025, Red Hat, Inc.
* 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.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import jdk.test.lib.process.OutputAnalyzer;
import tests.Helper;
/*
* @test
* @summary Verify that no errors are reported for files that have been
* upgraded when linking from the run-time image
* @requires (vm.compMode != "Xcomp" & os.maxMemory >= 2g)
* @library ../../lib /test/lib
* @modules java.base/jdk.internal.jimage
* jdk.jlink/jdk.tools.jlink.internal
* jdk.jlink/jdk.tools.jlink.plugin
* jdk.jlink/jdk.tools.jimage
* @build tests.* jdk.test.lib.process.OutputAnalyzer
* jdk.test.lib.process.ProcessTools
* @run main/othervm -Xmx1g UpgradeableFileCacertsTest
*/
public class UpgradeableFileCacertsTest extends ModifiedFilesTest {
/*
* Generated with:
* $ rm -f server.keystore && keytool -genkey -alias jlink-upgrade-test \
* -keyalg RSA -dname CN=jlink-upgrade-test \
* -storepass changeit -keysize 3072 -sigalg SHA512withRSA \
* -validity 7300 -keystore server.keystore
* $ keytool -export -alias jlink-upgrade-test -storepass changeit \
* -keystore server.keystore -rfc
*/
private static final String CERT = """
-----BEGIN CERTIFICATE-----
MIID3jCCAkagAwIBAgIJALiT/+HXBkSIMA0GCSqGSIb3DQEBDQUAMB0xGzAZBgNV
BAMTEmpsaW5rLXVwZ3JhZGUtdGVzdDAeFw0yNTA0MDQxMjA3MjJaFw00NTAzMzAx
MjA3MjJaMB0xGzAZBgNVBAMTEmpsaW5rLXVwZ3JhZGUtdGVzdDCCAaIwDQYJKoZI
hvcNAQEBBQADggGPADCCAYoCggGBANmrnCDKqSXEJRIiSi4yHWN97ILls3RqYjED
la3AZTeXnZrrEIgSjVFUMxCztYqbWoVzKa2lov42Vue2BXVYffcQ8TKc2EJDNO+2
uRKQZpsN7RI4QoVBR2Rq8emrO8CrdOQT7Hh4agxkN9AOvGKMFdt+fXeCIPIuflKP
f+RfvhLfC2A70Y+Uu74C5uWgLloA/HF0SsVxf9KmqS9fZBQaiTYhKyoDghCRlWpa
nPIHB1XVaRdw8aSpCuzIOQzSCTTlLcammJkBjbFwMZdQG7eglTWzIYryZwe/cyY2
xctLVW3xhUHvnMFG+MajeFny2mxNu163Rxf/rBu4e7jRC/LGSU784nJGapq5K170
WbaeceKp+YORJBviFFORrmkPIwIgE+iGCD6PD6Xwu8vcpeuTVDgsSWMlfgCL3NoI
GXmdGiI2Xc/hQX7uzu3UBF6IcPDMTcYr2JKYbgu3v2/vDlJu3qO2ycUeePo5jhuG
X2WgcHkb6uOU4W5qdbCA+wFPVZBuwQIDAQABoyEwHzAdBgNVHQ4EFgQUtMJM0+ct
ssKqryRckk4YEWdYAZkwDQYJKoZIhvcNAQENBQADggGBAI8A6gJQ8wDx12sy2ZI4
1q9b+WG6w3LcFEF6Fko5NBizhtfmVycQv4mBa/NJgx4DZmd+5d60gJcTp/hJXGY0
LZyFilm/AgxsLNUUQLbHAV6TWqd3ODWwswAuew9sFU6izl286a9W65tbMWL5r1EA
t34ZYVWZYbCS9+czU98WomH4uarRAOlzcEUui3ZX6ZcQxWbz/R2wtKcUPUAYnsqH
JPivpE25G5xW2Dp/yeQTrlffq9OLgZWVz0jtOguBUMnsUsgCcpQZtqZX08//wtpz
ohLHFGvpXTPbRumRasWWtnRR/QqGRT66tYDqybXXz37UtKZ8VKW0sv2ypVbmAEs5
pLkA/3XiXlstJuCD6cW0Gfbpb5rrPPD46O3FDVlmqlTH3b/MsiQREdydqGzqY7uG
AA2GFVaKFASA5ls01CfHLAcrKxSVixditXvsjeIqhddB7Pnbsx20RdzPQoeo9/hF
WeIrh4zePDPZChuLR8ZyxeVJhLB71nTrTDDjwXarVez9Xw==
-----END CERTIFICATE-----
""";
private static final String CERT_ALIAS = "jlink-upgrade-test";
public static void main(String[] args) throws Exception {
UpgradeableFileCacertsTest test = new UpgradeableFileCacertsTest();
test.run();
}
@Override
String initialImageName() {
return "java-base-jlink-upgrade-cacerts";
}
@Override
void testAndAssert(Path modifiedFile, Helper helper, Path initialImage) throws Exception {
CapturingHandler handler = new CapturingHandler();
jlinkUsingImage(new JlinkSpecBuilder()
.helper(helper)
.imagePath(initialImage)
.name("java-base-jlink-upgrade-cacerts-target")
.addModule("java.base")
.validatingModule("java.base")
.build(), handler);
OutputAnalyzer analyzer = handler.analyzer();
// verify we don't get any modified warning
analyzer.stdoutShouldNotContain(modifiedFile.toString() + " has been modified");
analyzer.stdoutShouldNotContain("java.lang.IllegalArgumentException");
analyzer.stdoutShouldNotContain("IOException");
}
// Add an extra certificate in the cacerts file so that it no longer matches
// the recorded hash sum at build time.
protected Path modifyFileInImage(Path jmodLessImg)
throws IOException, AssertionError {
Path cacerts = jmodLessImg.resolve(Path.of("lib", "security", "cacerts"));
try (FileInputStream fin = new FileInputStream(cacerts.toFile())) {
KeyStore certStore = KeyStore.getInstance(cacerts.toFile(),
(char[])null);
certStore.load(fin, (char[])null);
X509Certificate cert;
try (ByteArrayInputStream bin = new ByteArrayInputStream(CERT.getBytes())) {
cert = (X509Certificate)generateCertificate(bin);
} catch (ClassCastException | CertificateException ce) {
throw new AssertionError("Test failed unexpectedly", ce);
}
certStore.setCertificateEntry(CERT_ALIAS, cert);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
certStore.store(bout, (char[])null);
try (FileOutputStream fout = new FileOutputStream(cacerts.toFile())) {
fout.write(bout.toByteArray());
}
} catch (Exception e) {
throw new AssertionError("Test failed unexpectedly: ", e);
}
return cacerts;
}
private Certificate generateCertificate(InputStream in)
throws CertificateException, IOException {
byte[] data = in.readAllBytes();
return CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(data));
}
}