Prevent stack overflow when walking message revision chains during local backup.

This commit is contained in:
Cody Henthorne
2026-08-10 16:32:57 -04:00
committed by Greyson Parrelli
parent a4793ebba5
commit 603e12dd59
@@ -62,6 +62,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -597,23 +598,44 @@ public class FullBackupExporter extends FullBackupBase {
}
private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, @NonNull Cursor cursor) {
long id = CursorUtil.requireLong(cursor, MessageTable.ID);
long expireStarted = CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED);
long expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN);
long latestRevisionId = CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID);
return isNonExpiringMessage(db,
CursorUtil.requireLong(cursor, MessageTable.ID),
CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED),
CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN),
CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID));
}
/**
* Corrupt data can contain a cycle in {@link MessageTable#LATEST_REVISION_ID}, so the walk tracks visited ids.
*/
private static boolean isNonExpiringMessage(@NonNull SQLiteDatabase db, long id, long expireStarted, long expiresIn, long latestRevisionId) {
String[] columns = new String[] { MessageTable.ID, MessageTable.EXPIRE_STARTED, MessageTable.EXPIRES_IN, MessageTable.LATEST_REVISION_ID };
String where = MessageTable.ID + " = ?";
Set<Long> visited = new HashSet<>();
while (latestRevisionId > 0 && latestRevisionId != id) {
if (!visited.add(id)) {
Log.w(TAG, "Detected a cycle in the message revision chain! Stopping the walk at " + id);
break;
}
try (Cursor cursor = db.query(MessageTable.TABLE_NAME, columns, where, SqlUtil.buildArgs(latestRevisionId), null, null, null)) {
if (cursor == null || !cursor.moveToFirst()) {
return false;
}
id = CursorUtil.requireLong(cursor, MessageTable.ID);
expireStarted = CursorUtil.requireLong(cursor, MessageTable.EXPIRE_STARTED);
expiresIn = CursorUtil.requireLong(cursor, MessageTable.EXPIRES_IN);
latestRevisionId = CursorUtil.requireLong(cursor, MessageTable.LATEST_REVISION_ID);
}
}
long expiresAt = expireStarted + expiresIn;
long timeRemaining = expiresAt - System.currentTimeMillis();
if (latestRevisionId > 0 && latestRevisionId != id ) {
return isForNonExpiringMessage(db, latestRevisionId);
}
if (expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD) {
return false;
}
return true;
return !(expireStarted > 0 && timeRemaining <= EXPIRATION_BACKUP_THRESHOLD);
}
private static boolean isForNonExpiringMessage(@NonNull SQLiteDatabase db, long messageId) {