From 4e24dc003c2304041b342371adf430b120a9fec8 Mon Sep 17 00:00:00 2001 From: Severin Gehwolf Date: Tue, 15 Apr 2025 10:16:31 +0000 Subject: [PATCH] 8353185: Introduce the concept of upgradeable files in context of JEP 493 Reviewed-by: clanger, ihse, alanb --- make/modules/jdk.jlink/Java.gmk | 32 ++++ .../jdk/tools/jlink/internal/JRTArchive.java | 22 ++- .../jlink/internal/LinkableRuntimeImage.java | 47 +++++- .../runtimelink/upgrade_files_java.base.conf | 4 + .../UpgradeableFileCacertsTest.java | 153 ++++++++++++++++++ 5 files changed, 254 insertions(+), 4 deletions(-) create mode 100644 make/modules/jdk.jlink/Java.gmk create mode 100644 src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/upgrade_files_java.base.conf create mode 100644 test/jdk/tools/jlink/runtimeImage/UpgradeableFileCacertsTest.java diff --git a/make/modules/jdk.jlink/Java.gmk b/make/modules/jdk.jlink/Java.gmk new file mode 100644 index 00000000000..4ddd1eab03d --- /dev/null +++ b/make/modules/jdk.jlink/Java.gmk @@ -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_.conf files +COPY += .conf + +################################################################################ diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java index df7d35ac777..aac220e5b94 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/JRTArchive.java @@ -45,6 +45,7 @@ import java.util.HexFormat; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -76,6 +77,7 @@ public class JRTArchive implements Archive { private final Map resDiff; private final boolean errorOnModifiedFile; private final TaskHelper taskHelper; + private final Set upgradeableFiles; /** * JRTArchive constructor @@ -86,12 +88,15 @@ public class JRTArchive implements Archive { * install aborts the link. * @param perModDiff The lib/modules (a.k.a jimage) diff for this module, * 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, Path path, boolean errorOnModifiedFile, List perModDiff, - TaskHelper taskHelper) { + TaskHelper taskHelper, + Set upgradeableFiles) { this.module = module; this.path = path; this.ref = ModuleFinder.ofSystem() @@ -105,6 +110,7 @@ public class JRTArchive implements Archive { this.resDiff = Objects.requireNonNull(perModDiff).stream() .collect(Collectors.toMap(ResourceDiff::getName, Function.identity())); this.taskHelper = taskHelper; + this.upgradeableFiles = upgradeableFiles; } @Override @@ -217,7 +223,8 @@ public class JRTArchive implements Archive { // Read from the base JDK image. 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) { String msg = taskHelper.getMessage("err.runtime.link.modified.file", path.toString()); 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) { if (isSymlink) { return false; diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java index 935af4585ad..d564ed0bad8 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/LinkableRuntimeImage.java @@ -28,7 +28,10 @@ package jdk.tools.jlink.internal; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; +import java.util.HashSet; import java.util.List; +import java.util.Scanner; +import java.util.Set; 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"; // 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"; + // 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 @@ -62,7 +68,38 @@ public class LinkableRuntimeImage { private static InputStream getDiffInputStream(String module) throws IOException { String resourceName = String.format(DIFF_PATTERN, module); - return LinkableRuntimeImage.class.getModule().getResourceAsStream(resourceName); + return JDK_JLINK_MOD.getResourceAsStream(resourceName); + } + + private static Set 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 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, @@ -81,7 +118,13 @@ public class LinkableRuntimeImage { throw new AssertionError("Failure to retrieve resource diff for " + "module " + module, e); } - return new JRTArchive(module, path, !ignoreModifiedRuntime, perModuleDiff, taskHelper); + Set upgradeableFiles = upgradeableFiles(module); + return new JRTArchive(module, + path, + !ignoreModifiedRuntime, + perModuleDiff, + taskHelper, + upgradeableFiles); } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/upgrade_files_java.base.conf b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/upgrade_files_java.base.conf new file mode 100644 index 00000000000..df2ca809f08 --- /dev/null +++ b/src/jdk.jlink/share/classes/jdk/tools/jlink/internal/runtimelink/upgrade_files_java.base.conf @@ -0,0 +1,4 @@ +# Configuration for resource paths of files allowed to be +# upgraded (in java.base) +lib/tzdb.dat +lib/security/cacerts diff --git a/test/jdk/tools/jlink/runtimeImage/UpgradeableFileCacertsTest.java b/test/jdk/tools/jlink/runtimeImage/UpgradeableFileCacertsTest.java new file mode 100644 index 00000000000..86e0f8cefd5 --- /dev/null +++ b/test/jdk/tools/jlink/runtimeImage/UpgradeableFileCacertsTest.java @@ -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)); + } +}