fix(lib): harden Pack7 long decoding

Bound Pack7 long decoding to ten input bytes and fail closed on
truncated, over-width, and overflowing encodings.

Add boundary, truncation, exact-consumption, and deterministic
bounded-read regression tests.

Closes original audit finding H5.
This commit is contained in:
2026-07-30 22:33:46 +02:00
parent 2d35e61466
commit 87c59ab7fd
2 changed files with 191 additions and 10 deletions

View File

@@ -81,6 +81,16 @@ public final class Util { // NOPMD
private static final int DEFAULT_BUFFER_SIZE = 32 * 1024;
/** Largest unsigned 32-bit value accepted by the packed integer decoder. */
private static final long MAX_PACKED_INTEGER = 0xffff_ffffL;
/** Maximum number of bytes in a packed 64-bit value. */
private static final int MAX_PACKED_LONG_BYTES = 10;
/** One-based position of the high-order packed payload group. */
private static final int PACKED_HIGH_PAYLOAD_POSITION = 1;
/** Largest high-order payload permitted in a ten-byte packed long. */
private static final int MAX_PACKED_LONG_HIGH_PAYLOAD = 1;
/** Bit marking the final byte of a packed value. */
private static final int PACKED_TERMINATION_BIT = 0x80;
/** Mask selecting the seven payload bits of a packed byte. */
private static final int PACKED_PAYLOAD_MASK = 0x7f;
/**
* Private constructor to prevent instantiation of this utility class.
@@ -300,22 +310,49 @@ public final class Util { // NOPMD
/**
* Reads a long value from the input stream using packed 7-bit encoding
* (variable length).
* (variable length). Payload groups are stored most-significant first, and the
* high bit marks the final byte. At most ten bytes are accepted. In a ten-byte
* representation, the first payload group is limited to one bit so that the
* encoded value fits exactly in the 64-bit {@code long} bit pattern.
*
* <p>
* Both non-negative and negative {@code long} values produced by
* {@link #writePack7L(OutputStream, long)} are supported. The method consumes
* exactly one complete packed value and performs at most ten stream reads.
*
* @param in the input stream
* @return the long value read
* @throws IOException if an I/O error occurs or if the stream ends prematurely
* @throws EOFException if the stream ends before a terminating byte
* @throws IOException if an I/O error occurs, the encoding exceeds ten bytes,
* or the payload exceeds 64 bits
*/
public static long readPack7L(final InputStream in) throws IOException {
long result = in.read();
if (result > 0x7f) { // NOPMD
return result & 0x7fL;
long result = 0;
int highPayload = 0;
for (int bytes = PACKED_HIGH_PAYLOAD_POSITION; bytes <= MAX_PACKED_LONG_BYTES; bytes++) {
int current = in.read();
if (current < 0) {
throw new EOFException("read packed long EOF");
}
int payload = current & PACKED_PAYLOAD_MASK;
if (bytes == PACKED_HIGH_PAYLOAD_POSITION) {
highPayload = payload;
}
boolean terminated = (current & PACKED_TERMINATION_BIT) != 0;
if (bytes == MAX_PACKED_LONG_BYTES) {
if (!terminated) {
throw new IOException("packed long exceeds ten bytes");
}
if (highPayload > MAX_PACKED_LONG_HIGH_PAYLOAD) {
throw new IOException("packed long exceeds 64 bits");
}
}
result = (result << 7) | payload;
if (terminated) {
return result;
}
}
int i;
for (i = in.read(); i < 0x80; i = in.read()) {
result = (result << 7) | i;
}
return (result << 7) | (i & 0x7f);
throw new IOException("packed long exceeds ten bytes");
}
/**