Compare commits

...

4 Commits

Author SHA1 Message Date
besendorf
5169a46b59 Merge branch 'main' into fix_tombstone 2026-03-18 09:11:04 +01:00
Janik Besendorf
5a129430ae Add test artifact files for tombstone protobuf tests 2026-01-20 14:45:12 +01:00
Janik Besendorf
190a5f119f Remove unused TombstoneCrashArtifact init in test 2026-01-20 14:34:40 +01:00
Janik Besendorf
02c52f43c8 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
2026-01-20 13:38:07 +01:00
8 changed files with 110 additions and 8 deletions

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)

View File

@@ -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

View File

@@ -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(

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,83 @@ 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."""
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)"
# Empty files should be skipped in the module (not parsed)
# The actual skipping happens in the Tombstones module's run() method
# This test verifies that empty data is detectable

View File

@@ -0,0 +1 @@
5test/build/fingerprint:12/TEST/V1.0:user/release-keys test-rev-001(¹`0º`8èBu:r:system_app:s0J/system/bin/test_process <R SIGSEGV" SEGV_MAPERR

View File

@@ -0,0 +1 @@
5test/build/fingerprint:13/TEST/V2.0:user/release-keys test-rev-002(”M0•M8NBu:r:untrusted_app:s0J-/data/app/com.example.app/lib/arm64/libapp.so xRSIGABRT"SI_TKILL•M•M

View File

@@ -0,0 +1,2 @@
5test/build/fingerprint:11/TEST/V3.0:user/release-keys test-rev-003" (çV0èV8ÐB u:r:shell:s0J/system/bin/app_process64 RSIGILL"
ILL_ILLOPN