[ObjC] Carry recursion depth across MessageSet item parsing (#27571)

## Summary

`GPBMessage::parseMessageSet:` reads each MessageSet item's payload bytes into
a separate buffer (because the type-id and payload tags can appear in any
order), then allocates a fresh `GPBCodedInputStream` to decode that buffer. The
fresh stream's `state_.recursionDepth` starts at `0` even when the parent
stream is already several MessageSet items deep, so a chain of nested
MessageSet items grows the native call stack without ever tripping the
documented `kDefaultRecursionLimit` (100). Other parser entry points that
recurse on the same stream (`readGroup:`, `readMessage:`, `readMapEntry:`,
`SkipToEndGroupInternal`) already increment and check `recursionDepth`; the
MessageSet path was the only spot where depth tracking did not cross the stream
boundary.

This change adds a package-private `-initWithData:parentRecursionDepth:`
initializer to `GPBCodedInputStream` that seeds the child stream's depth from
the parent's and runs `CheckRecursionLimit` before parsing begins. To prevent
memory leaks if `CheckRecursionLimit` raises an exception during
initialization, the check is wrapped in a `@try/@catch` block that releases
`self`. `parseMessageSet:` uses that initializer, so depth tracking carries
across the fresh stream. This mirrors the depth-inheritance done by the C++
`ParseContext` spawn helper.

Regression coverage: `testParseMessageSetRecursionDepthCarriedFromParent` in
`GPBWireFormatTests` builds MessageSet-of-MessageSet payloads (each layer is
`MSetMessage -> MSetMessageExtension1.recursive -> MSetMessage -> ...`, adding
2 to the recursion depth per layer). It verifies that 50 layers (depth 100,
`kDefaultRecursionLimit`) parses successfully, while 51 layers (depth 101,
`kDefaultRecursionLimit + 1`) fails with
`GPBCodedInputStreamErrorRecursionDepthExceeded`. Existing
`testErrorRecursionDepthReached` and the rest of `GPBWireFormatTests` continue
to pass unchanged.

## Test plan

- Existing ObjC test suite passes (`GPBWireFormatTests`,
  `GPBMessageTests+Serialization`, `GPBCodedInputStreamTests`,
  `GPBUnknownFieldsTest`).
- New `testParseMessageSetRecursionDepthCarriedFromParent` passes for both 50
  layers (passing at kDefaultRecursionLimit) and 51 layers (failing at
  kDefaultRecursionLimit + 1).

Closes #27571

PiperOrigin-RevId: 959041690
This commit is contained in:
Tony Liao 2026-08-04 08:38:20 -07:00 committed by Copybara-Service
parent 42c88bf5ce
commit dd9d2ad02d
4 changed files with 120 additions and 1 deletions

View file

@ -389,6 +389,29 @@ void GPBCodedInputStreamCheckLastTagWas(GPBCodedInputStreamState *state, int32_t
return self;
}
- (instancetype)initWithData:(NSData *)data parentRecursionDepth:(NSUInteger)parentDepth {
if ((self = [self initWithData:data])) {
// The parent stream had already entered `parentDepth` nested parses; we
// are about to begin one more level in this child stream, so seed the
// depth accordingly and verify the limit before parsing starts. This
// matches the convention used by the C++ ParseContext spawn helper,
// which increments and checks the depth before recursing into a payload
// that has been read into a fresh buffer.
state_.recursionDepth = parentDepth + 1;
@try {
CheckRecursionLimit(&state_);
} @catch (NSException *exception) {
// If CheckRecursionLimit raises an exception (when recursion depth exceeds
// kDefaultRecursionLimit), `self` will not be returned to the caller.
// Explicitly release `self` here to avoid a memory leak before re-throwing.
[self release];
self = nil;
@throw;
}
}
return self;
}
- (void)dealloc {
[buffer_ release];
[super dealloc];

View file

@ -33,6 +33,16 @@ typedef struct GPBCodedInputStreamState {
NSData *buffer_;
}
// Initializes a new stream over `data` whose initial recursion depth is one
// deeper than `parentDepth`. Used when a parser needs to spawn a fresh
// CodedInputStream to decode a payload that has already been read into a
// separate buffer (e.g. MessageSet items), so that the native call stack
// growth is still bounded by kDefaultRecursionLimit. The initializer raises
// GPBCodedInputStreamErrorRecursionDepthExceeded if `parentDepth` is already
// at the limit. Mirrors the depth-inheritance done by the C++ ParseContext
// spawn helper.
- (instancetype)initWithData:(NSData *)data parentRecursionDepth:(NSUInteger)parentDepth;
// Group support is deprecated, so we hide this interface from users, but
// support for older data.
- (void)readGroup:(int32_t)fieldNumber

View file

@ -2444,7 +2444,16 @@ void GPBClearMessageAutocreator(GPBMessage *self) {
[self setExtension:extension value:targetMessage];
[targetMessage release];
}
GPBCodedInputStream *newInput = [[GPBCodedInputStream alloc] initWithData:rawBytes];
// Parsing the MessageSet item payload requires a fresh CodedInputStream
// because the payload bytes are read into a separate buffer before the
// item's type-id and payload tags are correlated. Carry the parent
// stream's recursion depth across to the child stream so that nested
// MessageSet items (which would otherwise reset the depth to 0 on every
// hop) remain bounded by kDefaultRecursionLimit. This mirrors the
// depth-inheritance behavior of the C++ ParseContext spawn helper.
GPBCodedInputStream *newInput =
[[GPBCodedInputStream alloc] initWithData:rawBytes
parentRecursionDepth:input->state_.recursionDepth];
@try {
[targetMessage mergeFromCodedInputStream:newInput
extensionRegistry:extensionRegistry

View file

@ -428,4 +428,81 @@ const int kUnknownTypeId2 = 1550056;
}
}
static NSData* MessageSetDataWithLayers(NSUInteger layers) {
MSetMessage* innermost = [MSetMessage message];
MSetMessageExtension1* innermostExt = [MSetMessageExtension1 message];
innermostExt.i = 1;
#if defined(GPB_UNITTEST_USE_C_FUNCTION_FOR_EXTENSIONS)
[innermost setExtension:MSetMessageExtension1_extension_MessageSetExtension() value:innermostExt];
#else
[innermost setExtension:[MSetMessageExtension1 messageSetExtension] value:innermostExt];
#endif
MSetMessage* current = innermost;
for (NSUInteger i = 1; i < layers; ++i) {
MSetMessageExtension1* ext = [MSetMessageExtension1 message];
ext.recursive = current;
MSetMessage* parent = [MSetMessage message];
#if defined(GPB_UNITTEST_USE_C_FUNCTION_FOR_EXTENSIONS)
[parent setExtension:MSetMessageExtension1_extension_MessageSetExtension() value:ext];
#else
[parent setExtension:[MSetMessageExtension1 messageSetExtension] value:ext];
#endif
current = parent;
}
return [current data];
}
- (void)testParseMessageSetRecursionDepthCarriedFromParent {
// Each MSetMessage carries a single MSetMessageExtension1, whose
// `recursive` field is again a MSetMessage. Chaining N of these produces
// a MessageSet-of-MessageSet payload nested N levels deep. The parser
// for each MessageSet item allocates a fresh CodedInputStream, so depth
// tracking has to be inherited across those streams for the documented
// kDefaultRecursionLimit (100) to actually apply.
//
// Each layer increases recursion depth by 2 (+1 for the child CodedInputStream
// in parseMessageSet:, +1 for the `recursive` message field in readMessage:).
// 50 layers reaches depth 100 (kDefaultRecursionLimit), which must parse successfully.
// 51 layers attempts to reach depth 101 (kDefaultRecursionLimit + 1), which must fail
// with GPBCodedInputStreamErrorRecursionDepthExceeded rather than silently parsing.
const NSUInteger kPassLayers = 50;
NSData* passData = MessageSetDataWithLayers(kPassLayers);
XCTAssertNotNil(passData);
NSError* error = nil;
#if defined(GPB_UNITTEST_USE_C_FUNCTION_FOR_EXTENSIONS)
MSetMessage* passParsed =
[MSetMessage parseFromData:passData
extensionRegistry:MSet_Objc_Protobuf_Tests_Mset_MSetUnittestMsetRoot_Registry()
error:&error];
#else
MSetMessage* passParsed = [MSetMessage parseFromData:passData
extensionRegistry:[MSetUnittestMsetRoot extensionRegistry]
error:&error];
#endif
XCTAssertNotNil(passParsed);
XCTAssertNil(error);
const NSUInteger kFailLayers = 51;
NSData* failData = MessageSetDataWithLayers(kFailLayers);
XCTAssertNotNil(failData);
error = nil;
#if defined(GPB_UNITTEST_USE_C_FUNCTION_FOR_EXTENSIONS)
MSetMessage* failParsed =
[MSetMessage parseFromData:failData
extensionRegistry:MSet_Objc_Protobuf_Tests_Mset_MSetUnittestMsetRoot_Registry()
error:&error];
#else
MSetMessage* failParsed = [MSetMessage parseFromData:failData
extensionRegistry:[MSetUnittestMsetRoot extensionRegistry]
error:&error];
#endif
XCTAssertNil(failParsed);
XCTAssertNotNil(error);
XCTAssertEqualObjects(error.domain, GPBCodedInputStreamErrorDomain);
XCTAssertEqual(error.code, GPBCodedInputStreamErrorRecursionDepthExceeded);
}
@end