| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 8d4bec2 commit 9a65b7f
2 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1802,6 +1802,24 @@ void Endpoint::Receive(const uint8_t* data, | |||
| 1802 | 1802 | // successfully decoded. Send a Version Negotiation response | |
| 1803 | 1803 | // per RFC 9000 Section 6. The VN packet's DCID is the client's | |
| 1804 | 1804 | // SCID and vice versa (mirrored back to the client). | |
| 1805 | + // | ||
| 1806 | + // ngtcp2_pkt_decode_version_cid() only enforces the | ||
| 1807 | + // NGTCP2_MAX_CIDLEN limit for *supported* versions; for an | ||
| 1808 | + // unsupported version it returns the raw connection ID lengths | ||
| 1809 | + // taken from the single-byte length fields on the wire, which can | ||
| 1810 | + // be up to 255. Constructing a CID -- backed by a fixed | ||
| 1811 | + // NGTCP2_MAX_CIDLEN-byte buffer -- from such a length writes past | ||
| 1812 | + // the buffer (an assertion abort in release builds). A single | ||
| 1813 | + // unauthenticated UDP datagram could therefore crash the endpoint | ||
| 1814 | + // before any handshake. Drop these packets, mirroring the | ||
| 1815 | + // CID-length policy applied below for supported versions. | ||
| 1816 | + if (pversion_cid.dcidlen > NGTCP2_MAX_CIDLEN || | ||
| 1817 | + pversion_cid.scidlen > NGTCP2_MAX_CIDLEN) { | ||
| 1818 | + Debug(this, | ||
| 1819 | + "Version negotiation packet had incorrectly sized CIDs, " | ||
| 1820 | + "ignoring"); | ||
| 1821 | + return; | ||
| 1822 | + } | ||
| 1805 | 1823 | Debug(this, | |
| 1806 | 1824 | "Packet version %d is not supported, sending version negotiation", | |
| 1807 | 1825 | pversion_cid.version); | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,89 @@ | |||
| 1 | + // Flags: --experimental-quic --no-warnings | ||
| 2 | + | ||
| 3 | + // Regression test for an unauthenticated remote crash in the QUIC Version | ||
| 4 | + // Negotiation path. | ||
| 5 | + // | ||
| 6 | + // ngtcp2_pkt_decode_version_cid() does not clamp the connection ID lengths | ||
| 7 | + // to NGTCP2_MAX_CIDLEN (20) when the packet's version is unsupported -- it | ||
| 8 | + // returns the raw single-byte length fields from the wire, which can be up | ||
| 9 | + // to 255. Endpoint::Receive() used to build CID objects (each backed by a | ||
| 10 | + // fixed 20-byte buffer) directly from those lengths before any bound check, | ||
| 11 | + // so a single crafted UDP datagram with an oversized DCID/SCID length would | ||
| 12 | + // overflow the buffer and abort the process before the handshake. | ||
| 13 | + // | ||
| 14 | + // This test sends such a datagram directly with node:dgram and asserts the | ||
| 15 | + // endpoint drops it without crashing, while a well-formed unsupported-version | ||
| 16 | + // datagram still produces exactly one Version Negotiation response. | ||
| 17 | + | ||
| 18 | + import { hasQuic, skip, mustNotCall } from '../common/index.mjs'; | ||
| 19 | + import assert from 'node:assert'; | ||
| 20 | + | ||
| 21 | + const { strictEqual } = assert; | ||
| 22 | + | ||
| 23 | + if (!hasQuic) { | ||
| 24 | + skip('QUIC is not enabled'); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + const { createSocket } = await import('node:dgram'); | ||
| 28 | + const { listen } = await import('../common/quic.mjs'); | ||
| 29 | + | ||
| 30 | + // A long-header QUIC packet must be at least NGTCP2_MAX_UDP_PAYLOAD_SIZE | ||
| 31 | + // (1200) bytes for an unsupported version to decode as a VN trigger. | ||
| 32 | + const PACKET_SIZE = 1200; | ||
| 33 | + | ||
| 34 | + // Build a QUIC long-header packet with an unsupported version and the given | ||
| 35 | + // DCID/SCID lengths. Lengths greater than 20 are only expressible because the | ||
| 36 | + // on-wire length field is a single byte (0-255). | ||
| 37 | + function buildLongHeaderPacket(dcidLen, scidLen) { | ||
| 38 | + const packet = Buffer.alloc(PACKET_SIZE); | ||
| 39 | + // Header form bit (0x80) + fixed bit (0x40). | ||
| 40 | + packet[0] = 0xc0; | ||
| 41 | + // Version 0x0a0a0a0a: nonzero (so it is not a real VN packet) and not a | ||
| 42 | + // version Node supports, which forces the NGTCP2_ERR_VERSION_NEGOTIATION | ||
| 43 | + // decode path. | ||
| 44 | + packet.writeUInt32BE(0x0a0a0a0a, 1); | ||
| 45 | + packet[5] = dcidLen; // DCID length byte | ||
| 46 | + // DCID bytes occupy [6, 6 + dcidLen); SCID length byte follows them. | ||
| 47 | + packet[6 + dcidLen] = scidLen; | ||
| 48 | + // Remaining bytes stay zero-filled as padding to reach PACKET_SIZE. | ||
| 49 | + return packet; | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + // No handshake ever completes: the test only sends raw datagrams, so the | ||
| 53 | + // session callback must never fire. | ||
| 54 | + const serverEndpoint = await listen(mustNotCall()); | ||
| 55 | + const { address, port } = serverEndpoint.address; | ||
| 56 | + | ||
| 57 | + const socket = createSocket('udp4'); | ||
| 58 | + | ||
| 59 | + function send(packet) { | ||
| 60 | + return new Promise((resolve, reject) => { | ||
| 61 | + socket.send(packet, port, address, (err) => { | ||
| 62 | + if (err) reject(err); | ||
| 63 | + else resolve(); | ||
| 64 | + }); | ||
| 65 | + }); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + // 1. Oversized DCID (21 > NGTCP2_MAX_CIDLEN). Before the fix this aborted the | ||
| 69 | + // process. After the fix it must be dropped silently. | ||
| 70 | + await send(buildLongHeaderPacket(21, 0)); | ||
| 71 | + | ||
| 72 | + // 2. A well-formed unsupported-version packet with valid (<= 20 byte) CIDs. | ||
| 73 | + // This must still elicit exactly one Version Negotiation response, proving | ||
| 74 | + // the fix did not break legitimate version negotiation. | ||
| 75 | + await send(buildLongHeaderPacket(8, 8)); | ||
| 76 | + | ||
| 77 | + // Poll until the valid packet has been processed into a VN response. | ||
| 78 | + const deadline = Date.now() + 2000; | ||
| 79 | + while (serverEndpoint.stats.versionNegotiationCount === 0n) { | ||
| 80 | + if (Date.now() > deadline) break; | ||
| 81 | + await new Promise((resolve) => setTimeout(resolve, 25)); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + // Exactly one VN response: the oversized packet was dropped (not crashed, not | ||
| 85 | + // negotiated), the valid packet was negotiated. | ||
| 86 | + strictEqual(serverEndpoint.stats.versionNegotiationCount, 1n); | ||
| 87 | + | ||
| 88 | + socket.close(); | ||
| 89 | + await serverEndpoint.close(); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments