mirror of
https://github.com/protocolbuffers/protobuf
synced 2026-08-26 02:23:14 -04:00
[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:
parent
42c88bf5ce
commit
dd9d2ad02d
4 changed files with 120 additions and 1 deletions
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue