Skip to content

THRIFT-6054: Limit recursion depth in Kotlin struct, union and exception read/write#3559

Open
Jens-G wants to merge 1 commit into
apache:masterfrom
Jens-G:kotlin-recursion-depth
Open

THRIFT-6054: Limit recursion depth in Kotlin struct, union and exception read/write#3559
Jens-G wants to merge 1 commit into
apache:masterfrom
Jens-G:kotlin-recursion-depth

Conversation

@Jens-G
Copy link
Copy Markdown
Member

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

THRIFT-6054: Limit recursion depth in Kotlin struct, union and exception read/write

Problem

Kotlin-generated struct and exception serialization routes through the shared Java runtime's TProtocol.readStruct() / writeStruct() callback helpers, which did not bound recursion depth. Kotlin-generated unions extend the shared org.apache.thrift.TUnion, whose read/write path calls readStructBegin() / writeStructBegin() directly and was likewise unbounded. A deeply nested or cyclic value could therefore be read or written without a limit.

Change

  • Wrap the readStruct() / writeStruct() callback bodies (consumed by Kotlin-generated structs and exceptions) in incrementRecursionDepth() / try … finally / decrementRecursionDepth().
  • Wrap TUnion's standard- and tuple-scheme read/write bodies the same way, closing the union path.

Both reuse the existing TProtocol infrastructure: limit from TConfiguration.getRecursionLimit() (default 64), TProtocolException.DEPTH_LIMIT on excess. The Java generator emits an inline StandardScheme that already increments separately and does not call the readStruct()/writeStruct() helpers, so Java struct serialization is unaffected (no double-counting). A union's standard and tuple schemes are mutually exclusive per protocol, so a union increments exactly once per level.

Notes for committer review

  • The TUnion change lives in the shared lib/java runtime, so it also bounds Java-generated union recursion (not only Kotlin). Consequently Java union writes become bounded, whereas Java struct writes remain unbounded (the Java generator guards reads only). This asymmetry is intentional here but flagged for review.
  • The TUnion tuple-scheme guard is not exercised by the test (which drives TBinaryProtocol, i.e. the standard scheme); it mirrors the verified standard-scheme pattern exactly.

Test

Replaces the isolated counter test with a generated-code round-trip (lib/kotlin RecursionDepthTest). Recursive types CoRec/CoRec2/RecTree/RecUnion/RecError are generated from src/test/resources/RecursionDepthTest.thrift (mirroring test/Recursive.thrift) and driven through TBase.read()/write():

  • chains at the limit round-trip; chains one past it (write and read) are rejected with DEPTH_LIMIT — for structs, unions and exceptions;
  • a wide structure confirms the counter is decremented per sibling;
  • a cyclic graph is rejected instead of recursing without bound.

12/12 cases pass with the fix; reverting only TUnion.java makes exactly the two union over-limit cases fail while struct and exception cases still pass — confirming the union gap is real, the fix is load-bearing, and exceptions ride the existing TProtocol guard.

🤖 Generated with Claude Code

@Jens-G Jens-G requested review from fishy and jimexist as code owners May 28, 2026 11:47
@mergeable mergeable Bot added java Pull requests that update Java code kotlin labels May 28, 2026
@Jens-G Jens-G marked this pull request as draft May 28, 2026 22:45
@Jens-G Jens-G force-pushed the kotlin-recursion-depth branch from 7334003 to 997d45b Compare May 30, 2026 12:30
@Jens-G Jens-G changed the title THRIFT-6054: Limit struct read/write recursion depth in Kotlin library THRIFT-6054: Limit recursion depth in Kotlin struct read/write May 30, 2026
@Jens-G Jens-G marked this pull request as ready for review May 30, 2026 12:30
…ion read/write

Client: java,kotlin

Kotlin-generated struct and exception serialization routes through
TProtocol.readStruct() and writeStruct() (the callback helpers in the shared
Java runtime), which did not bound recursion depth, so a deeply nested or
cyclic value was read or written without a limit. Kotlin-generated unions
extend the shared org.apache.thrift.TUnion, whose read/write path calls
readStructBegin()/writeStructBegin() directly and was likewise unbounded --
this affects Java-generated unions too.

Wrap the readStruct()/writeStruct() bodies, and TUnion's standard- and
tuple-scheme read/write bodies, in incrementRecursionDepth() / try ... finally /
decrementRecursionDepth(), reusing the existing TProtocol infrastructure (limit
from TConfiguration.getRecursionLimit(), default 64, TProtocolException.DEPTH_LIMIT
on excess). The Java generator emits an inline StandardScheme that already
increments separately and does not call the readStruct()/writeStruct() helpers,
so Java struct serialization is unaffected (no double-counting); only
Kotlin-generated code consumes them. A union's standard and tuple schemes are
mutually exclusive per protocol, so a union increments exactly once per level.

Replace the isolated counter test with a generated-code round-trip
(lib/kotlin RecursionDepthTest): the recursive types
CoRec/CoRec2/RecTree/RecUnion/RecError are generated from a new
src/test/resources/RecursionDepthTest.thrift (mirroring test/Recursive.thrift)
and driven through TBase.read()/write() -- chains at the limit round-trip,
chains one past it (write and read) are rejected with DEPTH_LIMIT for structs,
unions and exceptions, a wide structure confirms the counter is decremented per
sibling, and a cyclic graph is rejected instead of recursing without bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Jens-G Jens-G force-pushed the kotlin-recursion-depth branch from 997d45b to 3abeffe Compare June 1, 2026 17:28
@Jens-G Jens-G changed the title THRIFT-6054: Limit recursion depth in Kotlin struct read/write THRIFT-6054: Limit recursion depth in Kotlin struct, union and exception read/write Jun 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

java Pull requests that update Java code kotlin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant