Skip to content

fix: defensive null-safety for ServiceId fields in MessageEnvelope#1944

Closed
strichter wants to merge 2 commits intoAsamK:masterfrom
strichter:fix/defensive-null-serviceid-handling
Closed

fix: defensive null-safety for ServiceId fields in MessageEnvelope#1944
strichter wants to merge 2 commits intoAsamK:masterfrom
strichter:fix/defensive-null-serviceid-handling

Conversation

@strichter
Copy link

Summary

Signal Desktop v8.0.0 switched to binary ACI encoding (*AciBinary fields) in protobuf messages. While libsignal-service-java:2.15.3_unofficial_138 (already on master) adds dual-format parsing, the MessageEnvelope construction layer can still encounter null ServiceId values in edge cases, causing data loss or NPEs.

This PR adds comprehensive null-safety guards to all ServiceId-dependent fields in MessageEnvelope.java:

  • Quote (the main fix): Remove the .filter(q -> q.getAuthor() != null && q.getAuthor().isValid()) that silently drops the entire quote when the author cannot be resolved. Instead, preserve quote content and fall back to UNKNOWN_UUID for the author — the quote text, mentions, and attachments remain valuable context even without author attribution. Also use Optional.ofNullable() for quote text.

  • Reaction: Add null guard for targetAuthor before recipient resolution to prevent NPE.

  • Mention: Filter out individual mentions with null ServiceId while preserving valid mentions in the same message.

  • StoryContext: Add null guard for authorServiceId.

  • PollVote: Add null guard for targetAuthor.

Testing

Built from master and verified end-to-end with a linked device. Tested all three primary failure modes from Signal Desktop v8.0.0:

Test Before (v0.13.24) After (this PR)
Reaction from Desktop Empty dataMessage, reaction field missing reaction present with targetAuthorUuid
@mention from Desktop \uFFFC placeholder, no mentions array mentions array with resolved UUIDs
Quote/reply from Desktop Entire quote dropped quote with authorUuid and text

All cases also verified working from Mobile (string ACI encoding) — backward compatible.

Build and tests pass:

./gradlew compileJava  # ✓
./gradlew test         # ✓

Related

Signal Desktop v8.0.0 switched to binary ACI encoding (*AciBinary fields)
in protobuf messages. While libsignal-service-java v2.15.3_unofficial_138
adds dual-format parsing, the MessageEnvelope construction layer in
signal-cli can still encounter null ServiceId values in edge cases or
with older library versions.

This change adds comprehensive null-safety guards:

- Quote: Remove overly strict .filter() that drops the entire quote when
  the author ServiceId is null or invalid. Instead, preserve quote content
  and fall back to UNKNOWN_UUID for the author. Quote text and attachments
  remain valuable context even without author attribution.

- Reaction: Add null guard for targetAuthor before resolving recipient.
  A reaction without a target author cannot be meaningfully processed.

- Mention: Filter out mentions with null ServiceId to prevent NPE during
  recipient resolution while preserving valid mentions in the same message.

- StoryContext: Add null guard for authorServiceId before resolving.

- PollVote: Add null guard for targetAuthor before resolving.

- Quote text: Use Optional.ofNullable() instead of Optional.of() to
  handle null quote text defensively.

Fixes AsamK#1935, AsamK#1940
Related: AsamK#1938
@strichter
Copy link
Author

I did test the fix (reaction, mention, reply) with Signal Desktop and Mobile to ensure both are working.

Add tests for the defensive null-safety handling in MessageEnvelope.Data:

- Quote with valid author: preserved with correct UUID
- Quote with ACI.UNKNOWN author: preserved with UNKNOWN_UUID fallback
  (previously dropped entirely by the isValid() filter)
- Quote with empty text: handled defensively
- Reaction with valid author: preserved with emoji and timestamp
- Reaction with ACI.UNKNOWN author: preserved (non-null, still useful)
- Mention with valid ServiceId: preserved with correct position
- Multiple mentions: all preserved
- No mentions: returns empty list
- Plain message: no optional fields present
- Combined quote + mentions: both preserved

Also adds TestRecipientIdFactory to create RecipientId instances in tests
without requiring the package-private RecipientStore dependency.
strichter added a commit to strichter/signal-cli-rest-api that referenced this pull request Feb 28, 2026
Signal Desktop v8.0.0 switched from string ACI fields to binary ACI
encoding in protobuf messages. This breaks reactions, mentions, quotes,
and other message features when the library cannot parse the new format.

Two-part fix applied via patch to signal-cli v0.13.24 source build:

1. Bump signal-service-java from unofficial_137 to unofficial_138, which
   adds dual-format ACI parsing (string + binary fallback via
   ServiceId.parseOrNull).

2. Add defensive null guards in MessageEnvelope.java for cases where
   ServiceId resolution still fails (e.g. ACI.UNKNOWN). Preserves
   message content with UNKNOWN_UUID fallback rather than dropping
   entire message components (quotes, reactions, mentions, etc.).

The patch is applied during the x86_64 source build. The source-built
installDist output replaces the release tarball, so both the JVM and
native (GraalVM) paths get the fix.

Non-x86_64 architectures continue using the unpatched release tarball
until signal-cli cuts a new release with unofficial_138.

See: AsamK/signal-cli#1944

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@AsamK
Copy link
Owner

AsamK commented Feb 28, 2026

Did you test the latest master version without your fix as well? Because reaction, mention, reply should already work there.

@strichter
Copy link
Author

Hi Sebastian, I just checked with master and it is indeed working. Though I think the changes are valid protections. That said, feel free to close this PR.

@AsamK
Copy link
Owner

AsamK commented Feb 28, 2026

Thanks for checking!
These checks are already done in EnvelopeContentValidator in libsignal-service.
Unless signal changes the protocol representation of these again, the checks are redundant here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

"mentions" field missing from dataMessage in JSON-RPC receive notifications Reaction field missing from SSE output in daemon --http mode

2 participants