From cb92ff3566336c46e5a1222cc508105dd7ba0f30 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Mon, 28 Sep 2026 14:09:41 +1000 Subject: [PATCH] Keep protobuf's parseFrom so the fix survives the shrinker The serializer rebuilds a protobuf through parseFrom, reached reflectively, so R8 cannot see the call: usage.txt for a minified build listed SessionProtos$GroupUpdateMessage.parseFrom(byte[]) among the removed methods, along with 62 other protobuf parseFrom overloads. The read would have thrown NoSuchMethodException, been caught where the KryoException was caught, and dropped the row exactly as before -- so the fix was inert in every build that ships, and no JVM test could see it because unit tests are not minified. With the rule, none of the parseFrom(byte[]) methods on org.session.protos are removed and the kept seed is listed for the ones we parse. The new test covers the job's own serialize/create pair rather than the shared Kryo alone, so reverting either direction to a bare Kryo fails it. --- app/proguard-rules.pro | 7 ++++ .../libsession/messaging/jobs/JobKryoTest.kt | 41 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 867ef97233..cfe8d094bc 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -133,6 +133,13 @@ # Preserve class names for Kryo -keepnames class org.session.libsession.messaging.messages.Destination$** +# Protobuf messages are persisted as their own wire format and rebuilt through parseFrom, +# which is reached reflectively -- invisible to the shrinker, which otherwise removes it and +# leaves every queued message carrying a protobuf unreadable after a restart. +-keepclassmembers class * extends com.google.protobuf.GeneratedMessage { + public static *** parseFrom(byte[]); +} + ########## OPEN GROUP API (MESSAGES) ########## -keep class org.session.libsession.messaging.open_groups.OpenGroupApi$Message { *; } -keepclassmembers class org.session.libsession.messaging.open_groups.OpenGroupApi$Message { (); } diff --git a/app/src/test/java/org/session/libsession/messaging/jobs/JobKryoTest.kt b/app/src/test/java/org/session/libsession/messaging/jobs/JobKryoTest.kt index 44bd6d3fbe..3ce8e83877 100644 --- a/app/src/test/java/org/session/libsession/messaging/jobs/JobKryoTest.kt +++ b/app/src/test/java/org/session/libsession/messaging/jobs/JobKryoTest.kt @@ -2,10 +2,17 @@ package org.session.libsession.messaging.jobs import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test +import org.session.libsession.messaging.messages.Destination +import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.GroupUpdated +import org.session.libsession.messaging.utilities.Data import org.session.protos.SessionProtos class JobKryoTest { @@ -26,6 +33,40 @@ class JobKryoTest { assertEquals(message.sentTimestamp, restored.sentTimestamp) } + /** + * The round trip above proves the serializer; this proves the job is wired to it, in both + * directions — a `serialize()` and a `create()` that disagree lose the message silently. + */ + @Test + fun `a GroupUpdated send job survives serialize and deserialize`() { + val message = GroupUpdated( + SessionProtos.GroupUpdateMessage.newBuilder() + .setMemberLeftMessage(SessionProtos.GroupUpdateMemberLeftMessage.getDefaultInstance()) + .build() + ) + + val restored = slot() + val factory = mockk { + every { create(any()) } answers { callOriginal() } + every { create(capture(restored), any(), any()) } returns mockk(relaxed = true) + } + + assertNotNull(factory.create(sendJob(message).serialize())) + assertEquals(message.inner, (restored.captured as GroupUpdated).inner) + } + + private fun sendJob(message: Message) = MessageSendJob( + message = message, + destination = Destination.ClosedGroup("03${"11".repeat(32)}"), + statusCallback = null, + attachmentUploadJobFactory = mockk(relaxed = true), + messageDataProvider = mockk(relaxed = true), + storage = mockk(relaxed = true), + configFactory = mockk(relaxed = true), + messageSender = mockk(relaxed = true), + jobQueue = mockk(relaxed = true), + ) + private fun roundTrip(value: Any): Any { val output = Output(ByteArray(4096), Job.MAX_BUFFER_SIZE_BYTES) jobKryo().writeClassAndObject(output, value)