From ae047493366a285a362c4bcb67abf984a2e5abc2 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 12 Feb 2026 12:15:39 -0500 Subject: [PATCH] Add better logging for ovesized messages. --- .../org/signal/core/util/ProtoExtensions.kt | 92 +++++++++++ .../api/SignalServiceMessageSender.java | 101 +----------- .../org/signal/core/util/BuildSizeTreeTest.kt | 154 ++++++++++++++++++ 3 files changed, 249 insertions(+), 98 deletions(-) create mode 100644 lib/libsignal-service/src/test/java/org/signal/core/util/BuildSizeTreeTest.kt diff --git a/core/util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt b/core/util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt index 2deace46ad..cf9aa43c43 100644 --- a/core/util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt +++ b/core/util-jvm/src/main/java/org/signal/core/util/ProtoExtensions.kt @@ -12,8 +12,10 @@ import com.squareup.wire.Message import com.squareup.wire.ProtoAdapter import com.squareup.wire.ProtoReader import com.squareup.wire.ProtoWriter +import com.squareup.wire.WireField import okio.Buffer import okio.ByteString +import okio.utf8Size import org.signal.core.util.logging.Log import java.io.IOException import java.util.LinkedList @@ -87,6 +89,96 @@ fun writeUnknownEnumValue(tag: Int, enumValue: Int): ByteString { return buffer.readByteString() } +/** + * Builds a human-readable tree of serialized sizes for this proto message, + * recursively descending into all nested message fields. Useful for debugging + * oversized messages. + * + * Example output: + * ``` + * Content(1234) + * dataMessage(1100) + * body(500) + * groupV2(400) + * groupChange(350) + * attachments[2](200) + * ``` + */ +fun Message<*, *>.buildSizeTree(name: String): String { + val sb = StringBuilder() + appendSizeTree(name, 0, sb) + return sb.toString() +} + +@Suppress("UNCHECKED_CAST") +private fun Message<*, *>.appendSizeTree(name: String, depth: Int, sb: StringBuilder) { + val totalSize = (adapter as ProtoAdapter>).encodedSize(this) + repeat(depth) { sb.append(" ") } + sb.append(name).append("(").append(totalSize).append(")") + + for (field in this.javaClass.declaredFields) { + field.getAnnotation(WireField::class.java) ?: continue + + val value = try { + field.get(this) + } catch (_: IllegalAccessException) { + continue + } ?: continue + + val fieldName = field.name + + when (value) { + is Message<*, *> -> { + sb.append("\n") + value.appendSizeTree(fieldName, depth + 1, sb) + } + is List<*> -> { + if (value.isNotEmpty()) { + var listTotalSize = 0 + val isMessageList = value[0] is Message<*, *> + for (item in value) { + when (item) { + is Message<*, *> -> listTotalSize += (item.adapter as ProtoAdapter>).encodedSize(item) + is ByteString -> listTotalSize += item.size + is String -> listTotalSize += item.utf8Size().toInt() + } + } + sb.append("\n") + repeat(depth + 1) { sb.append(" ") } + sb.append(fieldName).append("[").append(value.size).append("](").append(listTotalSize).append(")") + + if (isMessageList) { + for (i in value.indices) { + sb.append("\n") + (value[i] as Message<*, *>).appendSizeTree("${fieldName}[$i]", depth + 2, sb) + } + } + } + } + is ByteString -> { + if (value.size > 0) { + sb.append("\n") + repeat(depth + 1) { sb.append(" ") } + sb.append(fieldName).append("(").append(value.size).append(")") + } + } + is String -> { + if (value.isNotEmpty()) { + sb.append("\n") + repeat(depth + 1) { sb.append(" ") } + sb.append(fieldName).append("(").append(value.utf8Size().toInt()).append(")") + } + } + } + } + + if (unknownFields.size > 0) { + sb.append("\n") + repeat(depth + 1) { sb.append(" ") } + sb.append("unknownFields(").append(unknownFields.size).append(")") + } +} + /** * Recursively retrieves all inner complex proto types inside a given proto. */ diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 8922314ee1..e7be79e5e8 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -8,6 +8,7 @@ package org.whispersystems.signalservice.api; import org.signal.core.models.ServiceId; import org.signal.core.models.ServiceId.PNI; import org.signal.core.util.Base64; +import org.signal.core.util.ProtoUtil; import org.signal.core.util.UuidUtil; import org.signal.libsignal.metadata.certificate.SenderCertificate; import org.signal.libsignal.protocol.IdentityKey; @@ -2981,6 +2982,7 @@ public class SignalServiceMessageSender { } else { message = buildContentTooLargeBreadcrumbs(content.getContent().get()); } + Log.w(TAG, "About to crash for exceeding max envelope size (" + size + " > " + maxEnvelopeSize + ")\n" + message); throw new ContentTooLargeException(size, message); } } @@ -2996,104 +2998,7 @@ public class SignalServiceMessageSender { } private String buildContentTooLargeBreadcrumbs(Content content) { - StringBuilder message = new StringBuilder(); - - if (content.dataMessage != null) { - message.append("Data message;"); - if (content.dataMessage.body != null && !content.dataMessage.body.isEmpty()) { - message.append("Body(").append(content.dataMessage.body.length()).append(");"); - } - if (content.dataMessage.groupV2 != null) { - if (content.dataMessage.groupV2.groupChange != null) { - message.append("GroupV2Change(").append(content.dataMessage.groupV2.groupChange.size()).append(");"); - } else { - message.append("GroupV2NoChange;"); - } - } - if (content.dataMessage.giftBadge != null) { - message.append("GiftBadge;"); - } - if (content.dataMessage.pollCreate != null) { - message.append("PollCreate;"); - } - if (content.dataMessage.pollTerminate != null) { - message.append("PollTerminate;"); - } - if (content.dataMessage.pollVote != null) { - message.append("PollVote;"); - } - if (content.dataMessage.pinMessage != null) { - message.append("PinMessage;"); - } - if (content.dataMessage.unpinMessage != null) { - message.append("UnpinMessage;"); - } - if (content.dataMessage.reaction != null) { - message.append("Reaction;"); - } - if (content.dataMessage.profileKey != null && content.dataMessage.profileKey.size() > 0) { - message.append("ProfileKey(").append(content.dataMessage.profileKey.size()).append(");"); - } - if (content.dataMessage.payment != null) { - message.append("Payment;"); - } - if (!content.dataMessage.attachments.isEmpty()) { - message.append("Attachments(").append(content.dataMessage.attachments.size()).append(");"); - } - if (!content.dataMessage.contact.isEmpty()) { - message.append("Contacts(").append(content.dataMessage.contact.size()).append(");"); - } - if (!content.dataMessage.bodyRanges.isEmpty()) { - message.append("Ranges(").append(content.dataMessage.bodyRanges.size()).append(");"); - } - if (content.dataMessage.quote != null) { - if (content.dataMessage.quote.text != null) { - message.append("Quote(").append(content.dataMessage.quote.text.length()).append(");"); - } else { - message.append("Quote(No text);"); - } - } - } - - if (content.syncMessage != null) { - message.append("Sync message;"); - - if (content.syncMessage.sent != null) { - if (content.syncMessage.sent.storyMessage != null) { - message.append("StoryMessage(").append(content.syncMessage.sent.storyMessageRecipients.size()).append(");"); - } - if (!content.syncMessage.sent.storyMessageRecipients.isEmpty()) { - message.append("StoryRecipients(").append(content.syncMessage.sent.storyMessageRecipients.size()).append(");"); - } - if (content.syncMessage.blocked != null) { - message.append("Blocked-AciString(").append(content.syncMessage.blocked.acis.size()).append(");"); - message.append("Blocked-AciBinary(").append(content.syncMessage.blocked.acisBinary.size()).append(");"); - message.append("Blocked-GroupIds(").append(content.syncMessage.blocked.groupIds.size()).append(");"); - message.append("Blocked-Numbers(").append(content.syncMessage.blocked.numbers.size()).append(");"); - } - if (content.syncMessage.outgoingPayment != null) { - message.append("OutgoingPayment"); - } - if (content.syncMessage.deleteForMe != null) { - message.append("DeleteForMe-Messages(").append(content.syncMessage.deleteForMe.messageDeletes.size()).append(");"); - message.append("DeleteForMe-Attachments(").append(content.syncMessage.deleteForMe.attachmentDeletes.size()).append(");"); - message.append("DeleteForMe-Conversations(").append(content.syncMessage.deleteForMe.conversationDeletes.size()).append(");"); - } - if (!content.syncMessage.read.isEmpty()) { - message.append("Read(").append(content.syncMessage.read.size()).append(");"); - } - if (!content.syncMessage.viewed.isEmpty()) { - message.append("Viewed(").append(content.syncMessage.read.size()).append(");"); - } - } - } - - if (content.receiptMessage != null) { - message.append("ReceiptMessage(").append(content.receiptMessage.timestamp.size()).append(");"); - message.append("ReceiptMessage(").append(content.receiptMessage.type.getValue()).append(");"); - } - - return message.toString(); + return ProtoUtil.buildSizeTree(content, "Content"); } public interface EventListener { diff --git a/lib/libsignal-service/src/test/java/org/signal/core/util/BuildSizeTreeTest.kt b/lib/libsignal-service/src/test/java/org/signal/core/util/BuildSizeTreeTest.kt new file mode 100644 index 0000000000..513b33953b --- /dev/null +++ b/lib/libsignal-service/src/test/java/org/signal/core/util/BuildSizeTreeTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2025 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.signal.core.util + +import assertk.assertThat +import assertk.assertions.contains +import assertk.assertions.doesNotContain +import assertk.assertions.isEqualTo +import assertk.assertions.startsWith +import okio.ByteString.Companion.toByteString +import org.junit.Test +import org.whispersystems.signalservice.internal.push.BodyRange +import org.whispersystems.signalservice.internal.push.Content +import org.whispersystems.signalservice.internal.push.DataMessage +import org.whispersystems.signalservice.internal.push.GroupContextV2 +import org.whispersystems.signalservice.internal.push.SyncMessage + +class BuildSizeTreeTest { + + @Test + fun `empty message has zero encoded size`() { + val msg = DataMessage() + val tree = msg.buildSizeTree("DataMessage") + assertThat(tree).isEqualTo("DataMessage(0)") + } + + @Test + fun `string field shows utf8 byte size`() { + val msg = DataMessage(body = "hello") + val tree = msg.buildSizeTree("DataMessage") + + assertThat(tree).startsWith("DataMessage(") + assertThat(tree).contains("\n body(5)") + } + + @Test + fun `multi-byte utf8 string shows correct byte size`() { + val msg = DataMessage(body = "\uD83D\uDE00") // emoji, 4 bytes in UTF-8 + val tree = msg.buildSizeTree("DataMessage") + + assertThat(tree).contains("body(4)") + } + + @Test + fun `ByteString field shows raw byte size`() { + val bytes = ByteArray(100).toByteString() + val msg = DataMessage(profileKey = bytes) + val tree = msg.buildSizeTree("DataMessage") + + assertThat(tree).contains("\n profileKey(100)") + } + + @Test + fun `null and empty fields are omitted`() { + val msg = DataMessage(body = "hi") + val tree = msg.buildSizeTree("DataMessage") + + assertThat(tree).doesNotContain("profileKey") + assertThat(tree).doesNotContain("groupV2") + assertThat(tree).doesNotContain("attachments") + assertThat(tree).doesNotContain("bodyRanges") + } + + @Test + fun `nested message field shows indented sub-tree`() { + val msg = Content( + dataMessage = DataMessage(body = "test") + ) + val tree = msg.buildSizeTree("Content") + + assertThat(tree).startsWith("Content(") + assertThat(tree).contains("\n dataMessage(") + assertThat(tree).contains("\n body(4)") + } + + @Test + fun `repeated message field shows count and total size with elements`() { + val ranges = listOf( + BodyRange(start = 0, length = 5), + BodyRange(start = 10, length = 3) + ) + val msg = DataMessage(bodyRanges = ranges) + val tree = msg.buildSizeTree("DataMessage") + + assertThat(tree).contains("bodyRanges[2](") + assertThat(tree).contains("bodyRanges[0](") + assertThat(tree).contains("bodyRanges[1](") + } + + @Test + fun `deeply nested structure shows correct indentation`() { + val groupChange = ByteArray(50).toByteString() + val masterKey = ByteArray(32).toByteString() + val msg = Content( + dataMessage = DataMessage( + body = "hi", + groupV2 = GroupContextV2( + masterKey = masterKey, + groupChange = groupChange, + revision = 5 + ) + ) + ) + val tree = msg.buildSizeTree("Content") + + // depth 0 + assertThat(tree).startsWith("Content(") + // depth 1 + assertThat(tree).contains("\n dataMessage(") + // depth 2 + assertThat(tree).contains("\n body(2)") + assertThat(tree).contains("\n groupV2(") + // depth 3 + assertThat(tree).contains("\n masterKey(32)") + assertThat(tree).contains("\n groupChange(50)") + } + + @Test + fun `encoded sizes match wire adapter`() { + val msg = Content( + dataMessage = DataMessage(body = "hello world") + ) + val tree = msg.buildSizeTree("Content") + + val contentSize = Content.ADAPTER.encodedSize(msg) + val dataMessageSize = DataMessage.ADAPTER.encodedSize(msg.dataMessage!!) + + assertThat(tree).startsWith("Content($contentSize)") + assertThat(tree).contains("\n dataMessage($dataMessageSize)") + } + + @Test + fun `multiple top-level fields are all included`() { + val msg = Content( + dataMessage = DataMessage(body = "hi"), + syncMessage = SyncMessage() + ) + val tree = msg.buildSizeTree("Content") + + assertThat(tree).contains("dataMessage(") + assertThat(tree).contains("syncMessage(") + } + + @Test + fun `custom root name is used`() { + val msg = DataMessage(body = "x") + val tree = msg.buildSizeTree("MyCustomName") + + assertThat(tree).startsWith("MyCustomName(") + } +}