Jens Geyer created THRIFT-6291:
----------------------------------

             Summary: Align the container-size limit across the TConfiguration 
bindings
                 Key: THRIFT-6291
                 URL: https://issues.apache.org/jira/browse/THRIFT-6291
             Project: Thrift
          Issue Type: Improvement
          Components: Documentation
            Reporter: Jens Geyer


Seven bindings let a caller bound the declared element count of a list, set or 
map before the decoder sizes the collection. Three do not, and where the knob 
exists it is spelled five different ways. The deliverable of this issue is the 
specification update below, which records the shape the project follows and 
settles the conventions. Closing the gap in the three bindings that lack the 
setting is done in its sub-tasks, one pull request per language.

h2. Current state (read-verified on master)

|| Binding || Container-count limit || Where || Default ||
| c_glib | yes | {{container_limit}} property, binary + compact protocol and 
their factories | {{0}} = off |
| C++ | yes | {{TBinaryProtocolT::setContainerSizeLimit()}}, likewise compact | 
{{0}} = off |
| Java | yes | {{containerLengthLimit_}} ctor argument | {{-1}} 
({{NO_LENGTH_LIMIT}}) = off |
| D | yes | {{containerSizeLimit}} ctor argument | {{16384000}} |
| Node.js | yes (compact) | {{container_limit_}} | {{0}} = off |
| Rust | yes | {{TConfiguration::max_container_size}} | {{None}} = off |
| Go | yes | {{TConfiguration.MaxContainerSize}} (THRIFT-6286) | {{<= 0}} = off 
|
| netstd | *no* | (none) | (none) |
| Haxe | *no* | (none) | (none) |
| Delphi | *no* | (none) | (none) |

h2. Why these three

netstd, Haxe and Delphi are not unbounded. All three call 
{{CheckReadBytesAvailable(count * GetMinSerializedSize(elementType))}} in every 
{{ReadListBegin}} / {{ReadSetBegin}} / {{ReadMapBegin}} of the binary, compact 
and JSON protocols, and that call is charged against the remaining message 
budget, which is seeded from {{MaxMessageSize}}.

That check bounds the container in *wire bytes*. It cannot bound it in 
*per-element in-memory footprint*, because the minimum serialized size of a 
struct element is 1 byte (the {{T_STOP}}). At the default {{MaxMessageSize}} of 
100 MB a list of structs therefore passes the check at up to ~100 million 
elements, each of which becomes a full object on the heap. 
{{doc/thrift-threat-model.md}} makes the same point in its appendix: the 
{{checkReadBytesAvailable}} path and the container-limit path are independent 
guards, and the message-size cap alone does not bound memory when the element 
type has a large in-memory footprint.

The other seven bindings give the operator a second lever for exactly that 
case; these three do not. That is the parity gap, and it is the only one — the 
bindings in the first group need no change.

h2. Shape to follow

* The three bindings already own a {{TConfiguration}}, and their protocols 
already take every other limit from it, so the new setting belongs there rather 
than on new protocol constructors or setters. This follows Rust and Go 
(THRIFT-6286).
* Bindings whose knob is already on the protocol (c_glib, C++, Java, D, 
Node.js) keep it. Mirroring it into {{TConfiguration}} would leave two ways to 
set one limit and buys no coverage.
* *Off by default*, so no message that is accepted today is rejected afterwards.
* One insertion point per binding: the three shared {{CheckReadBytesAvailable}} 
helpers on the protocol base class already carry the element count for all 
three protocols.

h2. Scope: container count only, not string length

{{MaxMessageSize}} already bounds a single string, because a string's declared 
length is its byte length and the remaining-bytes check sees it directly. The 
container count is the case the byte budget cannot see, since the multiplier is 
per-element in-memory footprint. A string-length setting would therefore be a 
second spelling of a limit that is already effective, and is deliberately out 
of scope here. Bindings that do expose a string limit as well (C++, Java, D, 
c_glib, Rust) did so for reasons outside this issue; nothing here proposes 
removing those.

h2. Spec

{{doc/specs/thrift-tconfiguration.md}} still reads "currently holds three 
settings" and was last revised in 2019. THRIFT-5021 was filed because limits 
were, in that document's own words, "not consistently managed and just randomly 
distributed across the code base", so adding a fourth setting to several 
bindings without writing it down would reproduce the condition the spec exists 
to prevent. This issue carries that update, and it settles two things the tree 
is currently inconsistent about:

* *The off sentinel.* Four spellings are in use: {{0}} (C++, c_glib, Node.js), 
{{-1}} (Java), {{None}} (Rust), {{<= 0}} (Go). The spec should name one for the 
new setting.
* *The default.* It is not uniformly off today — D ships a finite container 
limit of {{16384000}} and Rust ships a finite {{max_string_size}} of 100 MB. 
The spec should state the rule for the new setting and note D as the 
pre-existing exception rather than imply a uniformity that does not exist.

h2. Sub-tasks

One per binding, each its own pull request: netstd, Haxe, Delphi. They depend 
on this issue only for the two conventions above; the code change itself is 
independent in each.

_Drafted with AI assistance (Claude Opus 5); reviewed and filed by Jens Geyer._



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

Reply via email to