Jens Geyer created THRIFT-6243:
----------------------------------

             Summary: Grow the TNonblockingServer read buffer as the payload 
arrives, not on the frame header
                 Key: THRIFT-6243
                 URL: https://issues.apache.org/jira/browse/THRIFT-6243
             Project: Thrift
          Issue Type: Bug
          Components: C++ - Library
            Reporter: Jens Geyer


{{TNonblockingServer}}'s {{TConnection}} reserves the whole declared frame the 
moment it reads the four-byte length prefix: {{transition()}} in the 
{{APP_READ_FRAME_SIZE}} state reallocates the read buffer to the frame size 
({{TNonblockingServer.cpp}}) before any payload byte has arrived. A peer can 
therefore make the server reserve up to {{getMaxFrameSize()}} bytes per 
connection by sending only a four-byte header, with {{MAX_CONNECTIONS}} 
defaulting to {{INT_MAX}}. The reservation is address space rather than 
resident memory (it is a {{std::realloc}}), but it is still an unamplified 
header-to-allocation step.

This is part (4) of the nonblocking-server frame handling. THRIFT-6183 lowered 
the default limit; a separate change wires the server to a {{TConfiguration}} 
and adds a pre-allocation budget check. This change bounds the allocation by 
what the peer actually sends:

* {{transition()}} reserves only enough to hold the length prefix and begin 
reading (the prefix plus about a kilobyte), not the declared frame.
* The buffer grows toward the frame size in the {{SOCKET_RECV}} state as bytes 
are actually read -- by the same doubling the up-front allocation used, now 
driven by what the peer sends rather than by the size it declares, with the 
doubling guarded so it cannot overflow {{uint32_t}}.

A frame that fully arrives ends at the same buffer size as before (the next 
power of two at or above the frame size), so there is no change for legitimate 
traffic in the steady state and no change at all for frames of a kilobyte or 
less (still a single allocation). A header with no payload reserves next to 
nothing.

Tradeoff, for the reviewer: a large frame that does arrive now pays the 
amortized doubling copy that any grow-as-you-go buffer pays -- on the order of 
twice the frame size in {{memmove}} over the life of the connection, against a 
single allocation before. This is the cost of not trusting the declared size, 
and the same shape the scan that raised this recommended ("allocate as the 
payload arrives, not on the header").

Test: TNonblockingServerTest gains three cases. A Linux-only, forked case 
announces a 256 MiB frame, sends none of it, and asserts the server's VmSize 
does not grow by more than a small slack -- it reads VmSize rather than RSS 
because the reservation is committed, not resident; it fails when the buffer is 
reserved on the header and passes when it grows with the payload. Two portable 
cases drive multi-megabyte requests through the framed client -- each frame far 
larger than the initial reservation, so it is assembled over many libevent 
callbacks with the buffer doubling as the bytes arrive, and every request must 
round-trip byte-for-byte. One frees the grown buffer when the connection closes 
(via {{returnConnection}}) and regrows it on the next connection; the other 
forces the resize counter to 1 so the buffer is freed between two frames on a 
single live connection and regrown from empty mid-stream. Together they cover 
the grow, the free, and the regrow-from-empty paths, so a lost byte or a 
mishandled empty buffer shows up as a mismatch.

_Drafted with AI assistance (Claude Opus 4.8)._



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to