diff --git a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java index 10aa0397f0c..1c565a52c0f 100644 --- a/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java +++ b/src/java.base/share/classes/jdk/internal/jimage/BasicImageReader.java @@ -195,10 +195,9 @@ public class BasicImageReader implements AutoCloseable { } if (result.getMajorVersion() != ImageHeader.MAJOR_VERSION || - result.getMinorVersion() != ImageHeader.MINOR_VERSION) { - throw new IOException("The image file \"" + name + "\" is not " + - "the correct version. Major: " + result.getMajorVersion() + - ". Minor: " + result.getMinorVersion()); + result.getMinorVersion() != ImageHeader.MINOR_VERSION) { + throw new ImageVersionMismatchException( + name, result.getMajorVersion(), result.getMinorVersion()); } return result; @@ -447,4 +446,14 @@ public class BasicImageReader implements AutoCloseable { return new ByteArrayInputStream(bytes); } + + public static final class ImageVersionMismatchException extends IOException { + @Deprecated + private static final long serialVersionUID = 1L; + // If needed we could capture major/minor version for use by JImageTask. + ImageVersionMismatchException(String name, int majorVersion, int minorVersion) { + super("The image file \"" + name + "\" is not the correct version. " + + "Major: " + majorVersion + ". Minor: " + minorVersion); + } + } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java b/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java index df2aca02d68..3f324ba1364 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java +++ b/src/jdk.jlink/share/classes/jdk/tools/jimage/JImageTask.java @@ -435,7 +435,10 @@ class JImageTask { } } } catch (IOException ioe) { - throw TASK_HELPER.newBadArgs("err.invalid.jimage", file, ioe.getMessage()); + boolean isVersionMismatch = ioe instanceof BasicImageReader.ImageVersionMismatchException; + // Both messages take the file name and underlying message. + String msgKey = isVersionMismatch ? "err.wrong.version" : "err.invalid.jimage"; + throw TASK_HELPER.newBadArgs(msgKey, file, ioe.getMessage()); } } } diff --git a/src/jdk.jlink/share/classes/jdk/tools/jimage/resources/jimage.properties b/src/jdk.jlink/share/classes/jdk/tools/jimage/resources/jimage.properties index ac13505a0d9..3038dfcc5ec 100644 --- a/src/jdk.jlink/share/classes/jdk/tools/jimage/resources/jimage.properties +++ b/src/jdk.jlink/share/classes/jdk/tools/jimage/resources/jimage.properties @@ -89,15 +89,20 @@ main.opt.footer=\ \ glob:\n\ \ regex: - - err.not.a.task=task must be one of : {0} err.missing.arg=no value given for {0} err.ambiguous.arg=value for option {0} starts with \"--\" should use {0}= format err.not.a.dir=not a directory: {0} err.not.a.jimage=not a jimage file: {0} -err.invalid.jimage=Unable to open {0}: {1} err.no.jimage=no jimage provided err.option.unsupported={0} not supported: {1} err.unknown.option=unknown option: {0} err.cannot.create.dir=cannot create directory {0} + +# General failure to open a jimage file. +# {0} = path of jimage file, {1} = underlying error message +err.invalid.jimage=Unable to open {0}: {1} +# More specific alternative for cases of version mismatch +err.wrong.version=Unable to open {0}: mismatched file and tool version\n\ +Use ''/bin/jimage'' for the JDK associated with the jimage file:\n\ +{1} diff --git a/test/jdk/tools/jimage/JImageBadFileTest.java b/test/jdk/tools/jimage/JImageBadFileTest.java new file mode 100644 index 00000000000..26795f68d16 --- /dev/null +++ b/test/jdk/tools/jimage/JImageBadFileTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 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 java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.IntBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Consumer; + +import static java.nio.file.StandardOpenOption.READ; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.util.regex.Pattern.quote; + +/* + * @test + * @summary Tests to verify behavior for "invalid" jimage files + * @library /test/lib + * @modules jdk.jlink/jdk.tools.jimage + * @build jdk.test.lib.Asserts + * @run main JImageBadFileTest + */ +public class JImageBadFileTest extends JImageCliTest { + // src/java.base/share/native/libjimage/imageFile.hpp + // + // 31 -------- bits -------- 0 + // IDX +-------------------------+ + // 0 | Magic (0xCAFEDADA) | + // +------------+------------+ + // 1 | Major Vers | Minor Vers | + // +------------+------------+ + // 2 | Flags | + // +-------------------------+ + // 3 | Resource Count | + // +-------------------------+ + // 4 | Table Length | + // +-------------------------+ + // 5 | Attributes Size | + // +-------------------------+ + // 6 | Strings Size | + // +-------------------------+ + private static final int HEADER_SIZE_BYTES = 7 * 4; + + /** + * Helper to copy the default jimage file for the runtime under test and + * allow it to be corrupted in various ways. + * + * @param label label for the temporary file (arbitrary debug name) + * @param maxLen maximum number of bytes to copy (-1 to copy all) + * @param headerFn function which may corrupt specific header values + * @return the path of a temporary jimage file in the test directory containing + * the possibly corrupted jimage file (caller should delete) + */ + private Path writeModifiedJimage(String label, int maxLen, Consumer headerFn) + throws IOException { + int remaining = maxLen >= 0 ? maxLen : Integer.MAX_VALUE; + Path dst = Files.createTempFile(Path.of("."), "modules-" + label, ""); + try (InputStream rest = Files.newInputStream(Path.of(getImagePath()), READ); + OutputStream out = Files.newOutputStream(dst, TRUNCATE_EXISTING)) { + ByteBuffer bytes = ByteBuffer.wrap(rest.readNBytes(HEADER_SIZE_BYTES)); + bytes.order(ByteOrder.nativeOrder()); + headerFn.accept(bytes.asIntBuffer()); + int headerSize = Math.min(remaining, HEADER_SIZE_BYTES); + out.write(bytes.array(), 0, headerSize); + remaining -= headerSize; + if (remaining > 0) { + byte[] block = new byte[8192]; + do { + int copySize = Math.min(remaining, block.length); + out.write(block, 0, rest.readNBytes(block, 0, copySize)); + remaining -= copySize; + } while (rest.available() > 0 && remaining > 0); + } + return dst.toAbsolutePath(); + } catch (IOException e) { + Files.deleteIfExists(dst); + throw e; + } + } + + public void testBadMagicNumber() throws IOException { + // Flip some bits in the magic number. + Path tempJimage = writeModifiedJimage("bad_magic", -1, b -> b.put(0, b.get(1) ^ 0x1010)); + try { + JImageResult result = jimage("info", tempJimage.toString()); + result.assertShowsError(); + assertMatches(quote("Unable to open"), result.output); + assertMatches(quote("is not an image file"), result.output); + } finally { + Files.delete(tempJimage); + } + } + + public void testMismatchedVersion() throws IOException { + // Add one to minor version (lowest bits). + Path tempJimage = writeModifiedJimage("bad_version", -1, b -> b.put(1, b.get(1) + 1)); + try { + JImageResult result = jimage("info", tempJimage.toString()); + result.assertShowsError(); + assertMatches(quote("Unable to open"), result.output); + assertMatches(quote("/bin/jimage"), result.output); + assertMatches(quote("not the correct version"), result.output); + assertMatches("Major: \\d+", result.output); + assertMatches("Minor: \\d+", result.output); + } finally { + Files.delete(tempJimage); + } + } + + public void testTruncatedHeader() throws IOException { + // Copy less than the header. + Path tempJimage = writeModifiedJimage("truncated_header", HEADER_SIZE_BYTES - 4, b -> {}); + try { + JImageResult result = jimage("info", tempJimage.toString()); + result.assertShowsError(); + assertMatches(quote("Unable to open"), result.output); + assertMatches(quote("is not an image file"), result.output); + } finally { + Files.delete(tempJimage); + } + } + + public void testTruncatedData() throws IOException { + // Copy more than the header, but definitely less than the whole file. + Path tempJimage = writeModifiedJimage("truncated_data", HEADER_SIZE_BYTES + 1024, b -> {}); + try { + JImageResult result = jimage("info", tempJimage.toString()); + result.assertShowsError(); + assertMatches(quote("Unable to open"), result.output); + assertMatches("image file \".*\" is corrupted", result.output); + } finally { + Files.delete(tempJimage); + } + } + + public void testGoodFileCopy() throws IOException { + // Self test that the file copying isn't itself corrupting anything. + Path tempJimage = writeModifiedJimage("good_file", -1, b -> {}); + try { + jimage("info", tempJimage.toString()).assertSuccess(); + } finally { + Files.delete(tempJimage); + } + } + + public static void main(String[] args) throws Throwable { + new JImageBadFileTest().runTests(); + } +}