[ 
https://issues.apache.org/jira/browse/THRIFT-6291?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Jens Geyer updated THRIFT-6291:
-------------------------------
    Description: 
Eight 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 |
| Python | yes | {{container_length_limit}} on binary, compact and JSON, and 
their factories | {{None}} = 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 eight 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, Python, D, 
Node.js) keep it. Mirroring it into {{TConfiguration}} would leave two ways to 
set one limit and buys no coverage. Whether each of those knobs is actually 
*reachable* on every path that sizes a collection is a separate question, 
tracked outside this issue.
* *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, 
Python, 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}}, Python a finite {{string_length_limit}}, and Rust 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._

  was:
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._


> 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
>            Priority: Minor
>          Time Spent: 10m
>  Remaining Estimate: 0h
>
> Eight 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 |
> | Python | yes | {{container_length_limit}} on binary, compact and JSON, and 
> their factories | {{None}} = 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 eight 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, Python, 
> D, Node.js) keep it. Mirroring it into {{TConfiguration}} would leave two 
> ways to set one limit and buys no coverage. Whether each of those knobs is 
> actually *reachable* on every path that sizes a collection is a separate 
> question, tracked outside this issue.
> * *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, Python, 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}}, Python a finite {{string_length_limit}}, and Rust 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