8302514: Misleading error generated when empty class file encountered

Reviewed-by: vromero, jwaters
This commit is contained in:
Archie L. Cobbs 2023-02-16 14:48:39 +00:00 committed by Julian Waters
parent 3cc459b6c2
commit a58fa6e73e
10 changed files with 351 additions and 45 deletions

View File

@ -62,6 +62,7 @@ import com.sun.tools.javac.main.Option;
import com.sun.tools.javac.resources.CompilerProperties.Fragments;
import com.sun.tools.javac.resources.CompilerProperties.Warnings;
import com.sun.tools.javac.util.*;
import com.sun.tools.javac.util.ByteBuffer.UnderflowException;
import com.sun.tools.javac.util.DefinedBy.Api;
import com.sun.tools.javac.util.JCDiagnostic.DiagnosticPosition;
@ -329,7 +330,12 @@ public class ClassReader {
/** Read a character.
*/
char nextChar() {
char res = buf.getChar(bp);
char res;
try {
res = buf.getChar(bp);
} catch (UnderflowException e) {
throw badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
bp += 2;
return res;
}
@ -337,13 +343,22 @@ public class ClassReader {
/** Read a byte.
*/
int nextByte() {
return buf.getByte(bp++) & 0xFF;
try {
return buf.getByte(bp++) & 0xFF;
} catch (UnderflowException e) {
throw badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
}
/** Read an integer.
*/
int nextInt() {
int res = buf.getInt(bp);
int res;
try {
res = buf.getInt(bp);
} catch (UnderflowException e) {
throw badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
bp += 4;
return res;
}
@ -1482,7 +1497,12 @@ public class ClassReader {
/** Read parameter annotations.
*/
void readParameterAnnotations(Symbol meth) {
int numParameters = buf.getByte(bp++) & 0xFF;
int numParameters;
try {
numParameters = buf.getByte(bp++) & 0xFF;
} catch (UnderflowException e) {
throw badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
if (parameterAnnotations == null) {
parameterAnnotations = new ParameterAnnotations[numParameters];
} else if (parameterAnnotations.length != numParameters) {
@ -1771,7 +1791,12 @@ public class ClassReader {
}
Attribute readAttributeValue() {
char c = (char) buf.getByte(bp++);
char c;
try {
c = (char)buf.getByte(bp++);
} catch (UnderflowException e) {
throw badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
switch (c) {
case 'B':
return new Attribute.Constant(syms.byteType, poolReader.getConstant(nextChar()));

View File

@ -25,6 +25,7 @@
package com.sun.tools.javac.jvm;
import com.sun.tools.javac.util.ByteBuffer;
import com.sun.tools.javac.util.ByteBuffer.UnderflowException;
import com.sun.tools.javac.util.Convert;
import com.sun.tools.javac.util.Name.NameMapper;
@ -132,16 +133,26 @@ public class ModuleNameReader {
/** Read a character.
*/
char nextChar() {
char res = buf.getChar(bp);
char nextChar() throws BadClassFile {
char res;
try {
res = buf.getChar(bp);
} catch (UnderflowException e) {
throw new BadClassFile("class file truncated at offset " + e.getLength());
}
bp += 2;
return res;
}
/** Read an integer.
*/
int nextInt() {
int res = buf.getInt(bp);
int nextInt() throws BadClassFile {
int res;
try {
res = buf.getInt(bp);
} catch (UnderflowException e) {
throw new BadClassFile("class file truncated at offset " + e.getLength());
}
bp += 4;
return res;
}

View File

@ -32,6 +32,7 @@ import com.sun.tools.javac.code.Symtab;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.jvm.PoolConstant.NameAndType;
import com.sun.tools.javac.util.ByteBuffer;
import com.sun.tools.javac.util.ByteBuffer.UnderflowException;
import com.sun.tools.javac.util.Name;
import com.sun.tools.javac.util.Name.NameMapper;
import com.sun.tools.javac.util.Names;
@ -118,21 +119,30 @@ public class PoolReader {
* Get class name without resolving
*/
<Z> Z peekClassName(int index, NameMapper<Z> mapper) {
return peekName(buf.getChar(pool.offset(index)), mapper);
return peekItemName(index, mapper);
}
/**
* Get package name without resolving
*/
<Z> Z peekPackageName(int index, NameMapper<Z> mapper) {
return peekName(buf.getChar(pool.offset(index)), mapper);
return peekItemName(index, mapper);
}
/**
* Get module name without resolving
*/
<Z> Z peekModuleName(int index, NameMapper<Z> mapper) {
return peekName(buf.getChar(pool.offset(index)), mapper);
return peekItemName(index, mapper);
}
private <Z> Z peekItemName(int index, NameMapper<Z> mapper) {
try {
index = buf.getChar(pool.offset(index));
} catch (UnderflowException e) {
throw reader.badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
return peekName(index, mapper);
}
/**
@ -153,7 +163,11 @@ public class PoolReader {
* Peek a name from the pool at given index without resolving.
*/
<Z> Z peekName(int index, Name.NameMapper<Z> mapper) {
return getUtf8(index, mapper);
try {
return getUtf8(index, mapper);
} catch (UnderflowException e) {
throw reader.badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
}
/**
@ -188,7 +202,7 @@ public class PoolReader {
return pool.tag(index) == tag;
}
private <Z> Z getUtf8(int index, NameMapper<Z> mapper) {
private <Z> Z getUtf8(int index, NameMapper<Z> mapper) throws UnderflowException {
int tag = pool.tag(index);
int offset = pool.offset(index);
if (tag == CONSTANT_Utf8) {
@ -201,7 +215,7 @@ public class PoolReader {
}
}
private Object resolve(ByteBuffer poolbuf, int tag, int offset) {
private Object resolve(ByteBuffer poolbuf, int tag, int offset) throws UnderflowException {
switch (tag) {
case CONSTANT_Utf8: {
int len = poolbuf.getChar(offset);
@ -250,6 +264,14 @@ public class PoolReader {
* {@link PoolReader#peekClassName(int, NameMapper)}.
*/
int readPool(ByteBuffer poolbuf, int offset) {
try {
return readPoolInternal(poolbuf, offset);
} catch (UnderflowException e) {
throw reader.badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
}
private int readPoolInternal(ByteBuffer poolbuf, int offset) throws UnderflowException {
int poolSize = poolbuf.getChar(offset);
int index = 1;
offset += 2;
@ -343,7 +365,12 @@ public class PoolReader {
if (!expectedTags.get(currentTag)) {
throw reader.badClassFile("unexpected.const.pool.tag.at", tag(index), offset(index));
}
P p = (P)resolve(poolbuf, tag(index), offset(index));
P p;
try {
p = (P)resolve(poolbuf, tag(index), offset(index));
} catch (UnderflowException e) {
throw reader.badClassFile("bad.class.truncated.at.offset", Integer.toString(e.getLength()));
}
values[index] = p;
return p;
}

View File

@ -2431,6 +2431,9 @@ compiler.misc.bad.const.pool.tag.at=\
compiler.misc.unexpected.const.pool.tag.at=\
unexpected constant pool tag: {0} at {1}
compiler.misc.bad.class.truncated.at.offset=\
class file truncated at offset {0}
compiler.misc.bad.signature=\
bad signature: {0}

View File

@ -35,12 +35,30 @@ import java.lang.reflect.Array;
public class ArrayUtils {
private static int calculateNewLength(int currentLength, int maxIndex) {
while (currentLength < maxIndex + 1)
if (maxIndex == Integer.MAX_VALUE)
maxIndex--; // avoid negative overflow
while (currentLength < maxIndex + 1) {
currentLength = currentLength * 2;
if (currentLength <= 0) { // avoid infinite loop and negative overflow
currentLength = maxIndex + 1;
break;
}
}
return currentLength;
}
/**
* Ensure the given array has length at least {@code maxIndex + 1}.
*
* @param array original array
* @param maxIndex exclusive lower bound for desired length
* @return possibly reallocated array of length at least {@code maxIndex + 1}
* @throws NullPointerException if {@code array} is null
* @throws IllegalArgumentException if {@code maxIndex} is negative
*/
public static <T> T[] ensureCapacity(T[] array, int maxIndex) {
if (maxIndex < 0)
throw new IllegalArgumentException("maxIndex=" + maxIndex);
if (maxIndex < array.length) {
return array;
} else {
@ -52,7 +70,18 @@ public class ArrayUtils {
}
}
/**
* Ensure the given array has length at least {@code maxIndex + 1}.
*
* @param array original array
* @param maxIndex exclusive lower bound for desired length
* @return possibly reallocated array of length at least {@code maxIndex + 1}
* @throws NullPointerException if {@code array} is null
* @throws IllegalArgumentException if {@code maxIndex} is negative
*/
public static byte[] ensureCapacity(byte[] array, int maxIndex) {
if (maxIndex < 0)
throw new IllegalArgumentException("maxIndex=" + maxIndex);
if (maxIndex < array.length) {
return array;
} else {
@ -63,7 +92,18 @@ public class ArrayUtils {
}
}
/**
* Ensure the given array has length at least {@code maxIndex + 1}.
*
* @param array original array
* @param maxIndex exclusive lower bound for desired length
* @return possibly reallocated array of length at least {@code maxIndex + 1}
* @throws NullPointerException if {@code array} is null
* @throws IllegalArgumentException if {@code maxIndex} is negative
*/
public static char[] ensureCapacity(char[] array, int maxIndex) {
if (maxIndex < 0)
throw new IllegalArgumentException("maxIndex=" + maxIndex);
if (maxIndex < array.length) {
return array;
} else {
@ -74,7 +114,18 @@ public class ArrayUtils {
}
}
/**
* Ensure the given array has length at least {@code maxIndex + 1}.
*
* @param array original array
* @param maxIndex exclusive lower bound for desired length
* @return possibly reallocated array of length at least {@code maxIndex + 1}
* @throws NullPointerException if {@code array} is null
* @throws IllegalArgumentException if {@code maxIndex} is negative
*/
public static int[] ensureCapacity(int[] array, int maxIndex) {
if (maxIndex < 0)
throw new IllegalArgumentException("maxIndex=" + maxIndex);
if (maxIndex < array.length) {
return array;
} else {

View File

@ -147,35 +147,42 @@ public class ByteBuffer {
appendBytes(name.getByteArray(), name.getByteOffset(), name.getByteLength());
}
/** Append the content of a given input stream.
/** Append the content of a given input stream, and then close the stream.
*/
public void appendStream(InputStream is) throws IOException {
try {
int start = length;
int initialSize = is.available();
elems = ArrayUtils.ensureCapacity(elems, length + initialSize);
int r = is.read(elems, start, initialSize);
int bp = start;
while (r != -1) {
bp += r;
elems = ArrayUtils.ensureCapacity(elems, bp);
r = is.read(elems, bp, elems.length - bp);
}
} finally {
try {
is.close();
} catch (IOException e) {
/* Ignore any errors, as this stream may have already
* thrown a related exception which is the one that
* should be reported.
*/
try (InputStream input = is) {
while (true) {
// Read another chunk of data, using size hint from available().
// If available() is accurate, the array size should be just right.
int amountToRead = Math.max(input.available(), 64);
elems = ArrayUtils.ensureCapacity(elems, length + amountToRead);
int amountRead = input.read(elems, length, amountToRead);
if (amountRead == -1)
break;
length += amountRead;
// Check for the common case where input.available() returned the
// entire remaining input; in that case, avoid an extra array extension.
// Note we are guaranteed that elems.length >= length + 1 at this point.
if (amountRead == amountToRead) {
int byt = input.read();
if (byt == -1)
break;
elems[length++] = (byte)byt;
}
}
}
}
/** Extract an integer at position bp from elems.
*
* @param bp starting offset
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code bp} is negative
*/
public int getInt(int bp) {
public int getInt(int bp) throws UnderflowException {
verifyRange(bp, 4);
return
((elems[bp] & 0xFF) << 24) +
((elems[bp+1] & 0xFF) << 16) +
@ -185,8 +192,13 @@ public class ByteBuffer {
/** Extract a long integer at position bp from elems.
*
* @param bp starting offset
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code bp} is negative
*/
public long getLong(int bp) {
public long getLong(int bp) throws UnderflowException {
verifyRange(bp, 8);
DataInputStream elemsin =
new DataInputStream(new ByteArrayInputStream(elems, bp, 8));
try {
@ -197,8 +209,13 @@ public class ByteBuffer {
}
/** Extract a float at position bp from elems.
*
* @param bp starting offset
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code bp} is negative
*/
public float getFloat(int bp) {
public float getFloat(int bp) throws UnderflowException {
verifyRange(bp, 4);
DataInputStream elemsin =
new DataInputStream(new ByteArrayInputStream(elems, bp, 4));
try {
@ -209,8 +226,13 @@ public class ByteBuffer {
}
/** Extract a double at position bp from elems.
*
* @param bp starting offset
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code bp} is negative
*/
public double getDouble(int bp) {
public double getDouble(int bp) throws UnderflowException {
verifyRange(bp, 8);
DataInputStream elemsin =
new DataInputStream(new ByteArrayInputStream(elems, bp, 8));
try {
@ -221,13 +243,25 @@ public class ByteBuffer {
}
/** Extract a character at position bp from elems.
*
* @param bp starting offset
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code bp} is negative
*/
public char getChar(int bp) {
public char getChar(int bp) throws UnderflowException {
verifyRange(bp, 2);
return
(char)(((elems[bp] & 0xFF) << 8) + (elems[bp+1] & 0xFF));
}
public byte getByte(int bp) {
/** Extract a byte at position bp from elems.
*
* @param bp starting offset
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code bp} is negative
*/
public byte getByte(int bp) throws UnderflowException {
verifyRange(bp, 1);
return elems[bp];
}
@ -242,4 +276,39 @@ public class ByteBuffer {
public Name toName(Names names) {
return names.fromUtf(elems, 0, length);
}
/** Verify there are at least the specified number of bytes in this buffer at the specified offset.
*
* @param off starting offset
* @param len required length
* @throws UnderflowException if there is not enough data in this buffer
* @throws IllegalArgumentException if {@code off} or {@code len} is negative
*/
public void verifyRange(int off, int len) throws UnderflowException {
if (off < 0 || len < 0)
throw new IllegalArgumentException("off=" + off + ", len=" + len);
if (off + len < 0 || off + len > length)
throw new UnderflowException(length);
}
// UnderflowException
/** Thrown when trying to read past the end of the buffer.
*/
public static class UnderflowException extends Exception {
private static final long serialVersionUID = 0;
private final int length;
public UnderflowException(int length) {
this.length = length;
}
/** Get the length of the buffer, which apparently is not long enough.
*/
public int getLength() {
return length;
}
}
}

View File

@ -0,0 +1,119 @@
/*
* Copyright (c) 2023, 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 8302514
* @summary Verify truncated class files are detected and reported as truncated
*/
import com.sun.tools.javac.Main;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.file.Files;
public class TruncatedClassFileTest {
// We want a bunch of stuff in this file to make the classfile complicated
private static final String A_SOURCE = """
public class ClassA {
public static final boolean z = true;
public static final byte b = 7;
public static final char c = '*';
public static final short s = -12;
public static final int i = 123;
public static final float f = 123f;
public static final long j = 0x1234567812345678L;
public static final double d = Math.PI;
public static final String str = new String("test123");
@SuppressWarnings("blah")
public ClassA() {
new Thread();
}
}
""";
// This file will get compiled against a trunctated version of A.class
private static final String B_SOURCE = """
public class ClassB {
public ClassB() {
new ClassA();
}
}
""";
private static final File A_SOURCE_FILE = new File("ClassA.java");
private static final File B_SOURCE_FILE = new File("ClassB.java");
private static final File A_CLASS_FILE = new File("ClassA.class");
private static void createSourceFile(File file, String content) throws IOException {
try (PrintStream output = new PrintStream(new FileOutputStream(file))) {
output.println(content);
}
}
public static void main(String... args) throws Exception {
// Create A.java and B.java
createSourceFile(A_SOURCE_FILE, A_SOURCE);
createSourceFile(B_SOURCE_FILE, B_SOURCE);
// Compile A.java
createSourceFile(A_SOURCE_FILE, A_SOURCE);
int ret = Main.compile(new String[] { A_SOURCE_FILE.toString() });
if (ret != 0)
throw new AssertionError("compilation of " + A_SOURCE_FILE + " failed");
A_SOURCE_FILE.delete();
// Read A.class
final byte[] classfile = Files.readAllBytes(A_CLASS_FILE.toPath());
// Now compile B.java with truncated versions of A.class
for (int length = 0; length < classfile.length; length++) {
// Write out truncated class file A.class
try (FileOutputStream output = new FileOutputStream(A_CLASS_FILE)) {
output.write(classfile, 0, length);
}
// Try to compile file B.java
final StringWriter diags = new StringWriter();
final String[] params = new String[] {
"-classpath",
".",
"-XDrawDiagnostics",
B_SOURCE_FILE.toString()
};
ret = Main.compile(params, new PrintWriter(diags, true));
if (ret == 0)
throw new AssertionError("compilation with truncated class file (" + length + ") succeeded?");
final String errmsg = "compiler.misc.bad.class.truncated.at.offset: " + length;
if (!diags.toString().contains(errmsg))
throw new AssertionError("error message not found for truncated class file (" + length + "): " + diags);
}
}
}

View File

@ -45,6 +45,7 @@ compiler.err.type.var.more.than.once # UNUSED
compiler.err.type.var.more.than.once.in.result # UNUSED
compiler.err.unexpected.type
compiler.misc.bad.class.signature # bad class file
compiler.misc.bad.class.truncated.at.offset # bad class file
compiler.misc.bad.const.pool.tag # bad class file
compiler.misc.bad.const.pool.tag.at # bad class file
compiler.misc.unexpected.const.pool.tag.at # bad class file

View File

@ -381,7 +381,7 @@ public class EdgeCases extends ModuleTestBase {
.getOutputLines(OutputKind.DIRECT);
List<String> expected = Arrays.asList(
"- compiler.err.cant.access: <error>.module-info, (compiler.misc.bad.class.file.header: module-info.class, (compiler.misc.illegal.start.of.class.file))",
"- compiler.err.cant.access: <error>.module-info, (compiler.misc.bad.class.file.header: module-info.class, (compiler.misc.bad.class.truncated.at.offset: 0))",
"1 error");
if (!expected.equals(log)) {

View File

@ -102,7 +102,7 @@ public class NoAbortForBadClassFile extends TestRunner {
.getOutputLines(Task.OutputKind.DIRECT);
List<String> expectedOut = Arrays.asList(
"Test.java:1:57: compiler.err.cant.access: test.Broken, (compiler.misc.bad.class.file.header: Broken.class, (compiler.misc.class.file.wrong.class: java.lang.AutoCloseable))",
"Test.java:1:57: compiler.err.cant.access: test.Broken, (compiler.misc.bad.class.file.header: Broken.class, (compiler.misc.bad.class.truncated.at.offset: 0))",
"Test.java:1:73: compiler.err.cant.resolve.location.args: kindname.method, unknown, , , (compiler.misc.location: kindname.class, java.lang.String, null)",
"2 errors"
);