From 103cd5558f039ad2d624ffa09fa3ceb2b5f70794 Mon Sep 17 00:00:00 2001 From: Jens Geyer Date: Thu, 28 May 2026 02:14:04 +0200 Subject: [PATCH] THRIFT-6056: Limit recursion depth in Dart struct read/write Client: dart Add _recursionDepth counter and incrementRecursionDepth() / decrementRecursionDepth() methods to TProtocol, with a hard limit of 64. Update the Dart generator to call these methods around the struct read/write body using try/finally so the counter is always restored. Co-Authored-By: Claude Sonnet 4.6 --- .../src/thrift/generate/t_dart_generator.cc | 30 ++++- lib/dart/lib/src/protocol/t_protocol.dart | 15 +++ .../t_protocol_recursion_depth_test.dart | 103 ++++++++++++++++++ 3 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 lib/dart/test/protocol/t_protocol_recursion_depth_test.dart diff --git a/compiler/cpp/src/thrift/generate/t_dart_generator.cc b/compiler/cpp/src/thrift/generate/t_dart_generator.cc index 34f9d82e242..50c2341bc6e 100644 --- a/compiler/cpp/src/thrift/generate/t_dart_generator.cc +++ b/compiler/cpp/src/thrift/generate/t_dart_generator.cc @@ -889,6 +889,9 @@ void t_dart_generator::generate_dart_struct_reader(ostream& out, t_struct* tstru // Declare stack tmp variables and read struct header indent(out) << "TField field;" << '\n'; + indent(out) << "iprot.incrementRecursionDepth();" << '\n'; + indent(out) << "try"; + scope_up(out); indent(out) << "iprot.readStructBegin();" << '\n'; // Loop over reading in fields @@ -963,7 +966,12 @@ void t_dart_generator::generate_dart_struct_reader(ostream& out, t_struct* tstru // performs various checks (e.g. check that all required fields are set) indent(out) << "validate();" << '\n'; - scope_down(out, "\n\n"); + scope_down(out, " finally"); // close try, begin finally + scope_up(out); + indent(out) << "iprot.decrementRecursionDepth();" << '\n'; + scope_down(out); // close finally + + scope_down(out, "\n\n"); // close read() function } // generates dart method to perform various checks @@ -1029,6 +1037,9 @@ void t_dart_generator::generate_dart_struct_writer(ostream& out, t_struct* tstru // performs various checks (e.g. check that all required fields are set) indent(out) << "validate();" << '\n' << '\n'; + indent(out) << "oprot.incrementRecursionDepth();" << '\n'; + indent(out) << "try"; + scope_up(out); indent(out) << "oprot.writeStructBegin(_STRUCT_DESC);" << '\n'; for (f_iter = fields.begin(); f_iter != fields.end(); ++f_iter) { @@ -1064,7 +1075,12 @@ void t_dart_generator::generate_dart_struct_writer(ostream& out, t_struct* tstru indent(out) << "oprot.writeFieldStop();" << '\n' << indent() << "oprot.writeStructEnd();" << '\n'; - scope_down(out, "\n\n"); + scope_down(out, " finally"); // close try, begin finally + scope_up(out); + indent(out) << "oprot.decrementRecursionDepth();" << '\n'; + scope_down(out); // close finally + + scope_down(out, "\n\n"); // close write() function } /** @@ -1082,6 +1098,9 @@ void t_dart_generator::generate_dart_struct_result_writer(ostream& out, t_struct const vector& fields = tstruct->get_sorted_members(); vector::const_iterator f_iter; + indent(out) << "oprot.incrementRecursionDepth();" << '\n'; + indent(out) << "try"; + scope_up(out); indent(out) << "oprot.writeStructBegin(_STRUCT_DESC);" << '\n' << '\n'; bool first = true; @@ -1113,7 +1132,12 @@ void t_dart_generator::generate_dart_struct_result_writer(ostream& out, t_struct indent(out) << "oprot.writeFieldStop();" << '\n' << indent() << "oprot.writeStructEnd();" << '\n'; - scope_down(out, "\n\n"); + scope_down(out, " finally"); // close try, begin finally + scope_up(out); + indent(out) << "oprot.decrementRecursionDepth();" << '\n'; + scope_down(out); // close finally + + scope_down(out, "\n\n"); // close write() function } void t_dart_generator::generate_generic_field_getters(std::ostream& out, diff --git a/lib/dart/lib/src/protocol/t_protocol.dart b/lib/dart/lib/src/protocol/t_protocol.dart index f49c0321d76..7dfda870779 100644 --- a/lib/dart/lib/src/protocol/t_protocol.dart +++ b/lib/dart/lib/src/protocol/t_protocol.dart @@ -20,8 +20,23 @@ part of thrift; abstract class TProtocol { final TTransport transport; + int _recursionDepth = 0; + static const int _defaultRecursionDepth = 64; + TProtocol(this.transport); + void incrementRecursionDepth() { + if (_recursionDepth >= _defaultRecursionDepth) { + throw TProtocolError( + TProtocolErrorType.DEPTH_LIMIT, "Maximum recursion depth exceeded"); + } + _recursionDepth++; + } + + void decrementRecursionDepth() { + _recursionDepth--; + } + /// Write void writeMessageBegin(TMessage message); void writeMessageEnd(); diff --git a/lib/dart/test/protocol/t_protocol_recursion_depth_test.dart b/lib/dart/test/protocol/t_protocol_recursion_depth_test.dart new file mode 100644 index 00000000000..b5fcffcd476 --- /dev/null +++ b/lib/dart/test/protocol/t_protocol_recursion_depth_test.dart @@ -0,0 +1,103 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +library thrift.test.protocol.t_protocol_recursion_depth_test; + +import 'package:test/test.dart'; +import 'package:thrift/thrift.dart'; + +void main() { + late TProtocol protocol; + + setUp(() { + protocol = TBinaryProtocol(TBufferedTransport()); + }); + + test('64 increments succeed', () { + for (var i = 0; i < 64; i++) { + protocol.incrementRecursionDepth(); + } + for (var i = 0; i < 64; i++) { + protocol.decrementRecursionDepth(); + } + }); + + test('65th increment throws DEPTH_LIMIT', () { + for (var i = 0; i < 64; i++) { + protocol.incrementRecursionDepth(); + } + expect( + () => protocol.incrementRecursionDepth(), + throwsA(predicate((e) => + e is TProtocolError && + e.type == TProtocolErrorType.DEPTH_LIMIT)), + ); + for (var i = 0; i < 64; i++) { + protocol.decrementRecursionDepth(); + } + }); + + test('exception message is non-empty', () { + for (var i = 0; i < 64; i++) { + protocol.incrementRecursionDepth(); + } + try { + protocol.incrementRecursionDepth(); + fail('Expected TProtocolError'); + } on TProtocolError catch (e) { + expect(e.message, isNotEmpty); + } + for (var i = 0; i < 64; i++) { + protocol.decrementRecursionDepth(); + } + }); + + test('counter not changed by failed increment', () { + for (var i = 0; i < 64; i++) { + protocol.incrementRecursionDepth(); + } + try { + protocol.incrementRecursionDepth(); + } on TProtocolError { + // expected + } + // depth still at 64, so next increment must also throw + expect( + () => protocol.incrementRecursionDepth(), + throwsA(isA()), + ); + for (var i = 0; i < 64; i++) { + protocol.decrementRecursionDepth(); + } + }); + + test('balanced increment/decrement resets counter', () { + for (var i = 0; i < 32; i++) { + protocol.incrementRecursionDepth(); + } + for (var i = 0; i < 32; i++) { + protocol.decrementRecursionDepth(); + } + // At depth 0, 64 more increments must succeed + for (var i = 0; i < 64; i++) { + protocol.incrementRecursionDepth(); + } + for (var i = 0; i < 64; i++) { + protocol.decrementRecursionDepth(); + } + }); +}