Fix local backup crash on classic-encrypted attachments - #14912
Open
patjackson52 wants to merge 1 commit into
Open
Fix local backup crash on classic-encrypted attachments#14912patjackson52 wants to merge 1 commit into
patjackson52 wants to merge 1 commit into
Conversation
Attachments from the classic encryption era have a NULL data_random, but getLocalArchivableAttachments() read it with requireNonNullBlob(), so a single such attachment aborted the entire local backup with an NPE. The same read exists in getLocalArchivableAttachmentsForPlaintextExport(). Allow a null random through to getDataStream(), which already selects ClassicDecryptingPartInputStream when a random isn't a modern 32-byte value -- the same handling FullBackupExporter.openAttachmentStream() used for v1 backups. These attachments are therefore included in local backups as they were in v1, rather than filtered out of the query. Also widen the per-attachment failure handling in LocalArchiver. Opening an attachment happened outside the try that records readWriteFailures, so any failure there escaped to the job -- and a failure to decrypt arrives as an IllegalArgumentException rather than an IOException, which the existing catch would have missed anyway. One unreadable attachment now gets recorded and skipped instead of ending the backup. Fixes signalapp#14875
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Contributor checklist
playStagingSpinner) — reproduced the reported crash and verified the fix, details belowAttachmentTableTest_getLocalArchivableAttachmentsplus existingAttachmentTable*andbackup.v2.localsuites, 42/42Fixes #1234syntaxDescription
Fixes #14875.
Attachments from the classic encryption have a
NULLdata_random, butgetLocalArchivableAttachments()reads it withrequireNonNullBlob(). One such attachment aborts the entire local backup with an NPE, which is what the reporter is hitting. They can no longer create backups at all, though v1.backupfiles worked on the same data. The same read exists ingetLocalArchivableAttachmentsForPlaintextExport().Two changes:
1. Let the null random through (
AttachmentTable).LocalArchivableAttachment.randombecomes nullable, both queries userequireBlob, andgetDataStream()accepts a null random. It already picksClassicDecryptingPartInputStreamwhenever a random isn't a modern 32-byte value, so the decryption side needed no changes — this restores the nullable handlingFullBackupExporter.openAttachmentStream()had for v1 backups.randomhas exactly one consumer (getAttachmentStream→getDataStream), so the nullable widening doesn't propagate further.I chose this over adding
AND data_random IS NOT NULLto the queries: the guard is a smaller diff, but it would silently exclude these attachments from local backups that v1 included, which seems worse than a crash you can see.2. Widen per-attachment failure handling (
LocalArchiver). Device testing turned up a second problem in the same path.source()— which opens and MAC-verifies the attachment — sits outside thetrythat recordsreadWriteFailures, so any failure while opening an attachment escapes to the job rather than being recorded. And a failure to decrypt surfaces asIllegalArgumentException(fromSecretKeySpecwhen an install has no classic keys), notIOException, so the existing catch wouldn't have caught it even if it were correctly scoped. The loop already maintainscreateFailures/readWriteFailuresaccumulators, so per-attachment resilience is clearly the intent; this makes the scope match. There's precedent for the broader catch inArchiveFileSystem.deleteUnusedFiles().Testing
Device. Built onto a Pixel 10 Pro against a staging account with five attachments, then set
data_random = NULLon one row to simulate a classic-era attachment. Same database state and same action each time, only the binary differing:On
mainthe reported crash, reproduced exactly:With change 1 only, forcing the attachment to be re-read by clearing the archived copies — the NPE is gone, but the failure moves downstream and takes the process with it:
With both changes, same scenario:
To be straight about what that second trace does and doesn't show: my test row is synthetic. A null random on a fresh install (which has no classic keys) pointing at a file that is really modern-encrypted. That combination can't occur naturally, and a genuinely affected install has real classic keys and real classic-encrypted files, so change 1 alone is enough for the reporter. But it did expose that the surrounding failure handling doesn't cover opening an attachment, which is worth fixing regardless.
Unit. New
AttachmentTableTest_getLocalArchivableAttachmentscovers a classic (null) row, a modern row, and a mixed set; the classic cases reproduce the NPE without the fix. ExistingAttachmentTable*andbackup.v2.localsuites pass. 42/42 overall.Two things I noticed but did not touch
Both are pre-existing and independent of this change; happy to open separate issues if useful.
LocalArchiverdeletes the existing archived file before re-archiving whencipherLengthdoesn't match. Sincefiles/is shared across all snapshots, a failed re-archive removes that attachment from previously-good snapshots too. Write-to-temp-then-swap would avoid it.PartialSuccessis treated as success byLocalArchiveJob, so a backup that dropped attachments finalizes and reports success with only a log line recording it.