From b8b00bc63f3ae64f3ae1dc51b3b4887f1d3e3b8c Mon Sep 17 00:00:00 2001 From: Jens Geyer Date: Thu, 28 May 2026 00:51:06 +0200 Subject: [PATCH] THRIFT-6047: Harden PHP protocol recursion depth for struct read/write Client: php Co-Authored-By: Claude Sonnet 4.6 --- .../src/thrift/generate/t_php_generator.cc | 18 ++++++++++ lib/php/lib/Base/TBase.php | 10 ++++++ lib/php/lib/Protocol/TProtocol.php | 18 ++++++++++ .../test/Unit/Lib/Protocol/TProtocolTest.php | 34 +++++++++++++++++++ 4 files changed, 80 insertions(+) diff --git a/compiler/cpp/src/thrift/generate/t_php_generator.cc b/compiler/cpp/src/thrift/generate/t_php_generator.cc index 3b704a5a175..073bb9eb445 100644 --- a/compiler/cpp/src/thrift/generate/t_php_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_php_generator.cc @@ -1144,6 +1144,9 @@ void t_php_generator::generate_php_struct_reader(ostream& out, t_struct* tstruct // Declare stack tmp variables if (!binary_inline_) { + indent(out) << "$input->incrementRecursionDepth();" << '\n'; + indent(out) << "try {" << '\n'; + indent_up(); indent(out) << "$xfer += $input->readStructBegin($fname);" << '\n'; } @@ -1222,6 +1225,12 @@ void t_php_generator::generate_php_struct_reader(ostream& out, t_struct* tstruct if (!binary_inline_) { indent(out) << "$xfer += $input->readStructEnd();" << '\n'; + indent_down(); + indent(out) << "} finally {" << '\n'; + indent_up(); + indent(out) << "$input->decrementRecursionDepth();" << '\n'; + indent_down(); + indent(out) << "}" << '\n'; } if (needs_php_read_validator(tstruct, is_result)) { @@ -1265,6 +1274,9 @@ void t_php_generator::generate_php_struct_writer(ostream& out, t_struct* tstruct indent(out) << "$xfer = 0;" << '\n'; if (!binary_inline_) { + indent(out) << "$output->incrementRecursionDepth();" << '\n'; + indent(out) << "try {" << '\n'; + indent_up(); indent(out) << "$xfer += $output->writeStructBegin('" << name << "');" << '\n'; } @@ -1317,6 +1329,12 @@ void t_php_generator::generate_php_struct_writer(ostream& out, t_struct* tstruct } else { out << indent() << "$xfer += $output->writeFieldStop();" << '\n' << indent() << "$xfer += $output->writeStructEnd();" << '\n'; + indent_down(); + out << indent() << "} finally {" << '\n'; + indent_up(); + out << indent() << "$output->decrementRecursionDepth();" << '\n'; + indent_down(); + out << indent() << "}" << '\n'; } out << '\n'; diff --git a/lib/php/lib/Base/TBase.php b/lib/php/lib/Base/TBase.php index ae575a1461b..8e5430a2b19 100644 --- a/lib/php/lib/Base/TBase.php +++ b/lib/php/lib/Base/TBase.php @@ -216,6 +216,8 @@ protected function readStruct(string $class, array $spec, TProtocol $input): int $fname = null; $ftype = 0; $fid = 0; + $input->incrementRecursionDepth(); + try { $xfer += $input->readStructBegin($fname); while (true) { $xfer += $input->readFieldBegin($fname, $ftype, $fid); @@ -257,6 +259,9 @@ protected function readStruct(string $class, array $spec, TProtocol $input): int $xfer += $input->readFieldEnd(); } $xfer += $input->readStructEnd(); + } finally { + $input->decrementRecursionDepth(); + } return $xfer; } @@ -380,6 +385,8 @@ private function writeList(array $var, array $spec, TProtocol $output, bool $set protected function writeStruct(string $class, array $spec, TProtocol $output): int { $xfer = 0; + $output->incrementRecursionDepth(); + try { $xfer += $output->writeStructBegin($class); foreach ($spec as $fid => $fspec) { $var = $fspec['var']; @@ -410,6 +417,9 @@ protected function writeStruct(string $class, array $spec, TProtocol $output): i } $xfer += $output->writeFieldStop(); $xfer += $output->writeStructEnd(); + } finally { + $output->decrementRecursionDepth(); + } return $xfer; } diff --git a/lib/php/lib/Protocol/TProtocol.php b/lib/php/lib/Protocol/TProtocol.php index 42eaf71dee6..3eafd93dbd4 100644 --- a/lib/php/lib/Protocol/TProtocol.php +++ b/lib/php/lib/Protocol/TProtocol.php @@ -35,6 +35,10 @@ */ abstract class TProtocol { + public const DEFAULT_RECURSION_DEPTH = 64; + + private int $recursionDepth = 0; + protected function __construct(protected TTransport $trans) { } @@ -44,6 +48,20 @@ public function getTransport(): TTransport return $this->trans; } + public function incrementRecursionDepth(): void + { + ++$this->recursionDepth; + if ($this->recursionDepth > self::DEFAULT_RECURSION_DEPTH) { + --$this->recursionDepth; + throw new TProtocolException('Maximum recursion depth exceeded', TProtocolException::DEPTH_LIMIT); + } + } + + public function decrementRecursionDepth(): void + { + --$this->recursionDepth; + } + abstract public function writeMessageBegin(string $name, int $type, int $seqid): int; abstract public function writeMessageEnd(): int; diff --git a/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php b/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php index e0d8f4ba43c..7d5e5baa07e 100644 --- a/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php +++ b/lib/php/test/Unit/Lib/Protocol/TProtocolTest.php @@ -264,6 +264,40 @@ public function testSkipBinaryThrowsForUnknownType(): void TProtocol::skipBinary(new TMemoryBuffer(), 999); } + public function testIncrementRecursionDepthAllowsUpToLimit(): void + { + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + for ($i = 0; $i < TProtocol::DEFAULT_RECURSION_DEPTH; $i++) { + $protocol->incrementRecursionDepth(); + } + $this->assertTrue(true); // no exception thrown + } + + public function testIncrementRecursionDepthThrowsAtLimit(): void + { + $this->expectException(TProtocolException::class); + $this->expectExceptionCode(TProtocolException::DEPTH_LIMIT); + + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + for ($i = 0; $i <= TProtocol::DEFAULT_RECURSION_DEPTH; $i++) { + $protocol->incrementRecursionDepth(); + } + } + + public function testDecrementRecursionDepthRestoresCapacity(): void + { + $transport = new TMemoryBuffer(); + $protocol = new TBinaryProtocol($transport); + for ($i = 0; $i < TProtocol::DEFAULT_RECURSION_DEPTH; $i++) { + $protocol->incrementRecursionDepth(); + } + $protocol->decrementRecursionDepth(); + $protocol->incrementRecursionDepth(); // should not throw + $this->assertTrue(true); + } + private function buildBinaryBuffer(callable $writer): string { $transport = new TMemoryBuffer();