Skip to content

fix: bound inbound Artery TCP frame length at framing (#3492) - #3525

Merged
pjfanning merged 2 commits into
apache:1.7.xfrom
pjfanning:tcp-framing-bound-17
Sep 3, 2026
Merged

fix: bound inbound Artery TCP frame length at framing (#3492)#3525
pjfanning merged 2 commits into
apache:1.7.xfrom
pjfanning:tcp-framing-bound-17

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Motivation

Backport of #3492 to 1.7.x: TcpFraming read the 4-byte frame length from the wire and
passed it straight to reader.take / ByteBuffer allocation with no bound. A peer —
unauthenticated on the default tcp transport — could declare an oversized (up to
~2 GiB) or negative frame length and drive a large allocation. The decoder and
deserializer stages downstream of the inbound MergeHub are shared by every inbound
connection, so a fatal OutOfMemoryError there fails the shared inbound stream and,
after inbound-max-restarts, terminates the whole ActorSystem — one malformed
connection escalating to node loss.

Modification

Two commits:

  1. Cherry-pick of fcc340c (fix: bound inbound Artery TCP frame length at framing #3492) — applied clean, no conflicts. The
    already-configured maximum-frame-size / maximum-large-frame-size are threaded
    into TcpFraming; ReadStreamId selects the applicable bound per stream (the
    large-message stream keeps its larger limit) and ReadFrame rejects a negative or
    over-bound frame length before any data is buffered, as a per-connection
    FramingException.
  2. The two per-stream bound tests just added on main in test: cover the per-stream frame-size bound in TcpFraming #3524, so 1.7.x gets the same
    coverage: with distinct maxima configured, a frame over the ordinary maximum but
    within the large maximum is accepted on ArteryTransport.LargeStreamId and rejected
    on ArteryTransport.OrdinaryStreamId, with full payloads so a false accept cannot
    hide behind truncation.

New TcpFraming constructor params default to Int.MaxValue, so the change is source-
and binary-compatible; TcpFraming is @InternalApi regardless.

Result

Same as #3492: a malformed or oversized frame is rejected per-connection instead of
allocating without limit and risking a fatal error that escalates to ActorSystem
termination. Legitimate frames up to the configured maxima are unaffected.

Tests

References

Backport of #3492; includes the tests from #3524.

Motivation:
TcpFraming read the 4-byte frame length from the wire and passed it straight
to reader.take / ByteBuffer allocation with no bound. A peer - unauthenticated
on the default tcp transport - could declare an oversized or negative frame and
drive a large allocation. Because the decoder and deserializer stages downstream
of the MergeHub are shared by every inbound connection, an OutOfMemoryError there
is fatal to the shared stream and, after inbound-max-restarts, terminates the
whole ActorSystem - turning one malformed connection into node loss.

Modification:
Pass the configured maximum-frame-size and maximum-large-frame-size into
TcpFraming and reject a frame length that is negative or exceeds the maximum for
the connection's stream, before any data is buffered. The large-message stream
keeps its larger bound. Rejection is a FramingException, which tears down only
that connection.

Result:
A malformed or oversized frame is rejected per-connection instead of allocating
without limit and risking a fatal error on the shared inbound stream.

Tests:
- sbt "remote/testOnly org.apache.pekko.remote.artery.tcp.TcpFramingSpec" - 14 passed, incl. oversized, negative and at-limit frame cases
- sbt "remote/testOnly org.apache.pekko.remote.artery.LargeMessagesStreamSpec" - 4 passed (legitimate large frames still delivered)
- sbt "remote/mimaReportBinaryIssues" - no issues (TcpFraming is @internalapi; new params are defaulted)

References:
None - found while reviewing the draft threat model in apache#3478
Motivation:
Same test-coverage gap as on main: the bound tests from apache#3492 configure
only maximumFrameSize, so they cannot tell the per-stream bound
selection in ReadStreamId apart from a bug that applied one bound to
every stream.

Modification:
Add the two tests from apache#3524: with distinct maximumFrameSize and
maximumLargeFrameSize configured, a frame over the ordinary maximum but
within the large maximum is accepted on ArteryTransport.LargeStreamId
and rejected on ArteryTransport.OrdinaryStreamId. Both frames carry
their full declared payload, so a false accept cannot hide behind
truncation.

Result:
The per-stream bound selection is covered directly on 1.7.x as well.

Tests:
- sbt "remote/testOnly org.apache.pekko.remote.artery.tcp.TcpFramingSpec" - 17 passed
- sbt "++ 2.12.21 remote/Test/compile" - clean
- sbt "remote/scalafmtCheckAll" - clean

References:
Refs apache#3524
@pjfanning pjfanning added this to the 1.7.1 milestone Sep 3, 2026
@pjfanning
pjfanning merged commit 93e69ba into apache:1.7.x Sep 3, 2026
10 checks passed
@pjfanning
pjfanning deleted the tcp-framing-bound-17 branch September 3, 2026 17:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants