Add better logging for ovesized messages.

This commit is contained in:
Greyson Parrelli
2026-02-12 12:15:39 -05:00
committed by Alex Hart
parent caa743aba2
commit ae04749336
3 changed files with 249 additions and 98 deletions

View File

@@ -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<Message<*, *>>).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<Message<*, *>>).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.
*/

View File

@@ -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 {

View File

@@ -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(")
}
}