diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3e09a157fca..21dd97994d5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -310,46 +310,3 @@ jobs: bootjdk-platform: windows-x64 runs-on: windows-2025 - # Remove bundles so they are not misconstrued as binary distributions from the JDK project - remove-bundles: - name: 'Remove bundle artifacts' - runs-on: ubuntu-22.04 - if: always() - needs: - - build-linux-x64 - - build-linux-x86 - - build-linux-x64-hs-nopch - - build-linux-x64-hs-zero - - build-linux-x64-hs-minimal - - build-linux-x64-hs-optimized - - build-linux-cross-compile - - build-macos-x64 - - build-macos-aarch64 - - build-windows-x64 - - build-windows-aarch64 - - test-linux-x64 - - test-linux-x86 - - test-macos-x64 - - test-windows-x64 - - steps: - - name: 'Remove bundle artifacts' - run: | - # Find and remove all bundle artifacts - # See: https://docs.github.com/en/rest/actions/artifacts?apiVersion=2022-11-28 - ALL_ARTIFACT_IDS="$(curl -sL \ - -H 'Accept: application/vnd.github+json' \ - -H 'Authorization: Bearer ${{ github.token }}' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - '${{ github.api_url }}/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts?per_page=100')" - BUNDLE_ARTIFACT_IDS="$(echo "$ALL_ARTIFACT_IDS" | jq -r -c '.artifacts | map(select(.name|startswith("bundles-"))) | .[].id')" - for id in $BUNDLE_ARTIFACT_IDS; do - echo "Removing $id" - curl -sL \ - -X DELETE \ - -H 'Accept: application/vnd.github+json' \ - -H 'Authorization: Bearer ${{ github.token }}' \ - -H 'X-GitHub-Api-Version: 2022-11-28' \ - "${{ github.api_url }}/repos/${{ github.repository }}/actions/artifacts/$id" \ - || echo "Failed to remove bundle" - done diff --git a/make/CompileJavaModules.gmk b/make/CompileJavaModules.gmk index 46fb9b4219b..3795dc87f24 100644 --- a/make/CompileJavaModules.gmk +++ b/make/CompileJavaModules.gmk @@ -531,8 +531,13 @@ jdk.jfr_ADD_JAVAC_FLAGS := -XDstringConcat=inline -Xlint:-exports ################################################################################ # If this is an imported module that has prebuilt classes, only compile # module-info.java. -ifneq ($(wildcard $(IMPORT_MODULES_CLASSES)/$(MODULE)), ) - $(MODULE)_INCLUDE_FILES := module-info.java +ifneq ($(IMPORT_MODULES_CLASSES), ) + IMPORT_MODULE_DIR := $(IMPORT_MODULES_CLASSES)/$(MODULE) + ifneq ($(wildcard $(IMPORT_MODULE_DIR)), ) + $(MODULE)_INCLUDE_FILES := module-info.java + endif +else + IMPORT_MODULE_DIR := endif ################################################################################ @@ -638,13 +643,13 @@ endif # If this is an imported module, copy the pre built classes and resources into # the modules output dir -ifneq ($(wildcard $(IMPORT_MODULES_CLASSES)/$(MODULE)), ) +ifneq ($(wildcard $(IMPORT_MODULE_DIR)), ) $(JDK_OUTPUTDIR)/modules/$(MODULE)/_imported.marker: \ - $(call FindFiles, $(IMPORT_MODULES_CLASSES)/$(MODULE)) + $(call FindFiles, $(IMPORT_MODULE_DIR)) $(call MakeDir, $(@D)) # Do not delete marker and build meta data files $(RM) -r $(filter-out $(@D)/_%, $(wildcard $(@D)/*)) - $(CP) -R $(IMPORT_MODULES_CLASSES)/$(MODULE)/* $(@D)/ + $(CP) -R $(IMPORT_MODULE_DIR)/* $(@D)/ $(TOUCH) $@ TARGETS += $(JDK_OUTPUTDIR)/modules/$(MODULE)/_imported.marker diff --git a/src/bsd/doc/man/jarsigner.1 b/src/bsd/doc/man/jarsigner.1 index ee57c504487..0a64318c40a 100644 --- a/src/bsd/doc/man/jarsigner.1 +++ b/src/bsd/doc/man/jarsigner.1 @@ -624,6 +624,11 @@ Informational warnings include those that are not errors but regarded as bad pra hasExpiringCert This jar contains entries whose signer certificate will expire within six months\&. .TP +internalInconsistenciesDetected +This jar contains internal inconsistencies detected during verification +that may result in different contents when reading via JarFile +and JarInputStream\&. +.TP noTimestamp This jar contains signatures that does not include a timestamp\&. Without a timestamp, users may not be able to validate this JAR file after the signer certificate\&'s expiration date (\f3YYYY-MM-DD\fR) or after any future revocation date\&. .SH EXAMPLES diff --git a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp index 81e5e332b85..e15054ebbc9 100644 --- a/src/hotspot/os/linux/cgroupSubsystem_linux.cpp +++ b/src/hotspot/os/linux/cgroupSubsystem_linux.cpp @@ -35,7 +35,7 @@ #include "utilities/globalDefinitions.hpp" // controller names have to match the *_IDX indices -static const char* cg_controller_name[] = { "cpu", "cpuset", "cpuacct", "memory", "pids" }; +static const char* cg_controller_name[] = { "cpuset", "cpu", "cpuacct", "memory", "pids" }; CgroupSubsystem* CgroupSubsystemFactory::create() { CgroupV1MemoryController* memory = NULL; @@ -159,9 +159,10 @@ bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, char buf[MAXPATHLEN+1]; char *p; bool is_cgroupsV2; - // true iff all required controllers, memory, cpu, cpuset, cpuacct are enabled + // true iff all required controllers, memory, cpu, cpuacct are enabled // at the kernel level. // pids might not be enabled on older Linux distros (SLES 12.1, RHEL 7.1) + // cpuset might not be enabled on newer Linux distros (Fedora 41) bool all_required_controllers_enabled; /* @@ -193,6 +194,7 @@ bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, cg_infos[MEMORY_IDX]._hierarchy_id = hierarchy_id; cg_infos[MEMORY_IDX]._enabled = (enabled == 1); } else if (strcmp(name, "cpuset") == 0) { + log_debug(os, container)("Detected optional cpuset controller entry in %s", proc_cgroups); cg_infos[CPUSET_IDX]._name = os::strdup(name); cg_infos[CPUSET_IDX]._hierarchy_id = hierarchy_id; cg_infos[CPUSET_IDX]._enabled = (enabled == 1); @@ -216,8 +218,8 @@ bool CgroupSubsystemFactory::determine_type(CgroupInfo* cg_infos, is_cgroupsV2 = true; all_required_controllers_enabled = true; for (int i = 0; i < CG_INFO_LENGTH; i++) { - // pids controller is optional. All other controllers are required - if (i != PIDS_IDX) { + // pids and cpuset controllers are optional. All other controllers are required + if (i != PIDS_IDX && i != CPUSET_IDX) { is_cgroupsV2 = is_cgroupsV2 && cg_infos[i]._hierarchy_id == 0; all_required_controllers_enabled = all_required_controllers_enabled && cg_infos[i]._enabled; } diff --git a/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java b/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java index f32e299c206..a820937cb9c 100644 --- a/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java +++ b/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Main.java @@ -27,6 +27,8 @@ import java.io.*; import java.net.UnknownHostException; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.cert.CertPathValidatorException; import java.security.cert.PKIXBuilderParameters; import java.util.*; @@ -217,6 +219,8 @@ public static void main(String args[]) throws Exception { private Throwable chainNotValidatedReason = null; private Throwable tsaChainNotValidatedReason = null; + private List crossChkWarnings = new ArrayList<>(); + PKIXBuilderParameters pkixParameters; Set trustedCerts = new HashSet<>(); @@ -1045,6 +1049,7 @@ void verifyJar(String jarName) } } System.out.println(); + crossCheckEntries(jarName); if (!anySigned) { if (disabledAlgFound) { @@ -1079,6 +1084,143 @@ void verifyJar(String jarName) System.exit(1); } + private void crossCheckEntries(String jarName) throws Exception { + Set locEntries = new HashSet<>(); + + try (JarFile jarFile = new JarFile(jarName); + JarInputStream jis = new JarInputStream( + Files.newInputStream(Path.of(jarName)))) { + + Manifest cenManifest = jarFile.getManifest(); + Manifest locManifest = jis.getManifest(); + compareManifest(cenManifest, locManifest); + + JarEntry locEntry; + while ((locEntry = jis.getNextJarEntry()) != null) { + String entryName = locEntry.getName(); + locEntries.add(entryName); + + JarEntry cenEntry = jarFile.getJarEntry(entryName); + if (cenEntry == null) { + crossChkWarnings.add(String.format(rb.getString( + "entry.1.present.when.reading.jarinputstream.but.missing.via.jarfile"), + entryName)); + continue; + } + + try { + readEntry(jis); + } catch (SecurityException e) { + crossChkWarnings.add(String.format(rb.getString( + "signature.verification.failed.on.entry.1.when.reading.via.jarinputstream"), + entryName)); + continue; + } + + try (InputStream cenInputStream = jarFile.getInputStream(cenEntry)) { + if (cenInputStream == null) { + crossChkWarnings.add(String.format(rb.getString( + "entry.1.present.in.jarfile.but.unreadable"), + entryName)); + continue; + } else { + try { + readEntry(cenInputStream); + } catch (SecurityException e) { + crossChkWarnings.add(String.format(rb.getString( + "signature.verification.failed.on.entry.1.when.reading.via.jarfile"), + entryName)); + continue; + } + } + } + + compareSigners(cenEntry, locEntry); + } + + jarFile.stream() + .map(JarEntry::getName) + .filter(n -> !locEntries.contains(n) && !n.equals(JarFile.MANIFEST_NAME)) + .forEach(n -> crossChkWarnings.add(String.format(rb.getString( + "entry.1.present.when.reading.jarfile.but.missing.via.jarinputstream"), n))); + } + } + + private void readEntry(InputStream is) throws IOException { + is.transferTo(OutputStream.nullOutputStream()); + } + + private void compareManifest(Manifest cenManifest, Manifest locManifest) { + if (cenManifest == null) { + crossChkWarnings.add(rb.getString( + "manifest.missing.when.reading.jarfile")); + return; + } + if (locManifest == null) { + crossChkWarnings.add(rb.getString( + "manifest.missing.when.reading.jarinputstream")); + return; + } + + Attributes cenMainAttrs = cenManifest.getMainAttributes(); + Attributes locMainAttrs = locManifest.getMainAttributes(); + + for (Object key : cenMainAttrs.keySet()) { + Object cenValue = cenMainAttrs.get(key); + Object locValue = locMainAttrs.get(key); + + if (locValue == null) { + crossChkWarnings.add(String.format(rb.getString( + "manifest.attribute.1.present.when.reading.jarfile.but.missing.via.jarinputstream"), + key)); + } else if (!cenValue.equals(locValue)) { + crossChkWarnings.add(String.format(rb.getString( + "manifest.attribute.1.differs.jarfile.value.2.jarinputstream.value.3"), + key, cenValue, locValue)); + } + } + + for (Object key : locMainAttrs.keySet()) { + if (!cenMainAttrs.containsKey(key)) { + crossChkWarnings.add(String.format(rb.getString( + "manifest.attribute.1.present.when.reading.jarinputstream.but.missing.via.jarfile"), + key)); + } + } + } + + private void compareSigners(JarEntry cenEntry, JarEntry locEntry) { + CodeSigner[] cenSigners = cenEntry.getCodeSigners(); + CodeSigner[] locSigners = locEntry.getCodeSigners(); + + boolean cenHasSigners = cenSigners != null; + boolean locHasSigners = locSigners != null; + + if (cenHasSigners && locHasSigners) { + if (!Arrays.equals(cenSigners, locSigners)) { + crossChkWarnings.add(String.format(rb.getString( + "codesigners.different.for.entry.1.when.reading.jarfile.and.jarinputstream"), + cenEntry.getName())); + } + } else if (cenHasSigners) { + crossChkWarnings.add(String.format(rb.getString( + "entry.1.is.signed.in.jarfile.but.is.not.signed.in.jarinputstream"), + cenEntry.getName())); + } else if (locHasSigners) { + crossChkWarnings.add(String.format(rb.getString( + "entry.1.is.signed.in.jarinputstream.but.is.not.signed.in.jarfile"), + locEntry.getName())); + } + } + + private void displayCrossChkWarnings() { + System.out.println(); + // First is a summary warning + System.out.println(rb.getString("jar.contains.internal.inconsistencies.result.in.different.contents.via.jarfile.and.jarinputstream")); + // each warning message with prefix "- " + crossChkWarnings.forEach(warning -> System.out.println("- " + warning)); + } + private void displayMessagesAndResult(boolean isSigning) { String result; List errors = new ArrayList<>(); @@ -1314,6 +1456,9 @@ private void displayMessagesAndResult(boolean isSigning) { System.out.println(rb.getString("Warning.")); warnings.forEach(System.out::println); } + if (!crossChkWarnings.isEmpty()) { + displayCrossChkWarnings(); + } } else { if (!errors.isEmpty() || !warnings.isEmpty()) { System.out.println(); @@ -1321,6 +1466,9 @@ private void displayMessagesAndResult(boolean isSigning) { errors.forEach(System.out::println); warnings.forEach(System.out::println); } + if (!crossChkWarnings.isEmpty()) { + displayCrossChkWarnings(); + } } if (!isSigning && (!errors.isEmpty() || !warnings.isEmpty())) { if (! (verbose != null && showcerts)) { diff --git a/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java b/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java index 6c3d5260bc2..80e81c562a1 100644 --- a/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java +++ b/src/jdk.jartool/share/classes/sun/security/tools/jarsigner/Resources.java @@ -324,6 +324,34 @@ public class Resources extends java.util.ListResourceBundle { {"Cannot.find.environment.variable.", "Cannot find environment variable: "}, {"Cannot.find.file.", "Cannot find file: "}, + {"manifest.missing.when.reading.jarfile", + "Manifest is missing when reading via JarFile"}, + {"manifest.missing.when.reading.jarinputstream", + "Manifest is missing when reading via JarInputStream"}, + {"manifest.attribute.1.present.when.reading.jarfile.but.missing.via.jarinputstream", + "Manifest main attribute %s is present when reading via JarFile but missing when reading via JarInputStream"}, + {"manifest.attribute.1.present.when.reading.jarinputstream.but.missing.via.jarfile", + "Manifest main attribute %s is present when reading via JarInputStream but missing when reading via JarFile"}, + {"manifest.attribute.1.differs.jarfile.value.2.jarinputstream.value.3", + "Manifest main attribute %1$s differs: JarFile value = %2$s, JarInputStream value = %3$s"}, + {"entry.1.present.when.reading.jarinputstream.but.missing.via.jarfile", + "Entry %s is present when reading via JarInputStream but missing when reading via JarFile"}, + {"entry.1.present.when.reading.jarfile.but.missing.via.jarinputstream", + "Entry %s is present when reading via JarFile but missing when reading via JarInputStream"}, + {"entry.1.present.in.jarfile.but.unreadable", + "Entry %s is present in JarFile but unreadable"}, + {"codesigners.different.for.entry.1.when.reading.jarfile.and.jarinputstream", + "Code signers are different for entry %s when reading from JarFile and JarInputStream"}, + {"entry.1.is.signed.in.jarfile.but.is.not.signed.in.jarinputstream", + "Entry %s is signed in JarFile but is not signed in JarInputStream"}, + {"entry.1.is.signed.in.jarinputstream.but.is.not.signed.in.jarfile", + "Entry %s is signed in JarInputStream but is not signed in JarFile"}, + {"jar.contains.internal.inconsistencies.result.in.different.contents.via.jarfile.and.jarinputstream", + "This JAR file contains internal inconsistencies that may result in different contents when reading via JarFile and JarInputStream:"}, + {"signature.verification.failed.on.entry.1.when.reading.via.jarinputstream", + "Signature verification failed on entry %s when reading via JarInputStream"}, + {"signature.verification.failed.on.entry.1.when.reading.via.jarfile", + "Signature verification failed on entry %s when reading via JarFile"}, }; /** diff --git a/src/linux/doc/man/jarsigner.1 b/src/linux/doc/man/jarsigner.1 index ee57c504487..0a64318c40a 100644 --- a/src/linux/doc/man/jarsigner.1 +++ b/src/linux/doc/man/jarsigner.1 @@ -624,6 +624,11 @@ Informational warnings include those that are not errors but regarded as bad pra hasExpiringCert This jar contains entries whose signer certificate will expire within six months\&. .TP +internalInconsistenciesDetected +This jar contains internal inconsistencies detected during verification +that may result in different contents when reading via JarFile +and JarInputStream\&. +.TP noTimestamp This jar contains signatures that does not include a timestamp\&. Without a timestamp, users may not be able to validate this JAR file after the signer certificate\&'s expiration date (\f3YYYY-MM-DD\fR) or after any future revocation date\&. .SH EXAMPLES diff --git a/src/solaris/doc/sun/man/man1/jarsigner.1 b/src/solaris/doc/sun/man/man1/jarsigner.1 index ee57c504487..0a64318c40a 100644 --- a/src/solaris/doc/sun/man/man1/jarsigner.1 +++ b/src/solaris/doc/sun/man/man1/jarsigner.1 @@ -624,6 +624,11 @@ Informational warnings include those that are not errors but regarded as bad pra hasExpiringCert This jar contains entries whose signer certificate will expire within six months\&. .TP +internalInconsistenciesDetected +This jar contains internal inconsistencies detected during verification +that may result in different contents when reading via JarFile +and JarInputStream\&. +.TP noTimestamp This jar contains signatures that does not include a timestamp\&. Without a timestamp, users may not be able to validate this JAR file after the signer certificate\&'s expiration date (\f3YYYY-MM-DD\fR) or after any future revocation date\&. .SH EXAMPLES diff --git a/test/jdk/ProblemList.txt b/test/jdk/ProblemList.txt index 3051f69cccf..cfe012d2582 100644 --- a/test/jdk/ProblemList.txt +++ b/test/jdk/ProblemList.txt @@ -177,14 +177,14 @@ java/awt/GridLayout/LayoutExtraGaps/LayoutExtraGaps.java 8000171 windows-all java/awt/Mouse/GetMousePositionTest/GetMousePositionWithPopup.java 8196017 windows-all java/awt/Scrollbar/ScrollbarMouseWheelTest/ScrollbarMouseWheelTest.java 8196018 windows-all,linux-all java/awt/TrayIcon/ActionCommand/ActionCommand.java 8150540 windows-all -java/awt/TrayIcon/ActionEventMask/ActionEventMask.java 8150540 windows-all +java/awt/TrayIcon/ActionEventMask/ActionEventMask.java 8150540,8295300 windows-all,linux-all java/awt/TrayIcon/ActionEventTest/ActionEventTest.java 8150540,8242801 windows-all,macosx-all -java/awt/TrayIcon/ModalityTest/ModalityTest.java 8150540 windows-all,macosx-all -java/awt/TrayIcon/MouseEventMask/MouseEventMaskTest.java 8150540 windows-all -java/awt/TrayIcon/MouseMovedTest/MouseMovedTest.java 8150540 windows-all -java/awt/TrayIcon/SecurityCheck/FunctionalityCheck/FunctionalityCheck.java 8150540 windows-all -java/awt/TrayIcon/TrayIconEventModifiers/TrayIconEventModifiersTest.java 8150540 windows-all -java/awt/TrayIcon/TrayIconEvents/TrayIconEventsTest.java 8150540 windows-all +java/awt/TrayIcon/ModalityTest/ModalityTest.java 8150540,8295300 windows-all,macosx-all,linux-all +java/awt/TrayIcon/MouseEventMask/MouseEventMaskTest.java 8150540,8295300 windows-all,linux-all +java/awt/TrayIcon/MouseMovedTest/MouseMovedTest.java 8150540,8295300 windows-all,linux-all +java/awt/TrayIcon/SecurityCheck/FunctionalityCheck/FunctionalityCheck.java 8150540,8295300 windows-all,linux-all +java/awt/TrayIcon/TrayIconEventModifiers/TrayIconEventModifiersTest.java 8150540,8295300 windows-all,linux-all +java/awt/TrayIcon/TrayIconEvents/TrayIconEventsTest.java 8150540,8295300 windows-all,linux-all java/awt/TrayIcon/TrayIconMouseTest/TrayIconMouseTest.java 8150540 windows-all java/awt/TrayIcon/TrayIconPopup/TrayIconPopupClickTest.java 8150540 windows-all,macosx-all java/awt/TrayIcon/TrayIconPopup/TrayIconPopupTest.java 8150540 windows-all diff --git a/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetrics.java b/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetrics.java index eda17c09a45..09cd60baabe 100644 --- a/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetrics.java +++ b/test/jdk/jdk/internal/platform/docker/TestDockerMemoryMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2021, 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 @@ -108,6 +108,23 @@ private static void testMemoryLimit(String value, boolean addCgroupMount) throws private static void testMemoryFailCount(String value) throws Exception { Common.logNewTestCase("testMemoryFailCount" + value); + + // Check whether swapping really works for this test + // On some systems there is no swap space enabled. And running + // 'java -Xms{mem-limit} -Xmx{mem-limit} -version' would fail due to swap space size being 0. + DockerRunOptions preOpts = + new DockerRunOptions(imageName, "/jdk/bin/java", "-version"); + preOpts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/") + .addDockerOpts("--memory=" + value) + .addJavaOpts("-Xms" + value) + .addJavaOpts("-Xmx" + value); + OutputAnalyzer oa = DockerTestUtils.dockerRunJava(preOpts); + String output = oa.getOutput(); + if (!output.contains("version")) { + System.out.println("Swapping doesn't work for this test."); + return; + } + DockerRunOptions opts = new DockerRunOptions(imageName, "/jdk/bin/java", "MetricsMemoryTester"); opts.addDockerOpts("--volume", Utils.TEST_CLASSES + ":/test-classes/") @@ -116,7 +133,13 @@ private static void testMemoryFailCount(String value) throws Exception { .addJavaOpts("-cp", "/test-classes/") .addJavaOpts("--add-exports", "java.base/jdk.internal.platform=ALL-UNNAMED") .addClassOptions("failcount"); - DockerTestUtils.dockerRunJava(opts).shouldHaveExitValue(0).shouldContain("TEST PASSED!!!"); + oa = DockerTestUtils.dockerRunJava(opts); + output = oa.getOutput(); + if (output.contains("Ignoring test")) { + System.out.println("Ignored by the tester"); + return; + } + oa.shouldHaveExitValue(0).shouldContain("TEST PASSED!!!"); } private static void testMemoryAndSwapLimit(String memory, String memandswap) throws Exception { diff --git a/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java b/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java new file mode 100644 index 00000000000..e2554ee0f91 --- /dev/null +++ b/test/jdk/sun/security/tools/jarsigner/VerifyJarEntryName.java @@ -0,0 +1,142 @@ +/* + * 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. + * + * 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. + */ + +/* + * @test + * @bug 8339280 + * @summary Test that jarsigner -verify emits a warning when the filename of + * an entry in the LOC is changed + * @library /test/lib + * @run junit VerifyJarEntryName + */ + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.FileOutputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import jdk.test.lib.SecurityTools; +import static org.junit.jupiter.api.Assertions.fail; + +public class VerifyJarEntryName { + + private static final Path ORIGINAL_JAR = Path.of("test.jar"); + private static final Path MODIFIED_JAR = Path.of("modified_test.jar"); + + @BeforeAll + static void setup() throws Exception { + try (FileOutputStream fos = new FileOutputStream(ORIGINAL_JAR.toFile()); + ZipOutputStream zos = new ZipOutputStream(fos)) { + zos.putNextEntry(new ZipEntry(JarFile.MANIFEST_NAME)); + zos.write("Manifest-Version: 1.0\nCreated-By: Test\n". + getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + + // Add hello.txt file + ZipEntry textEntry = new ZipEntry("hello.txt"); + zos.putNextEntry(textEntry); + zos.write("hello".getBytes(StandardCharsets.UTF_8)); + zos.closeEntry(); + } + + SecurityTools.keytool("-genkeypair -keystore ks -storepass changeit " + + "-alias mykey -keyalg rsa -dname CN=me "); + + SecurityTools.jarsigner("-keystore ks -storepass changeit " + + ORIGINAL_JAR + " mykey") + .shouldHaveExitValue(0); + } + + @BeforeEach + void cleanup() throws Exception { + Files.deleteIfExists(MODIFIED_JAR); + } + + /* + * Modify a single byte in "MANIFEST.MF" filename in LOC, and + * validate that jarsigner -verify emits a warning message. + */ + @Test + void verifyManifestEntryName() throws Exception { + modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "META-INF/MANIFEST.MF"); + SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR) + .shouldContain("This JAR file contains internal " + + "inconsistencies that may result in different " + + "contents when reading via JarFile and JarInputStream:") + .shouldContain("- Manifest is missing when " + + "reading via JarInputStream") + .shouldHaveExitValue(0); + } + + /* + * Modify a single byte in signature filename in LOC, and + * validate that jarsigner -verify emits a warning message. + */ + @Test + void verifySignatureEntryName() throws Exception { + modifyJarEntryName(ORIGINAL_JAR, MODIFIED_JAR, "META-INF/MYKEY.SF"); + SecurityTools.jarsigner("-verify -verbose " + MODIFIED_JAR) + .shouldContain("This JAR file contains internal " + + "inconsistencies that may result in different " + + "contents when reading via JarFile and JarInputStream:") + .shouldContain("- Entry XETA-INF/MYKEY.SF is present when reading " + + "via JarInputStream but missing when reading via JarFile") + .shouldHaveExitValue(0); + } + + /* + * Validate that jarsigner -verify on a valid JAR works without + * emitting warnings about internal inconsistencies. + */ + @Test + void verifyOriginalJar() throws Exception { + SecurityTools.jarsigner("-verify -verbose " + ORIGINAL_JAR) + .shouldNotContain("This JAR file contains internal " + + "inconsistencies that may result in different contents when " + + "reading via JarFile and JarInputStream:") + .shouldHaveExitValue(0); + } + + private void modifyJarEntryName(Path origJar, Path modifiedJar, + String entryName) throws Exception { + byte[] jarBytes = Files.readAllBytes(origJar); + byte[] entryNameBytes = entryName.getBytes(StandardCharsets.UTF_8); + int pos = 0; + try { + while (!Arrays.equals(jarBytes, pos, pos + entryNameBytes.length, + entryNameBytes, 0, entryNameBytes.length)) pos++; + } catch (ArrayIndexOutOfBoundsException ignore) { + fail(entryName + " is not present in the JAR"); + } + jarBytes[pos] = 'X'; + Files.write(modifiedJar, jarBytes); + } +}