java-topology/test/jdk/java/net/httpclient/quic/QuicFramesDecoderTest.java
russell@unturf.com 0a580b313d undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com

Patches, unit tests, benchmarks, whitepaper, and outreach briefs.
Public domain — no copyright claimed. Use freely.
2026-03-26 17:11:57 -04:00

300 lines
15 KiB
Java

/*
* Copyright (c) 2024, 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 jdk.internal.net.http.quic.frames.QuicFrame;
import jdk.internal.net.quic.QuicTransportErrors;
import jdk.internal.net.quic.QuicTransportException;
import java.nio.ByteBuffer;
import java.util.HexFormat;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
/*
* @test
* @library /test/lib
* @summary Tests to check QUIC frame decoding errors are handled correctly
* @run junit/othervm QuicFramesDecoderTest
*/
public class QuicFramesDecoderTest {
// correct frames. Single byte frames (padding, ping, handshake_done) omitted.
// ACK without ECN, acked = 0, 2; delay = 2
private static final byte[] ACK_BASE = HexFormat.of().parseHex("02020201000000");
private static final byte[] ACK_BASE_B = HexFormat.of().parseHex("0202020100004000");
// ACK with ECN, acked = 0, 2; delay = 2, ECN=(3,4,5)
private static final byte[] ACK_ECN = HexFormat.of().parseHex("03020201000000030405");
private static final byte[] ACK_ECN_B = HexFormat.of().parseHex("0302020100000003044005");
// RESET_STREAM, stream 3, error 2, final size 1
private static final byte[] RESET_STREAM = HexFormat.of().parseHex("04030201");
private static final byte[] RESET_STREAM_B = HexFormat.of().parseHex("0403024001");
// STOP_SENDING, stream 4, error 3
private static final byte[] STOP_SENDING = HexFormat.of().parseHex("050403");
private static final byte[] STOP_SENDING_B = HexFormat.of().parseHex("05044003");
// CRYPTO, offset 5, length 4, data
private static final byte[] CRYPTO = HexFormat.of().parseHex("06050403020100");
// NEW_TOKEN, length 6, data
private static final byte[] NEW_TOKEN = HexFormat.of().parseHex("0706050403020100");
// STREAM-o-l-f, stream 7, no data
private static final byte[] STREAM = HexFormat.of().parseHex("0807");
// STREAM-o-l+f, stream 8, no data
private static final byte[] STREAM_F = HexFormat.of().parseHex("0908");
// STREAM-o+l-f, stream 9, length 8
private static final byte[] STREAM_L = HexFormat.of().parseHex("0a09080706050403020100");
// STREAM-o+l+f, stream 10, length 9
private static final byte[] STREAM_LF = HexFormat.of().parseHex("0b0a09080706050403020100");
// STREAM+o-l-f, stream 11, offset 0, no data
private static final byte[] STREAM_O = HexFormat.of().parseHex("0c000a");
// STREAM+o-l+f, stream 12, offset 0, no data
private static final byte[] STREAM_OF = HexFormat.of().parseHex("0d000b");
// STREAM+o+l-f, stream 13, offset 0, length 3
private static final byte[] STREAM_OL = HexFormat.of().parseHex("0e000c03020100");
// STREAM+o+l+f, stream 14, offset 0, length 3
private static final byte[] STREAM_OLF = HexFormat.of().parseHex("0f000d03020100");
// MAX_DATA, max=15
private static final byte[] MAX_DATA = HexFormat.of().parseHex("100f");
private static final byte[] MAX_DATA_B = HexFormat.of().parseHex("10400f");
// MAX_STREAM_DATA, stream = 16 max=15
private static final byte[] MAX_STREAM_DATA = HexFormat.of().parseHex("11100f");
private static final byte[] MAX_STREAM_DATA_B = HexFormat.of().parseHex("1110400f");
// MAX_STREAMS, bidi, streams = 2^60
private static final byte[] MAX_STREAMS_B = HexFormat.of().parseHex("12d000000000000000");
// MAX_STREAMS, uni, streams = 2^60
private static final byte[] MAX_STREAMS_U = HexFormat.of().parseHex("13d000000000000000");
// DATA_BLOCKED, max=19
private static final byte[] DATA_BLOCKED = HexFormat.of().parseHex("1413");
private static final byte[] DATA_BLOCKED_B = HexFormat.of().parseHex("144013");
// STREAM_DATA_BLOCKED, stream = 20 max=19
private static final byte[] STREAM_DATA_BLOCKED = HexFormat.of().parseHex("151413");
private static final byte[] STREAM_DATA_BLOCKED_B = HexFormat.of().parseHex("15144013");
// STREAMS_BLOCKED, bidi, streams = 2^60
private static final byte[] STREAMS_BLOCKED_B = HexFormat.of().parseHex("16d000000000000000");
// STREAMS_BLOCKED, uni, streams = 2^60
private static final byte[] STREAMS_BLOCKED_U = HexFormat.of().parseHex("17d000000000000000");
// NEW_CONNECTION_ID, seq=23, retire=22, len = 5
private static final byte[] NEW_CONNECTION_ID = HexFormat.of().parseHex("181716"+
"051413121110"+"0f0e0d0c0b0a09080706050403020100");
// RETIRE_CONNECTION_ID, seq=24
private static final byte[] RETIRE_CONNECTION_ID = HexFormat.of().parseHex("1918");
private static final byte[] RETIRE_CONNECTION_ID_B = HexFormat.of().parseHex("194018");
// PATH_CHALLENGE
private static final byte[] PATH_CHALLENGE = HexFormat.of().parseHex("1a0706050403020100");
// PATH_RESPONSE
private static final byte[] PATH_RESPONSE = HexFormat.of().parseHex("1b0706050403020100");
// CONNECTION_CLOSE, quic, error 27, frame type 26, reason='\0'
private static final byte[] CONNECTION_CLOSE_Q = HexFormat.of().parseHex("1c1b1a0100");
// CONNECTION_CLOSE, quic, error 27, frame type 26, reason=
// efbfbf (U+FFFF) - "not a valid unicode character
// edb080 (U+DC00) - low surrogate, prohibited in UTF8 (RFC3629), must be preceded by high surrogate otherwise
// eda080 (U+D800) - high surrogate, prohibited in UTF8 (RFC3629), must be followed by low surrogate otherwise
// 80 - not a first byte of UTF8 sequence
// c0d0e0f0ff - not a valid UTF8 sequence
private static final byte[] CONNECTION_CLOSE_Q_BAD_REASON = HexFormat.of().parseHex("1c1b1a0fefbfbfedb080eda08080c0d0e0f0ff");
// CONNECTION_CLOSE, app, error 28, reason='\0'
private static final byte[] CONNECTION_CLOSE_A = HexFormat.of().parseHex("1d1c0100");
// CONNECTION_CLOSE, app, error 28, reason= same as CONNECTION_CLOSE_Q_BAD_REASON
private static final byte[] CONNECTION_CLOSE_A_BAD_REASON = HexFormat.of().parseHex("1d1c0fefbfbfedb080eda08080c0d0e0f0ff");
// end of correct frames
// malformed frames other than truncated
// ACK acknowledging negative packet
// ACK without ECN, acked = -1, 1; delay = 2
private static final byte[] ACK_NEG_BASE = HexFormat.of().parseHex("02010201000000");
// ACK with ECN, acked = -1, 1; delay = 2, ECN=(3,4,5)
private static final byte[] ACK_NEG_ECN = HexFormat.of().parseHex("03010201000000030405");
// ACK without ECN, acked = -1, 0; delay = 2
private static final byte[] ACK_NEG_BASE_2 = HexFormat.of().parseHex("0200020001");
// CRYPTO out of range: offset MAX_VL_INT, len=1
private static final byte[] CRYPTO_OOR = HexFormat.of().parseHex("06ffffffffffffffff0100");
// NEW_TOKEN empty
private static final byte[] NEW_TOKEN_EMPTY = HexFormat.of().parseHex("0700");
// MAX_STREAMS out of range
// MAX_STREAMS, bidi, streams = 2^60+1
private static final byte[] MAX_STREAMS_B_OOR = HexFormat.of().parseHex("12d000000000000001");
// MAX_STREAMS, uni, streams = 2^60+1
private static final byte[] MAX_STREAMS_U_OOR = HexFormat.of().parseHex("13d000000000000001");
// STREAMS_BLOCKED out of range
// STREAMS_BLOCKED, bidi, streams = 2^60+1
private static final byte[] STREAMS_BLOCKED_B_OOR = HexFormat.of().parseHex("16d000000000000001");
// STREAMS_BLOCKED, uni, streams = 2^60+1
private static final byte[] STREAMS_BLOCKED_U_OOR = HexFormat.of().parseHex("17d000000000000001");
// NEW_CONNECTION_ID, seq=23, retire=22, len = 0
private static final byte[] NEW_CONNECTION_ID_ZERO = HexFormat.of().parseHex("181716"+
"00"+"0f0e0d0c0b0a09080706050403020100");
public static Object[][] goodFrames() {
return new Object[][]{
new Object[]{"ack without ecn", ACK_BASE, false},
new Object[]{"ack without ecn", ACK_BASE_B, true},
new Object[]{"ack with ecn", ACK_ECN, false},
new Object[]{"ack with ecn", ACK_ECN_B, true},
new Object[]{"RESET_STREAM", RESET_STREAM, false},
new Object[]{"RESET_STREAM", RESET_STREAM_B, true},
new Object[]{"STOP_SENDING", STOP_SENDING, false},
new Object[]{"STOP_SENDING", STOP_SENDING_B, true},
new Object[]{"CRYPTO", CRYPTO, false},
new Object[]{"NEW_TOKEN", NEW_TOKEN, false},
new Object[]{"STREAM-o-l-f", STREAM, false},
new Object[]{"STREAM-o-l+f", STREAM_F, false},
new Object[]{"STREAM-o+l-f", STREAM_L, false},
new Object[]{"STREAM-o+l+f", STREAM_LF, false},
new Object[]{"STREAM+o-l-f", STREAM_O, false},
new Object[]{"STREAM+o-l+f", STREAM_OF, false},
new Object[]{"STREAM+o+l-f", STREAM_OL, false},
new Object[]{"STREAM+o+l+f", STREAM_OLF, false},
new Object[]{"MAX_DATA", MAX_DATA, false},
new Object[]{"MAX_DATA", MAX_DATA_B, true},
new Object[]{"MAX_STREAM_DATA", MAX_STREAM_DATA, false},
new Object[]{"MAX_STREAM_DATA", MAX_STREAM_DATA_B, true},
new Object[]{"MAX_STREAMS bidi", MAX_STREAMS_B, false},
new Object[]{"MAX_STREAMS uni", MAX_STREAMS_U, false},
new Object[]{"DATA_BLOCKED", DATA_BLOCKED, false},
new Object[]{"DATA_BLOCKED", DATA_BLOCKED_B, true},
new Object[]{"STREAM_DATA_BLOCKED", STREAM_DATA_BLOCKED, false},
new Object[]{"STREAM_DATA_BLOCKED", STREAM_DATA_BLOCKED_B, true},
new Object[]{"STREAMS_BLOCKED bidi", STREAMS_BLOCKED_B, false},
new Object[]{"STREAMS_BLOCKED uni", STREAMS_BLOCKED_U, false},
new Object[]{"NEW_CONNECTION_ID", NEW_CONNECTION_ID, false},
new Object[]{"RETIRE_CONNECTION_ID", RETIRE_CONNECTION_ID, false},
new Object[]{"RETIRE_CONNECTION_ID", RETIRE_CONNECTION_ID_B, true},
new Object[]{"PATH_CHALLENGE", PATH_CHALLENGE, false},
new Object[]{"PATH_RESPONSE", PATH_RESPONSE, false},
new Object[]{"CONNECTION_CLOSE QUIC", CONNECTION_CLOSE_Q, false},
new Object[]{"CONNECTION_CLOSE QUIC non-utf8 reason", CONNECTION_CLOSE_Q_BAD_REASON, false},
new Object[]{"CONNECTION_CLOSE app", CONNECTION_CLOSE_A, false},
new Object[]{"CONNECTION_CLOSE app non-utf8 reason", CONNECTION_CLOSE_A_BAD_REASON, false},
};
}
public static Object[][] badFrames() {
return new Object[][]{
new Object[]{"ack without ecn, negative pn", ACK_NEG_BASE},
new Object[]{"ack without ecn, negative pn, v2", ACK_NEG_BASE_2},
new Object[]{"ack with ecn, negative pn", ACK_NEG_ECN},
new Object[]{"CRYPTO out of range", CRYPTO_OOR},
new Object[]{"NEW_TOKEN empty", NEW_TOKEN_EMPTY},
new Object[]{"MAX_STREAMS bidi out of range", MAX_STREAMS_B_OOR},
new Object[]{"MAX_STREAMS uni out of range", MAX_STREAMS_U_OOR},
new Object[]{"STREAMS_BLOCKED bidi out of range", STREAMS_BLOCKED_B_OOR},
new Object[]{"STREAMS_BLOCKED uni out of range", STREAMS_BLOCKED_U_OOR},
new Object[]{"NEW_CONNECTION_ID zero length", NEW_CONNECTION_ID_ZERO},
};
}
@ParameterizedTest
@MethodSource("goodFrames")
public void testReencode(String desc, byte[] frame, boolean bloated) throws Exception {
// check if the goodFrames provider indeed contains good frames
ByteBuffer buf = ByteBuffer.wrap(frame);
var qf = QuicFrame.decode(buf);
assertFalse(buf.hasRemaining(), buf.remaining() + " bytes left in buffer after parsing");
// some frames deliberately use suboptimal encoding, skip them
if (bloated) return;
assertEquals(frame.length, qf.size(), "Frame size mismatch");
buf.clear();
ByteBuffer encoded = ByteBuffer.allocate(frame.length);
qf.encode(encoded);
assertFalse(encoded.hasRemaining(), "Actual frame length mismatch");
encoded.flip();
assertEquals(encoded, buf, "Encoded buffer is different from the original one");
}
@ParameterizedTest
@MethodSource("goodFrames")
public void testToString(String desc, byte[] frame, boolean bloated) throws Exception {
// check if the goodFrames provider indeed contains good frames
ByteBuffer buf = ByteBuffer.wrap(frame);
var qf = QuicFrame.decode(buf);
assertFalse(buf.hasRemaining(), buf.remaining() + " bytes left in buffer after parsing");
System.out.println(qf); // should not throw
}
@ParameterizedTest
@MethodSource("goodFrames")
public void testTruncatedFrame(String desc, byte[] frame, boolean bloated) throws Exception {
// check if parsing a truncated frame throws the right error
ByteBuffer buf = ByteBuffer.wrap(frame);
for (int i = 1; i < buf.capacity(); i++) {
buf.position(0);
buf.limit(i);
try {
var qf = QuicFrame.decode(buf);
fail("Expected the decoder to throw on length " + i + ", got: " + qf);
} catch (QuicTransportException e) {
assertEquals(QuicTransportErrors.FRAME_ENCODING_ERROR.code(), e.getErrorCode());
}
}
}
@ParameterizedTest
@MethodSource("badFrames")
public void testBadFrame(String desc, byte[] frame) throws Exception {
// check if parsing a bad frame throws the right error
ByteBuffer buf = ByteBuffer.wrap(frame);
try {
var qf = QuicFrame.decode(buf);
fail("Expected the decoder to throw, got: "+qf);
} catch (QuicTransportException e) {
assertEquals(QuicTransportErrors.FRAME_ENCODING_ERROR.code(), e.getErrorCode());
}
}
}