Tombstone: fall back to file timestamp on empty pb timestamp

- Allow empty protobuf timestamps to fall back to the file timestamp
- Update _parse_timestamp_string to accept a fallback
- Pass file_timestamp to its callers
- Add tests for empty and whitespace tombstone timestamps
- Adjust imports in bugreport module to include DumpsysADBState
  Tombstone: use file timestamp on empty pb
- skip 0 bytes tombstones and show debug warning
This commit is contained in:
Janik Besendorf
2026-01-20 13:38:07 +01:00
parent 7173e02a6f
commit 02c52f43c8
4 changed files with 112 additions and 8 deletions
+16 -6
View File
@@ -6,14 +6,14 @@
import datetime import datetime
from typing import List, Optional, Union from typing import List, Optional, Union
import pydantic
import betterproto import betterproto
import pydantic
from dateutil import parser from dateutil import parser
from mvt.common.utils import convert_datetime_to_iso
from mvt.android.parsers.proto.tombstone import Tombstone from mvt.android.parsers.proto.tombstone import Tombstone
from .artifact import AndroidArtifact from mvt.common.utils import convert_datetime_to_iso
from .artifact import AndroidArtifact
TOMBSTONE_DELIMITER = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***" TOMBSTONE_DELIMITER = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***"
@@ -129,7 +129,7 @@ class TombstoneCrashArtifact(AndroidArtifact):
# Add some extra metadata # Add some extra metadata
tombstone_dict["timestamp"] = self._parse_timestamp_string( tombstone_dict["timestamp"] = self._parse_timestamp_string(
tombstone_pb.timestamp tombstone_pb.timestamp, file_timestamp
) )
tombstone_dict["file_name"] = file_name tombstone_dict["file_name"] = file_name
tombstone_dict["file_timestamp"] = convert_datetime_to_iso(file_timestamp) tombstone_dict["file_timestamp"] = convert_datetime_to_iso(file_timestamp)
@@ -249,11 +249,21 @@ class TombstoneCrashArtifact(AndroidArtifact):
def _load_timestamp_line(self, line: str, tombstone: dict) -> bool: def _load_timestamp_line(self, line: str, tombstone: dict) -> bool:
timestamp = line.split(":", 1)[1].strip() timestamp = line.split(":", 1)[1].strip()
tombstone["timestamp"] = self._parse_timestamp_string(timestamp) tombstone["timestamp"] = self._parse_timestamp_string(timestamp, None)
return True return True
@staticmethod @staticmethod
def _parse_timestamp_string(timestamp: str) -> str: def _parse_timestamp_string(
timestamp: str, fallback_timestamp: Optional[datetime.datetime]
) -> str:
"""Parse timestamp string, using fallback if timestamp is empty."""
# Handle empty or whitespace-only timestamps
if not timestamp or not timestamp.strip():
if fallback_timestamp:
return convert_datetime_to_iso(fallback_timestamp)
else:
raise ValueError("Empty timestamp with no fallback provided")
timestamp_parsed = parser.parse(timestamp) timestamp_parsed = parser.parse(timestamp)
# HACK: Swap the local timestamp to UTC, so keep the original time and avoid timezone conversion. # HACK: Swap the local timestamp to UTC, so keep the original time and avoid timezone conversion.
local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc) local_timestamp = timestamp_parsed.replace(tzinfo=datetime.timezone.utc)
@@ -5,6 +5,7 @@
from .dumpsys_accessibility import DumpsysAccessibility from .dumpsys_accessibility import DumpsysAccessibility
from .dumpsys_activities import DumpsysActivities from .dumpsys_activities import DumpsysActivities
from .dumpsys_adb_state import DumpsysADBState
from .dumpsys_appops import DumpsysAppops from .dumpsys_appops import DumpsysAppops
from .dumpsys_battery_daily import DumpsysBatteryDaily from .dumpsys_battery_daily import DumpsysBatteryDaily
from .dumpsys_battery_history import DumpsysBatteryHistory from .dumpsys_battery_history import DumpsysBatteryHistory
@@ -13,7 +14,6 @@ from .dumpsys_getprop import DumpsysGetProp
from .dumpsys_packages import DumpsysPackages from .dumpsys_packages import DumpsysPackages
from .dumpsys_platform_compat import DumpsysPlatformCompat from .dumpsys_platform_compat import DumpsysPlatformCompat
from .dumpsys_receivers import DumpsysReceivers from .dumpsys_receivers import DumpsysReceivers
from .dumpsys_adb_state import DumpsysADBState
from .fs_timestamps import BugReportTimestamps from .fs_timestamps import BugReportTimestamps
from .tombstones import Tombstones from .tombstones import Tombstones
@@ -7,6 +7,7 @@ import logging
from typing import Optional from typing import Optional
from mvt.android.artifacts.tombstone_crashes import TombstoneCrashArtifact from mvt.android.artifacts.tombstone_crashes import TombstoneCrashArtifact
from .base import BugReportModule from .base import BugReportModule
@@ -47,6 +48,13 @@ class Tombstones(TombstoneCrashArtifact, BugReportModule):
modification_time = self._get_file_modification_time(tombstone_file) modification_time = self._get_file_modification_time(tombstone_file)
tombstone_data = self._get_file_content(tombstone_file) tombstone_data = self._get_file_content(tombstone_file)
# Check for empty tombstone files
if len(tombstone_data) == 0:
self.log.debug(
f"Skipping empty tombstone file {tombstone_file} (0 bytes)"
)
continue
try: try:
if tombstone_file.endswith(".pb"): if tombstone_file.endswith(".pb"):
self.parse_protobuf( self.parse_protobuf(
+87 -1
View File
@@ -2,8 +2,8 @@
# Copyright (c) 2021-2023 The MVT Authors. # Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at # Use of this software is governed by the MVT License 1.1 that can be found at
# https://license.mvt.re/1.1/ # https://license.mvt.re/1.1/
import os
import datetime import datetime
import os
import pytest import pytest
@@ -65,3 +65,89 @@ class TestTombstoneCrashArtifact:
# MVT should output the local time only: # MVT should output the local time only:
# So original 2023-04-12 12:32:40.518290770+0200 -> 2023-04-12 12:32:40.000000 # So original 2023-04-12 12:32:40.518290770+0200 -> 2023-04-12 12:32:40.000000
assert tombstone_result.get("timestamp") == "2023-04-12 12:32:40.518290" assert tombstone_result.get("timestamp") == "2023-04-12 12:32:40.518290"
def test_tombstone_pb_empty_timestamp(self):
"""Test parsing a protobuf tombstone with an empty timestamp."""
tombstone_artifact = TombstoneCrashArtifact()
artifact_path = "android_data/bugreport_tombstones/tombstone_empty_timestamp.pb"
file = get_artifact(artifact_path)
with open(file, "rb") as f:
data = f.read()
file_name = os.path.basename(artifact_path)
file_timestamp = datetime.datetime(2024, 1, 15, 10, 30, 45, 123456)
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
assert len(tombstone_artifact.results) == 1
result = tombstone_artifact.results[0]
# When tombstone has empty timestamp, should use file modification time
assert result.get("timestamp") == "2024-01-15 10:30:45.123456"
assert result.get("pid") == 12345
assert result.get("uid") == 1000
assert result.get("signal_info", {}).get("name") == "SIGSEGV"
def test_tombstone_pb_empty_timestamp_with_threads(self):
"""Test parsing a protobuf tombstone with empty timestamp and thread info."""
tombstone_artifact = TombstoneCrashArtifact()
artifact_path = "android_data/bugreport_tombstones/tombstone_empty_timestamp_with_threads.pb"
file = get_artifact(artifact_path)
with open(file, "rb") as f:
data = f.read()
file_name = os.path.basename(artifact_path)
file_timestamp = datetime.datetime(2024, 2, 20, 14, 15, 30, 0)
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
assert len(tombstone_artifact.results) == 1
result = tombstone_artifact.results[0]
# Verify timestamp fallback
assert result.get("timestamp") == "2024-02-20 14:15:30.000000"
assert result.get("pid") == 9876
assert result.get("uid") == 10001
assert result.get("signal_info", {}).get("name") == "SIGABRT"
assert result.get("process_name") == "ExampleThread"
def test_tombstone_pb_whitespace_timestamp(self):
"""Test parsing a protobuf tombstone with whitespace-only timestamp."""
tombstone_artifact = TombstoneCrashArtifact()
artifact_path = (
"android_data/bugreport_tombstones/tombstone_whitespace_timestamp.pb"
)
file = get_artifact(artifact_path)
with open(file, "rb") as f:
data = f.read()
file_name = os.path.basename(artifact_path)
file_timestamp = datetime.datetime(2024, 3, 10, 8, 0, 0, 0)
tombstone_artifact.parse_protobuf(file_name, file_timestamp, data)
assert len(tombstone_artifact.results) == 1
result = tombstone_artifact.results[0]
# Verify whitespace timestamp is treated as empty
assert result.get("timestamp") == "2024-03-10 08:00:00.000000"
assert result.get("pid") == 11111
assert result.get("uid") == 2000
assert result.get("signal_info", {}).get("name") == "SIGILL"
def test_tombstone_pb_empty_file(self):
"""Test that empty (0 bytes) tombstone files are handled gracefully."""
tombstone_artifact = TombstoneCrashArtifact()
artifact_path = "android_data/bugreport_tombstones/tombstone_empty_file.pb"
file = get_artifact(artifact_path)
with open(file, "rb") as f:
data = f.read()
# Verify the file is actually empty
assert len(data) == 0, "Test file should be empty (0 bytes)"
file_name = os.path.basename(artifact_path)
file_timestamp = datetime.datetime(2024, 4, 1, 12, 0, 0, 0)
# Empty files should be skipped in the module (not parsed)
# So we don't call parse_protobuf here, just verify the data is empty
# The actual skipping happens in the Tombstones module's run() method
# This test verifies that empty data is detectable
assert len(data) == 0