TFramedTransport::readFrame() knows the exact size of the frame it hands to the
protocol, but the budget the protocol is checked against stays at
TConfiguration::maxMessageSize for the life of the connection.
updateKnownMessageSize() had no caller anywhere in the C++ library: the
mechanism was there and entirely unused.
That has two consequences. The one reported on this ticket: the budget is a
per-connection allowance that only consume() draws down and only readEnd() or
flush() restores, so a long run of frames read without either exhausts it and
starts refusing frames well within the limit. Sixty-four frames over a
transport with room for any single one of them, but not for their sum, ends in
"MaxMessageSize reached". And the other direction: a 68-byte frame could
declare a 64 MB field, and readStringBody() would resize the caller's string to
64 MB before the following read discovered there was nothing behind it. A
5-byte frame could likewise declare an 8-million-element list.
Three parts:
- readFrame() resets the budget and then binds it to the frame, after the
maxFrameSize check and before the buffer is allocated. The full reset first
is not optional: resetConsumedMessageSize() refuses to grow a budget, so a
frame larger than its predecessor would be rejected outright, and
updateKnownMessageSize() on its own carries the previous frame's consumption
forward. The reset belongs here rather than in readEnd(), which a oneway call
never reaches.
- TBufferBase::read() checks the budget against the bytes it can deliver rather
than the bytes asked for, for a transport that sets the new
budgetBoundToBuffer_. read() may always return less than requested, so once
the budget is one frame, an ordinary "give me up to N bytes" would otherwise
be refused instead of short-read; TransportTest does exactly that with random
chunk sizes. This does not loosen the bound the protocol is held to -- every
allocation-gating check, readStringBody() and the container element counts in
all three protocols, calls checkReadBytesAvailable() directly with the size
the wire declared and does not come through read().
- TNonblockingServer takes the frame apart itself and hands the payload to a
TMemoryBuffer, with an identity transport factory by default, so the protocol
reads straight from that buffer and none of the above reaches it. New public
TMemoryBuffer::bindMessageSizeToBuffer() binds the budget to what the buffer
holds, called at both resetBuffer sites.
THeaderTransport overrides readFrame() and so is unchanged; binding there is a
separate question, since in unframed mode it hands out four bytes at a time.
Twelve tests, written before the change. Five describe traffic that is
legitimate today and pass unmodified; three fail unmodified -- the field case
asserts on the size of the string the protocol was asked to fill, because both
outcomes raise TTransportException(END_OF_FILE) and only the allocation tells
them apart. The remaining four cover the new TMemoryBuffer entry point.
Client: cpp
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>