1 Commits
Author SHA1 Message Date
Jens GeyerandClaude Opus 5 c98e79ad9f THRIFT-5371: Bind the C++ read budget to the frame that carries the message
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>
2026-08-31 13:39:50 +02:00