Jens Geyer created THRIFT-6169:
----------------------------------

             Summary: Size containers from the payload rather than the declared 
count in the Python C extension
                 Key: THRIFT-6169
                 URL: https://issues.apache.org/jira/browse/THRIFT-6169
             Project: Thrift
          Issue Type: Bug
          Components: Python - Library
            Reporter: Jens Geyer
             Fix For: 0.25.0


The C extension sizes a list, tuple or set from the element count the peer 
declares, before any of the elements have been read 
(lib/py/src/ext/protocol.tcc, the T_LIST/T_SET case of decodeValue):

    bool use_tuple = type == T_LIST && parsedargs.immutable;
    ScopedPyObject ret(use_tuple ? PyTuple_New(len) : PyList_New(len));

The only check on len is checkLengthLimit(len, containerLimit()), and 
containerLimit_ defaults to INT32_MAX (protocol.h, and again in module.cpp as 
the fallback when the protocol's container_length_limit attribute is None, 
which is its documented default). Measured with tracemalloc on CPython: a nine 
byte body declaring 2,000,000 elements reserves 16 MB, and 20,000,000 reserves 
152 MB.

This is the same defect as THRIFT-3175, which was fixed in 0.9.3 by a hard cap 
of 10,000 elements. That cap turned out to reject legitimate large containers 
-- see jrf's comment on THRIFT-3175 -- and THRIFT-3532 lifted it in 0.10.0 in 
favour of the configurable container_length_limit, whose default is unlimited. 
So the count has been taken at face value again since 0.10.0 for anyone who 
does not set that limit.

The fix implements the heuristic dvirsky proposed on THRIFT-3175 in 2015: size 
the container from what the message can actually supply. Every element occupies 
at least one byte on the wire whatever the protocol, so the bytes still in the 
decode buffer bound how many can arrive. A message that is already buffered 
whole -- a memory buffer, or a frame -- always has at least as many bytes left 
as elements declared, so it is still allocated at full size in one go and 
nothing about the common path changes. Anything else grows as the elements 
materialise. Neither of the two earlier failure modes returns: large containers 
are not refused, and the declared count is not trusted.

The pure-Python decoder is not affected; it appends in a loop and never 
preallocates. Of the other native accelerated implementations in the tree, 
Ruby's already caps this (new_container_array in lib/rb/ext/struct.c) and PHP's 
uses array_init with no size hint.



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

Reply via email to