diff --git a/src/mvt/android/artifacts/tombstone_crashes.py b/src/mvt/android/artifacts/tombstone_crashes.py index 0b8e522..64c6d5e 100644 --- a/src/mvt/android/artifacts/tombstone_crashes.py +++ b/src/mvt/android/artifacts/tombstone_crashes.py @@ -6,14 +6,14 @@ import datetime from typing import List, Optional, Union -import pydantic import betterproto +import pydantic from dateutil import parser -from mvt.common.utils import convert_datetime_to_iso 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 = "*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***" @@ -129,7 +129,7 @@ class TombstoneCrashArtifact(AndroidArtifact): # Add some extra metadata tombstone_dict["timestamp"] = self._parse_timestamp_string( - tombstone_pb.timestamp + tombstone_pb.timestamp, file_timestamp ) tombstone_dict["file_name"] = file_name 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: timestamp = line.split(":", 1)[1].strip() - tombstone["timestamp"] = self._parse_timestamp_string(timestamp) + tombstone["timestamp"] = self._parse_timestamp_string(timestamp, None) return True @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) # 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) diff --git a/src/mvt/android/modules/bugreport/__init__.py b/src/mvt/android/modules/bugreport/__init__.py index 1594af9..d491e53 100644 --- a/src/mvt/android/modules/bugreport/__init__.py +++ b/src/mvt/android/modules/bugreport/__init__.py @@ -5,6 +5,7 @@ from .dumpsys_accessibility import DumpsysAccessibility from .dumpsys_activities import DumpsysActivities +from .dumpsys_adb_state import DumpsysADBState from .dumpsys_appops import DumpsysAppops from .dumpsys_battery_daily import DumpsysBatteryDaily from .dumpsys_battery_history import DumpsysBatteryHistory @@ -13,7 +14,6 @@ from .dumpsys_getprop import DumpsysGetProp from .dumpsys_packages import DumpsysPackages from .dumpsys_platform_compat import DumpsysPlatformCompat from .dumpsys_receivers import DumpsysReceivers -from .dumpsys_adb_state import DumpsysADBState from .fs_timestamps import BugReportTimestamps from .tombstones import Tombstones diff --git a/src/mvt/android/modules/bugreport/tombstones.py b/src/mvt/android/modules/bugreport/tombstones.py index 6447e61..bfceec9 100644 --- a/src/mvt/android/modules/bugreport/tombstones.py +++ b/src/mvt/android/modules/bugreport/tombstones.py @@ -7,6 +7,7 @@ import logging from typing import Optional from mvt.android.artifacts.tombstone_crashes import TombstoneCrashArtifact + from .base import BugReportModule @@ -47,6 +48,13 @@ class Tombstones(TombstoneCrashArtifact, BugReportModule): modification_time = self._get_file_modification_time(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: if tombstone_file.endswith(".pb"): self.parse_protobuf( diff --git a/tests/android/test_artifact_tombstones.py b/tests/android/test_artifact_tombstones.py index d1d5682..1ca684e 100644 --- a/tests/android/test_artifact_tombstones.py +++ b/tests/android/test_artifact_tombstones.py @@ -2,8 +2,8 @@ # Copyright (c) 2021-2023 The MVT Authors. # Use of this software is governed by the MVT License 1.1 that can be found at # https://license.mvt.re/1.1/ -import os import datetime +import os import pytest @@ -65,3 +65,89 @@ class TestTombstoneCrashArtifact: # MVT should output the local time only: # 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" + + 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