Skip to content

THRIFT-6053: Limit struct read/write recursion depth in D library#3558

Merged
Jens-G merged 1 commit into
apache:masterfrom
Jens-G:d-recursion-depth
May 30, 2026
Merged

THRIFT-6053: Limit struct read/write recursion depth in D library#3558
Jens-G merged 1 commit into
apache:masterfrom
Jens-G:d-recursion-depth

Conversation

@Jens-G
Copy link
Copy Markdown
Member

@Jens-G Jens-G commented May 28, 2026

Summary

THRIFT-6053 — bound the recursion depth of D struct read/write.

D struct serialization runs through the readStruct / writeStruct templates
in thrift.codegen.base — the generated read() / write() methods forward to
them. On master these templates do not bound recursion depth, so a deeply
nested message is read or written without a limit.

This PR wraps the readStruct / writeStruct bodies with a thread-local depth
counter that throws TProtocolException.DEPTH_LIMIT once nesting exceeds 64
levels, decrementing on the way out via scope(exit). The counter is a
module-level variable and therefore thread-local in D, so concurrent
(de)serialization on separate threads is unaffected. Unknown-field skipping
(skip()) has its own, separate traversal and is left unchanged.

Test

The previous test set the depth counter directly and called readStruct /
writeStruct once with a null protocol — it never serialized a nested struct.
It is replaced by a round-trip unittest in thrift.codegen.base that drives
the generated struct read/write path with a self-recursive RecTree, mirroring
test/Recursive.thrift.

The CoRec / CoRec2 / RecList types in Recursive.thrift recurse by
value
and cannot be expressed as D structs at all (a struct cannot contain
itself by value). RecTree, which nests through a list, is the
representable case; a cyclic object graph is likewise unconstructible with D
value structs, so the equivalent case — an arbitrarily deep wire payload —
is covered by the crafted read below.

case expectation
chain at the limit (64) round-trips
chain one over (65), write DEPTH_LIMIT
chain one over (65), read (payload crafted with raw protocol primitives) DEPTH_LIMIT
wide shallow tree (192 children) round-trips (counter decremented per sibling)

Validation (dmd -unittest, DMD 2.087):

  • with this PR: all 7 thrift.codegen.base unittests pass.
  • with the depth guard removed (master behaviour): the round-trip test fails
    at "writing past the limit must throw" — an over-limit chain serializes with
    no DEPTH_LIMIT — confirming the gap this change closes.

D is excluded from the Linux CI matrix (--without-d), so this was validated
locally with DMD.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

@mergeable mergeable Bot added the d Pull requests that update D code label May 28, 2026
@Jens-G Jens-G marked this pull request as draft May 28, 2026 22:45
Client: d

D struct serialization runs through the readStruct/writeStruct templates in
thrift.codegen.base (the generated read()/write() methods forward to them).
These did not bound recursion depth, so a deeply nested message was read or
written without a limit.

Wrap the readStruct/writeStruct bodies with a thread-local depth counter that
throws TProtocolException.DEPTH_LIMIT once nesting exceeds 64 levels and is
decremented on the way out via scope(exit). The counter is a module-level
variable and therefore thread-local in D, so concurrent (de)serialization on
separate threads is unaffected. Unknown-field skipping (skip()) has its own,
separate traversal and is left unchanged.

Replace the isolated counter test with a round-trip unittest in
thrift.codegen.base that drives the generated struct read/write path with a
self-recursive RecTree, mirroring test/Recursive.thrift. (The CoRec/CoRec2/
RecList types there recurse by value and cannot be expressed as D structs, so
RecTree -- which nests through a list -- is the representable case.) A chain at
the limit round-trips; a chain one level over is rejected with DEPTH_LIMIT on
both write and read (the over-limit read payload is crafted with raw protocol
primitives, since a normal write would itself be rejected); and a wide, shallow
tree round-trips, confirming the counter is decremented per sibling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Jens-G Jens-G force-pushed the d-recursion-depth branch from 7965608 to c89d8f0 Compare May 30, 2026 14:40
@Jens-G Jens-G marked this pull request as ready for review May 30, 2026 14:40
@Jens-G Jens-G merged commit 5e29f31 into apache:master May 30, 2026
87 of 89 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

d Pull requests that update D code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant