THRIFT-6177: Bound the WebSocket frame payload length in the C++ library - #3780
Closed
Jens-G wants to merge 1 commit into
Closed
THRIFT-6177: Bound the WebSocket frame payload length in the C++ library#3780Jens-G wants to merge 1 commit into
Jens-G wants to merge 1 commit into
Conversation
TWebSocketServer::readFrame sizes its read buffer from the payload length
the frame header declares, before a single payload byte has arrived:
// size_t is smaller than a ulong on a 32-bit system
if (payloadLength > UINT32_MAX) {
failConnection(CloseCode::MessageTooBig);
return false;
}
auto length = static_cast<uint32_t>(payloadLength);
if (length > 0) {
...
readBuffer_.resetBuffer(length);
uint8_t* buffer = readBuffer_.getWritePtr(length);
read = transport_->read(buffer, length);
UINT32_MAX is the only bound, so the fourteen bytes of a header carrying a
64-bit length decide the size of the allocation. Measured on a Linux x86-64
build: a header declaring 0xFFFFFFFF takes the process from a VmPeak of
10,512 kB to 4,205,092 kB and asks the transport underneath for
4,294,967,295 bytes, with no payload sent at all. resetBuffer() constructs
a fresh TMemoryBuffer of that size, so the allocation happens whether or
not the bytes ever turn up, and the buffer is a member that lives as long
as the connection does.
The transport has a TConfiguration and consults neither of its limits here.
Hold the declared length to TConfiguration::maxFrameSize, the ceiling
TFramedTransport::readFrame already applies to its own frames, and refuse
anything above it with close code 1009 Message Too Big, which is what this
transport already sends when a frame is too large for it.
Worth a release note: maxFrameSize defaults to 16384000, so a peer sending
a single WebSocket frame larger than that is refused where it was accepted
before. The bound follows whatever an operator sets.
Six tests, the first this transport has had. They assert the largest read
the server asked of the transport underneath it rather than merely that the
read failed: a payload that never arrives ends the frame either way, so
"did it fail?" passes on the unmodified library too. Two fail before the
change, 67108864 > 1024 and 32768 > 1024, and all six pass after; the other
four are regression guards -- an ordinary frame, a frame of exactly the
maximum, two frames in a row, and a length with the high bit set -- that
pass either way.
Client: cpp
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Sep 2, 2026
Member
Author
|
wht ai do you used to write code? |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TWebSocketServer::readFramesizes its read buffer from the payload length the frame header declares, before a single payload byte has arrived.UINT32_MAXis the only bound, so the fourteen bytes of a header carrying a 64-bit length decide the size of the allocation.Measured on a Linux x86-64 build: a header declaring
0xFFFFFFFFtakes the process from aVmPeakof 10,512 kB to 4,205,092 kB and asks the transport underneath for 4,294,967,295 bytes, with no payload sent at all.resetBuffer()constructs a freshTMemoryBufferof that size, so the allocation happens whether or not the bytes ever turn up, and the buffer is a member that lives as long as the connection does.The transport has a
TConfigurationand consults neither of its limits on this path. This holds the declared length toTConfiguration::maxFrameSize— the ceilingTFramedTransport::readFramealready applies to its own frames — and refuses anything above it with close code 1009 Message Too Big, which is what this transport already sends when a frame is too large for it.Compatibility — worth a release note
maxFrameSizedefaults to 16384000, so a peer sending a single WebSocket frame larger than that is refused where it was accepted before. The bound follows whatever an operator sets.Tests
lib/cpp/test/TWebSocketServerTest.cpp, the first tests this transport has had. They assert the largest read the server asked of the transport underneath it rather than merely that the read failed — a payload that never arrives ends the frame either way, so "did it fail?" passes on the unmodified library too.67108864 > 1024and32768 > 1024. All six pass after.bin/UnitTests: 106 of 107 before and after. The one failure,TServerSocketTest/test_bind_to_address, is a pre-existing environment failure on this host, unrelated and present onmaster.The test is registered under
if(OPENSSL_FOUND AND WITH_OPENSSL)in CMake, because that is the condition under whichTWebSocketServer.cppis built at all; the autotools build compiles it unconditionally, soMakefile.amneeds no guard.Separable, found while checking this and deliberately not in scope
readFramereads the payload with a singletransport_->read()and treats a short read as end of stream. Verified by execution: an ordinary 40-byte frame delivered as 20 + 20 bytes makesreadAllreturn 0, so any frame whose payload does not arrive in one read is silently dropped. That is a separate correctness defect, it changes blocking behaviour to fix, and the bound added here is the precondition for fixing it safely —readAllon an unbounded declared length would wait for 4 GiB. It will follow as its own ticket.🤖 Generated with Claude Code