THRIFT-6142: Enforce Ruby unframed HeaderTransport limits - #3706
Conversation
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
This PR fixes a gap in Ruby HeaderTransport where unframed Binary/Compact reads bypassed max_frame_size, adding byte accounting and size-limit enforcement for unframed messages (including protocol signatures) and resetting budgets at message boundaries.
Changes:
- Track and enforce unframed message byte budgets in
HeaderTransport, raisingTransportException::SIZE_LIMITbefore exceedingmax_frame_size. - Reset unframed size budgets at message boundaries by notifying transports from pure-Ruby and native protocol
read_message_begin. - Add specs covering exact-limit acceptance, over-limit rejection, signature handling, partial reads, and sequential message budgeting (incl. accelerated binary).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| lib/rb/spec/header_transport_spec.rb | Adds tests validating unframed size-limit enforcement and per-message budgeting. |
| lib/rb/lib/thrift/transport/header_transport.rb | Implements unframed byte accounting, limit checks, and message-boundary resets. |
| lib/rb/lib/thrift/protocol/compact_protocol.rb | Notifies transports of new-message boundaries to reset unframed budgets. |
| lib/rb/lib/thrift/protocol/binary_protocol.rb | Notifies transports of new-message boundaries to reset unframed budgets. |
| lib/rb/ext/thrift_native.c | Interns reset_message_size method ID for native protocol boundary notifications. |
| lib/rb/ext/constants.h | Exposes reset_message_size method ID for native extension usage. |
| lib/rb/ext/compact_protocol.c | Calls reset_message_size at message start in native compact reader. |
| lib/rb/ext/binary_protocol_accelerated.c | Calls reset_message_size at message start in accelerated binary reader. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
188921a to
6085009
Compare
6085009 to
b1c15eb
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (1)
lib/rb/lib/thrift/transport/header_transport.rb:358
- In
handle_unframed, whenreq_sz > 4and the configured@max_frame_sizeis exactly the 4-byte protocol signature,read_unframed(bytes_left)will raise becauseremaining == 0, causing thisreadcall to raise after consumingfirst_wordfrom the underlying transport (dropping bytes). SinceHeaderTransport#readnow supports returning partial unframed data up to the limit, this path should also allow returning justfirst_wordwhen no budget remains, and defer raisingSIZE_LIMITuntil a subsequent read attempts to exceed the limit.
bytes_left = req_sz - 4
if bytes_left > 0
rest = read_unframed(bytes_left)
@read_buffer = StringIO.new(first_word + rest)
Client: rb Co-Authored-By: OpenAI Codex (GPT-5.6) <codex@openai.com>
b1c15eb to
1fff4ca
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (2)
lib/rb/lib/thrift/protocol/binary_protocol.rb:41
BinaryProtocol#initializenow callsmessage_boundaries?unconditionally. This is a behavioral API requirement change for transports: any duck-typed transport (or test double) that previously only neededread/write/read_allwill now raiseNoMethodErrorunless it also implementsmessage_boundaries?. Since the check is cached once per protocol instance, you can preserve backward compatibility by guarding the call withrespond_to?and defaulting tofalsewhen absent.
def initialize(trans, strict_read = true, strict_write = true)
super(trans)
@reset_message_size = trans.message_boundaries?
@strict_read = strict_read
lib/rb/lib/thrift/protocol/compact_protocol.rb:115
CompactProtocol#initializenow callsmessage_boundaries?unconditionally. This makesmessage_boundaries?a required method for any duck-typed transport used with CompactProtocol, which can break existing custom transports and lightweight test doubles. Since you only need this once per instance, guarding withrespond_to?retains compatibility while still caching the result.
def initialize(transport)
super(transport)
@reset_message_size = transport.message_boundaries?
Ruby HeaderTransport enforced
max_frame_sizefor Header and framed clients but passed unframed Binary and Compact reads directly to the underlying transport without applying the configured limit.This change counts bytes consumed by each unframed protocol message, including the initial protocol signature. Reads are capped at the remaining budget, so partial reads and exact-limit messages remain supported; a later read after the budget is exhausted raises
TransportException::SIZE_LIMIT.Binary and Compact protocols query
BaseTransport#message_boundaries?once during construction and cache the result. The base transport returnsfalse, while HeaderTransport returnstrue, so pure-Ruby and native readers notify HeaderTransport when sequential messages need independent budgets. The normal read path performs one cached boolean check without repeated method lookup, runtime class changes, or per-instance extensions; native readers resolve the transport only inside the enabled branch.Benchmarks
I compared the commit before this PR (
a9663bc) with the current commit (1fff4ca) on Ruby 4.0.6 for aarch64 Linux. The benchmark reads 100,000 minimal message envelopes; each result is the median of 21 warmed trials, with payload construction outside the timed section.On this deliberately small workload, the measurable direct-protocol cost is about 17 ns per message for accelerated Binary and 30 ns per message for native Compact. The pure-Ruby direct readers were within run-to-run noise. Normal RPCs also perform field decoding, transport I/O, and application work, so this benchmark emphasizes the fixed per-message cost.
[skip ci]anywhere in the commit message to free up build resources.