From 4728531c162f2f9e8c2ca1add713cfee2db6be3b Mon Sep 17 00:00:00 2001
From: Protobuf Team Bot <protobuf-github-bot@google.com>
Date: Tue, 17 Sep 2024 12:03:36 -0700
Subject: [PATCH] Add recursion check when parsing unknown fields in Java.

PiperOrigin-RevId: 675657198
---
 .../com/google/protobuf/ArrayDecoders.java    |  28 +++
 .../com/google/protobuf/CodedInputStream.java |  18 +-
 .../com/google/protobuf/MessageSchema.java    |  12 +-
 .../com/google/protobuf/MessageSetSchema.java |   3 +-
 .../google/protobuf/UnknownFieldSchema.java   |  29 ++-
 .../google/protobuf/CodedInputStreamTest.java | 158 ++++++++++++
 .../java/com/google/protobuf/LiteTest.java    | 232 ++++++++++++++++++
 7 files changed, 466 insertions(+), 14 deletions(-)

diff --git a/java/core/src/main/java/com/google/protobuf/ArrayDecoders.java b/java/core/src/main/java/com/google/protobuf/ArrayDecoders.java
index 9bf1439..bf5f922 100644
--- a/java/core/src/main/java/com/google/protobuf/ArrayDecoders.java
+++ b/java/core/src/main/java/com/google/protobuf/ArrayDecoders.java
@@ -23,6 +23,10 @@ import java.io.IOException;
  */
 @CheckReturnValue
 final class ArrayDecoders {
+  static final int DEFAULT_RECURSION_LIMIT = 100;
+
+  @SuppressWarnings("NonFinalStaticField")
+  private static volatile int recursionLimit = DEFAULT_RECURSION_LIMIT;
 
   private ArrayDecoders() {}
 
@@ -37,6 +41,7 @@ final class ArrayDecoders {
     public long long1;
     public Object object1;
     public final ExtensionRegistryLite extensionRegistry;
+    public int recursionDepth;
 
     Registers() {
       this.extensionRegistry = ExtensionRegistryLite.getEmptyRegistry();
@@ -244,7 +249,10 @@ final class ArrayDecoders {
     if (length < 0 || length > limit - position) {
       throw InvalidProtocolBufferException.truncatedMessage();
     }
+    registers.recursionDepth++;
+    checkRecursionLimit(registers.recursionDepth);
     schema.mergeFrom(msg, data, position, position + length, registers);
+    registers.recursionDepth--;
     registers.object1 = msg;
     return position + length;
   }
@@ -262,8 +270,11 @@ final class ArrayDecoders {
     // A group field must has a MessageSchema (the only other subclass of Schema is MessageSetSchema
     // and it can't be used in group fields).
     final MessageSchema messageSchema = (MessageSchema) schema;
+    registers.recursionDepth++;
+    checkRecursionLimit(registers.recursionDepth);
     final int endPosition =
         messageSchema.parseMessage(msg, data, position, limit, endGroup, registers);
+    registers.recursionDepth--;
     registers.object1 = msg;
     return endPosition;
   }
@@ -1024,6 +1035,8 @@ final class ArrayDecoders {
         final UnknownFieldSetLite child = UnknownFieldSetLite.newInstance();
         final int endGroup = (tag & ~0x7) | WireFormat.WIRETYPE_END_GROUP;
         int lastTag = 0;
+        registers.recursionDepth++;
+        checkRecursionLimit(registers.recursionDepth);
         while (position < limit) {
           position = decodeVarint32(data, position, registers);
           lastTag = registers.int1;
@@ -1032,6 +1045,7 @@ final class ArrayDecoders {
           }
           position = decodeUnknownField(lastTag, data, position, limit, child, registers);
         }
+        registers.recursionDepth--;
         if (position > limit || lastTag != endGroup) {
           throw InvalidProtocolBufferException.parseFailure();
         }
@@ -1078,4 +1092,18 @@ final class ArrayDecoders {
         throw InvalidProtocolBufferException.invalidTag();
     }
   }
+
+  /**
+   * Set the maximum recursion limit that ArrayDecoders will allow. An exception will be thrown if
+   * the depth of the message exceeds this limit.
+   */
+  public static void setRecursionLimit(int limit) {
+    recursionLimit = limit;
+  }
+
+  private static void checkRecursionLimit(int depth) throws InvalidProtocolBufferException {
+    if (depth >= recursionLimit) {
+      throw InvalidProtocolBufferException.recursionLimitExceeded();
+    }
+  }
 }
diff --git a/java/core/src/main/java/com/google/protobuf/CodedInputStream.java b/java/core/src/main/java/com/google/protobuf/CodedInputStream.java
index 8f1ac73..f8df4d3 100644
--- a/java/core/src/main/java/com/google/protobuf/CodedInputStream.java
+++ b/java/core/src/main/java/com/google/protobuf/CodedInputStream.java
@@ -2180,7 +2180,14 @@ public abstract class CodedInputStream {
     public void skipMessage() throws IOException {
       while (true) {
         final int tag = readTag();
-        if (tag == 0 || !skipField(tag)) {
+        if (tag == 0 ) {
+          return;
+        }
+	checkRecursionLimit();
+	++recursionDepth;
+	boolean fieldSkipped = skipField(tag);
+	--recursionDepth;
+        if (!fieldSkipped) {
           return;
         }
       }
@@ -2190,9 +2197,16 @@ public abstract class CodedInputStream {
     public void skipMessage(CodedOutputStream output) throws IOException {
       while (true) {
         final int tag = readTag();
-        if (tag == 0 || !skipField(tag, output)) {
+        if (tag == 0) {
           return;
         }
+	checkRecursionLimit();
+        ++recursionDepth;
+	boolean fieldSkipped = skipField(tag, output);
+        --recursionDepth;
+        if (!fieldSkipped) {
+          return;
+	}
       }
     }
 
diff --git a/java/core/src/main/java/com/google/protobuf/MessageSchema.java b/java/core/src/main/java/com/google/protobuf/MessageSchema.java
index f8f79fc..5ad6762 100644
--- a/java/core/src/main/java/com/google/protobuf/MessageSchema.java
+++ b/java/core/src/main/java/com/google/protobuf/MessageSchema.java
@@ -3006,8 +3006,8 @@ final class MessageSchema<T> implements Schema<T> {
               unknownFields = unknownFieldSchema.getBuilderFromMessage(message);
             }
             // Unknown field.
-
-            if (unknownFieldSchema.mergeOneFieldFrom(unknownFields, reader)) {
+            if (unknownFieldSchema.mergeOneFieldFrom(
+                unknownFields, reader, /* currentDepth= */ 0)) {
               continue;
             }
           }
@@ -3382,8 +3382,8 @@ final class MessageSchema<T> implements Schema<T> {
               if (unknownFields == null) {
                 unknownFields = unknownFieldSchema.getBuilderFromMessage(message);
               }
-
-              if (!unknownFieldSchema.mergeOneFieldFrom(unknownFields, reader)) {
+              if (!unknownFieldSchema.mergeOneFieldFrom(
+                  unknownFields, reader, /* currentDepth= */ 0)) {
                 return;
               }
               break;
@@ -3399,8 +3399,8 @@ final class MessageSchema<T> implements Schema<T> {
             if (unknownFields == null) {
               unknownFields = unknownFieldSchema.getBuilderFromMessage(message);
             }
-
-            if (!unknownFieldSchema.mergeOneFieldFrom(unknownFields, reader)) {
+            if (!unknownFieldSchema.mergeOneFieldFrom(
+                unknownFields, reader, /* currentDepth= */ 0)) {
               return;
             }
           }
diff --git a/java/core/src/main/java/com/google/protobuf/MessageSetSchema.java b/java/core/src/main/java/com/google/protobuf/MessageSetSchema.java
index a17037e..ec37d41 100644
--- a/java/core/src/main/java/com/google/protobuf/MessageSetSchema.java
+++ b/java/core/src/main/java/com/google/protobuf/MessageSetSchema.java
@@ -278,8 +278,7 @@ final class MessageSetSchema<T> implements Schema<T> {
               reader, extension, extensionRegistry, extensions);
           return true;
         } else {
-
-          return unknownFieldSchema.mergeOneFieldFrom(unknownFields, reader);
+          return unknownFieldSchema.mergeOneFieldFrom(unknownFields, reader, /* currentDepth= */ 0);
         }
       } else {
         return reader.skipField();
diff --git a/java/core/src/main/java/com/google/protobuf/UnknownFieldSchema.java b/java/core/src/main/java/com/google/protobuf/UnknownFieldSchema.java
index a43bc2a..80602b1 100644
--- a/java/core/src/main/java/com/google/protobuf/UnknownFieldSchema.java
+++ b/java/core/src/main/java/com/google/protobuf/UnknownFieldSchema.java
@@ -13,6 +13,11 @@ import java.io.IOException;
 @CheckReturnValue
 abstract class UnknownFieldSchema<T, B> {
 
+  static final int DEFAULT_RECURSION_LIMIT = 100;
+
+  @SuppressWarnings("NonFinalStaticField")
+  private static volatile int recursionLimit = DEFAULT_RECURSION_LIMIT;
+
   /** Whether unknown fields should be dropped. */
   abstract boolean shouldDiscardUnknownFields(Reader reader);
 
@@ -55,7 +60,9 @@ abstract class UnknownFieldSchema<T, B> {
   /** Marks unknown fields as immutable. */
   abstract void makeImmutable(Object message);
 
-  final boolean mergeOneFieldFrom(B unknownFields, Reader reader) throws IOException {
+  /** Merges one field into the unknown fields. */
+  final boolean mergeOneFieldFrom(B unknownFields, Reader reader, int currentDepth)
+      throws IOException {
     int tag = reader.getTag();
     int fieldNumber = WireFormat.getTagFieldNumber(tag);
     switch (WireFormat.getTagWireType(tag)) {
@@ -74,7 +81,12 @@ abstract class UnknownFieldSchema<T, B> {
       case WireFormat.WIRETYPE_START_GROUP:
         final B subFields = newBuilder();
         int endGroupTag = WireFormat.makeTag(fieldNumber, WireFormat.WIRETYPE_END_GROUP);
-        mergeFrom(subFields, reader);
+        currentDepth++;
+        if (currentDepth >= recursionLimit) {
+          throw InvalidProtocolBufferException.recursionLimitExceeded();
+        }
+        mergeFrom(subFields, reader, currentDepth);
+        currentDepth--;
         if (endGroupTag != reader.getTag()) {
           throw InvalidProtocolBufferException.invalidEndTag();
         }
@@ -87,10 +99,11 @@ abstract class UnknownFieldSchema<T, B> {
     }
   }
 
-  private final void mergeFrom(B unknownFields, Reader reader) throws IOException {
+  private final void mergeFrom(B unknownFields, Reader reader, int currentDepth)
+      throws IOException {
     while (true) {
       if (reader.getFieldNumber() == Reader.READ_DONE
-          || !mergeOneFieldFrom(unknownFields, reader)) {
+          || !mergeOneFieldFrom(unknownFields, reader, currentDepth)) {
         break;
       }
     }
@@ -107,4 +120,12 @@ abstract class UnknownFieldSchema<T, B> {
   abstract int getSerializedSizeAsMessageSet(T message);
 
   abstract int getSerializedSize(T unknowns);
+
+  /**
+   * Set the maximum recursion limit that ArrayDecoders will allow. An exception will be thrown if
+   * the depth of the message exceeds this limit.
+   */
+  public void setRecursionLimit(int limit) {
+    recursionLimit = limit;
+  }
 }
diff --git a/java/core/src/test/java/com/google/protobuf/CodedInputStreamTest.java b/java/core/src/test/java/com/google/protobuf/CodedInputStreamTest.java
index 2de3273..70525bb 100644
--- a/java/core/src/test/java/com/google/protobuf/CodedInputStreamTest.java
+++ b/java/core/src/test/java/com/google/protobuf/CodedInputStreamTest.java
@@ -10,6 +10,9 @@ package com.google.protobuf;
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.common.truth.Truth.assertWithMessage;
 import static org.junit.Assert.assertArrayEquals;
+
+import com.google.common.primitives.Bytes;
+import map_test.MapTestProto.MapContainer;
 import protobuf_unittest.UnittestProto.BoolMessage;
 import protobuf_unittest.UnittestProto.Int32Message;
 import protobuf_unittest.UnittestProto.Int64Message;
@@ -34,6 +37,13 @@ public class CodedInputStreamTest {
 
   private static final int DEFAULT_BLOCK_SIZE = 4096;
 
+  private static final int GROUP_TAP = WireFormat.makeTag(3, WireFormat.WIRETYPE_START_GROUP);
+
+  private static final byte[] NESTING_SGROUP = generateSGroupTags();
+
+  private static final byte[] NESTING_SGROUP_WITH_INITIAL_BYTES = generateSGroupTagsForMapField();
+
+
   private enum InputType {
     ARRAY {
       @Override
@@ -116,6 +126,17 @@ public class CodedInputStreamTest {
     return bytes;
   }
 
+  private static byte[] generateSGroupTags() {
+    byte[] bytes = new byte[100000];
+    Arrays.fill(bytes, (byte) GROUP_TAP);
+    return bytes;
+  }
+
+  private static byte[] generateSGroupTagsForMapField() {
+    byte[] initialBytes = {18, 1, 75, 26, (byte) 198, (byte) 154, 12};
+    return Bytes.concat(initialBytes, NESTING_SGROUP);
+  }
+
   /**
    * An InputStream which limits the number of bytes it reads at a time. We use this to make sure
    * that CodedInputStream doesn't screw up when reading in small blocks.
@@ -659,6 +680,143 @@ public class CodedInputStreamTest {
     }
   }
 
+  @Test
+  public void testMaliciousRecursion_unknownFields() throws Exception {
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> TestRecursiveMessage.parseFrom(NESTING_SGROUP));
+
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousRecursion_skippingUnknownField() throws Exception {
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () ->
+                DiscardUnknownFieldsParser.wrap(TestRecursiveMessage.parser())
+                    .parseFrom(NESTING_SGROUP));
+
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousSGroupTagsWithMapField_fromInputStream() throws Exception {
+    Throwable parseFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () ->
+                MapContainer.parseFrom(
+                    new ByteArrayInputStream(NESTING_SGROUP_WITH_INITIAL_BYTES)));
+    Throwable mergeFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () ->
+                MapContainer.newBuilder()
+                    .mergeFrom(new ByteArrayInputStream(NESTING_SGROUP_WITH_INITIAL_BYTES)));
+
+    assertThat(parseFromThrown)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+    assertThat(mergeFromThrown)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousSGroupTags_inputStream_skipMessage() throws Exception {
+    ByteArrayInputStream inputSteam = new ByteArrayInputStream(NESTING_SGROUP);
+    CodedInputStream input = CodedInputStream.newInstance(inputSteam);
+    CodedOutputStream output = CodedOutputStream.newInstance(new byte[NESTING_SGROUP.length]);
+
+    Throwable thrown = assertThrows(InvalidProtocolBufferException.class, input::skipMessage);
+    Throwable thrown2 =
+        assertThrows(InvalidProtocolBufferException.class, () -> input.skipMessage(output));
+
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+    assertThat(thrown2)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousSGroupTagsWithMapField_fromByteArray() throws Exception {
+    Throwable parseFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.parseFrom(NESTING_SGROUP_WITH_INITIAL_BYTES));
+    Throwable mergeFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.newBuilder().mergeFrom(NESTING_SGROUP_WITH_INITIAL_BYTES));
+
+    assertThat(parseFromThrown)
+        .hasMessageThat()
+        .contains("the input ended unexpectedly in the middle of a field");
+    assertThat(mergeFromThrown)
+        .hasMessageThat()
+        .contains("the input ended unexpectedly in the middle of a field");
+  }
+
+  @Test
+  public void testMaliciousSGroupTags_arrayDecoder_skipMessage() throws Exception {
+    CodedInputStream input = CodedInputStream.newInstance(NESTING_SGROUP);
+    CodedOutputStream output = CodedOutputStream.newInstance(new byte[NESTING_SGROUP.length]);
+
+    Throwable thrown = assertThrows(InvalidProtocolBufferException.class, input::skipMessage);
+    Throwable thrown2 =
+        assertThrows(InvalidProtocolBufferException.class, () -> input.skipMessage(output));
+
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+    assertThat(thrown2)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousSGroupTagsWithMapField_fromByteBuffer() throws Exception {
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.parseFrom(ByteBuffer.wrap(NESTING_SGROUP_WITH_INITIAL_BYTES)));
+
+    assertThat(thrown)
+        .hasMessageThat()
+        .contains("the input ended unexpectedly in the middle of a field");
+  }
+
+  @Test
+  public void testMaliciousSGroupTags_byteBuffer_skipMessage() throws Exception {
+    CodedInputStream input = InputType.NIO_DIRECT.newDecoder(NESTING_SGROUP);
+    CodedOutputStream output = CodedOutputStream.newInstance(new byte[NESTING_SGROUP.length]);
+
+    Throwable thrown = assertThrows(InvalidProtocolBufferException.class, input::skipMessage);
+    Throwable thrown2 =
+        assertThrows(InvalidProtocolBufferException.class, () -> input.skipMessage(output));
+
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+    assertThat(thrown2)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousSGroupTags_iterableByteBuffer() throws Exception {
+    CodedInputStream input = InputType.ITER_DIRECT.newDecoder(NESTING_SGROUP);
+    CodedOutputStream output = CodedOutputStream.newInstance(new byte[NESTING_SGROUP.length]);
+
+    Throwable thrown = assertThrows(InvalidProtocolBufferException.class, input::skipMessage);
+    Throwable thrown2 =
+        assertThrows(InvalidProtocolBufferException.class, () -> input.skipMessage(output));
+
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+    assertThat(thrown2)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
   private void checkSizeLimitExceeded(InvalidProtocolBufferException e) {
     assertThat(e)
         .hasMessageThat()
diff --git a/java/lite/src/test/java/com/google/protobuf/LiteTest.java b/java/lite/src/test/java/com/google/protobuf/LiteTest.java
index b42a4b9..8fed7cd 100644
--- a/java/lite/src/test/java/com/google/protobuf/LiteTest.java
+++ b/java/lite/src/test/java/com/google/protobuf/LiteTest.java
@@ -2462,6 +2462,211 @@ public class LiteTest {
     }
   }
 
+  @Test
+  public void testParseFromInputStream_concurrent_nestingUnknownGroups() throws Exception {
+    int numThreads = 200;
+    ArrayList<Thread> threads = new ArrayList<>();
+
+    ByteString byteString = generateNestingGroups(99);
+    AtomicBoolean thrown = new AtomicBoolean(false);
+
+    for (int i = 0; i < numThreads; i++) {
+      Thread thread =
+          new Thread(
+              () -> {
+                try {
+                  TestAllTypesLite unused = TestAllTypesLite.parseFrom(byteString);
+                } catch (IOException e) {
+                  if (e.getMessage().contains("Protocol message had too many levels of nesting")) {
+                    thrown.set(true);
+                  }
+                }
+              });
+      thread.start();
+      threads.add(thread);
+    }
+
+    for (Thread thread : threads) {
+      thread.join();
+    }
+
+    assertThat(thrown.get()).isFalse();
+  }
+
+  @Test
+  public void testParseFromInputStream_nestingUnknownGroups() throws IOException {
+    ByteString byteString = generateNestingGroups(99);
+
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class, () -> TestAllTypesLite.parseFrom(byteString));
+    assertThat(thrown)
+        .hasMessageThat()
+        .doesNotContain("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testParseFromInputStream_nestingUnknownGroups_exception() throws IOException {
+    ByteString byteString = generateNestingGroups(100);
+
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class, () -> TestAllTypesLite.parseFrom(byteString));
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testParseFromInputStream_setRecursionLimit_exception() throws IOException {
+    ByteString byteString = generateNestingGroups(199);
+    UnknownFieldSchema<?, ?> schema = SchemaUtil.unknownFieldSetLiteSchema();
+    schema.setRecursionLimit(200);
+
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class, () -> TestAllTypesLite.parseFrom(byteString));
+    assertThat(thrown)
+        .hasMessageThat()
+        .doesNotContain("Protocol message had too many levels of nesting");
+    schema.setRecursionLimit(UnknownFieldSchema.DEFAULT_RECURSION_LIMIT);
+  }
+
+  @Test
+  public void testParseFromBytes_concurrent_nestingUnknownGroups() throws Exception {
+    int numThreads = 200;
+    ArrayList<Thread> threads = new ArrayList<>();
+
+    ByteString byteString = generateNestingGroups(99);
+    AtomicBoolean thrown = new AtomicBoolean(false);
+
+    for (int i = 0; i < numThreads; i++) {
+      Thread thread =
+          new Thread(
+              () -> {
+                try {
+                  // Should pass in byte[] instead of ByteString to go into ArrayDecoders.
+                  TestAllTypesLite unused = TestAllTypesLite.parseFrom(byteString.toByteArray());
+                } catch (InvalidProtocolBufferException e) {
+                  if (e.getMessage().contains("Protocol message had too many levels of nesting")) {
+                    thrown.set(true);
+                  }
+                }
+              });
+      thread.start();
+      threads.add(thread);
+    }
+
+    for (Thread thread : threads) {
+      thread.join();
+    }
+
+    assertThat(thrown.get()).isFalse();
+  }
+
+  @Test
+  public void testParseFromBytes_nestingUnknownGroups() throws IOException {
+    ByteString byteString = generateNestingGroups(99);
+
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> TestAllTypesLite.parseFrom(byteString.toByteArray()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .doesNotContain("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testParseFromBytes_nestingUnknownGroups_exception() throws IOException {
+    ByteString byteString = generateNestingGroups(100);
+
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> TestAllTypesLite.parseFrom(byteString.toByteArray()));
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testParseFromBytes_setRecursionLimit_exception() throws IOException {
+    ByteString byteString = generateNestingGroups(199);
+    ArrayDecoders.setRecursionLimit(200);
+
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> TestAllTypesLite.parseFrom(byteString.toByteArray()));
+    assertThat(thrown)
+        .hasMessageThat()
+        .doesNotContain("Protocol message had too many levels of nesting");
+    ArrayDecoders.setRecursionLimit(ArrayDecoders.DEFAULT_RECURSION_LIMIT);
+  }
+
+  @Test
+  public void testParseFromBytes_recursiveMessages() throws Exception {
+    byte[] data99 = makeRecursiveMessage(99).toByteArray();
+    byte[] data100 = makeRecursiveMessage(100).toByteArray();
+
+    RecursiveMessage unused = RecursiveMessage.parseFrom(data99);
+    Throwable thrown =
+        assertThrows(
+            InvalidProtocolBufferException.class, () -> RecursiveMessage.parseFrom(data100));
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testParseFromBytes_recursiveKnownGroups() throws Exception {
+    byte[] data99 = makeRecursiveGroup(99).toByteArray();
+    byte[] data100 = makeRecursiveGroup(100).toByteArray();
+
+    RecursiveGroup unused = RecursiveGroup.parseFrom(data99);
+    Throwable thrown =
+        assertThrows(InvalidProtocolBufferException.class, () -> RecursiveGroup.parseFrom(data100));
+    assertThat(thrown).hasMessageThat().contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  @SuppressWarnings("ProtoParseFromByteString")
+  public void testMaliciousSGroupTagsWithMapField_fromByteArray() throws Exception {
+    ByteString byteString = generateNestingGroups(102);
+
+    Throwable parseFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.parseFrom(byteString.toByteArray()));
+    Throwable mergeFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.newBuilder().mergeFrom(byteString.toByteArray()));
+
+    assertThat(parseFromThrown)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+    assertThat(mergeFromThrown)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
+  @Test
+  public void testMaliciousSGroupTagsWithMapField_fromInputStream() throws Exception {
+    byte[] bytes = generateNestingGroups(101).toByteArray();
+
+    Throwable parseFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.parseFrom(new ByteArrayInputStream(bytes)));
+    Throwable mergeFromThrown =
+        assertThrows(
+            InvalidProtocolBufferException.class,
+            () -> MapContainer.newBuilder().mergeFrom(new ByteArrayInputStream(bytes)));
+
+    assertThat(parseFromThrown)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+    assertThat(mergeFromThrown)
+        .hasMessageThat()
+        .contains("Protocol message had too many levels of nesting");
+  }
+
   @Test
   public void testParseFromByteBuffer_extensions() throws Exception {
     TestAllExtensionsLite message =
@@ -2818,4 +3023,31 @@ public class LiteTest {
     }
     return false;
   }
+
+  private static ByteString generateNestingGroups(int num) throws IOException {
+    int groupTap = WireFormat.makeTag(3, WireFormat.WIRETYPE_START_GROUP);
+    ByteString.Output byteStringOutput = ByteString.newOutput();
+    CodedOutputStream codedOutput = CodedOutputStream.newInstance(byteStringOutput);
+    for (int i = 0; i < num; i++) {
+      codedOutput.writeInt32NoTag(groupTap);
+    }
+    codedOutput.flush();
+    return byteStringOutput.toByteString();
+  }
+
+  private static RecursiveMessage makeRecursiveMessage(int num) {
+    if (num == 0) {
+      return RecursiveMessage.getDefaultInstance();
+    } else {
+      return RecursiveMessage.newBuilder().setRecurse(makeRecursiveMessage(num - 1)).build();
+    }
+  }
+
+  private static RecursiveGroup makeRecursiveGroup(int num) {
+    if (num == 0) {
+      return RecursiveGroup.getDefaultInstance();
+    } else {
+      return RecursiveGroup.newBuilder().setRecurse(makeRecursiveGroup(num - 1)).build();
+    }
+  }
 }
-- 
2.43.0

