Compare commits

..
Author SHA1 Message Date
besendorf 22ed92f4b2 Merge branch 'main' into fix_tombstone 2026-11-05 16:31:06 +01:00
Janik Besendorf 3222bcd98f Merge main and resolve upstream integration conflicts 2026-10-11 14:27:54 +02:00
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
12 changed files with 106 additions and 109 deletions

No files matched your search

-1
View File
@@ -31,4 +31,3 @@ clean:
dist:
$(UV) build
+1 -1
View File
@@ -66,4 +66,4 @@ docker run --rm -it \
ghcr.io/mvt-project/mvt:latest-ios lockdown info
```
If you built the image from source, replace `ghcr.io/mvt-project/mvt` with `mvt`.
If you built the image from source, replace `ghcr.io/mvt-project/mvt` with `mvt`.
+13 -3
View File
@@ -145,7 +145,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)
@@ -288,11 +288,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)
return convert_datetime_to_iso(timestamp_parsed)
+1 -1
View File
@@ -59,7 +59,7 @@ class SMS(BackupModule):
def collect_url_results(self) -> None:
for message in self.results:
for url in message.get("links") or []:
for url in message.get("links", []):
self.add_url_result(url, message.get("isodate"), "sms")
def run(self) -> None:
@@ -7,6 +7,7 @@ from mvt.common.module import MVTModule
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
@@ -15,7 +16,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
from .mounts import Mounts
@@ -68,6 +68,11 @@ class Tombstones(TombstoneCrashArtifact, BugReportModule):
}
try:
data = self._get_file_content(file_path)
if not data:
self.log.debug("Skipping empty tombstone file %s (0 bytes)", file_path)
source_info["error"] = "empty tombstone"
parsed_sources[source] = source_info
continue
if source == "protobuf":
record = self.parse_protobuf_record(
file_name, file_timestamp, data
+81 -1
View File
@@ -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
@@ -130,3 +130,83 @@ class TestTombstoneCrashArtifact:
# Tombstones include an explicit offset, so normalize them to UTC.
assert tombstone_result.get("timestamp") == "2023-04-12 10: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
-101
View File
@@ -4,118 +4,17 @@
# https://license.mvt.re/1.1/
import io
import json
import logging
import os
import tarfile
import zlib
import pytest
from mvt.android.modules.backup.sms import SMS
from mvt.android.parsers.backup import parse_backup_file
from mvt.common.config import settings
from mvt.common.module import run_module
from ..utils import get_android_backup_folder
class TestBackupModule:
@pytest.mark.parametrize("link_fields", [{}, {"links": None}, {"links": []}])
def test_collect_urls_with_empty_links(self, link_fields):
mod = SMS()
mod.results = [
{"links": ["https://example.org/first"], "isodate": "2026-11-04 12:00:00"},
{"body": "", **link_fields},
{"links": ["https://example.org/last"], "isodate": "2026-11-04 13:00:00"},
]
mod.collect_url_results()
assert mod.url_results == [
{
"url": "https://example.org/first",
"expanded_url": None,
"timestamp": "2026-11-04 12:00:00",
"source": "sms",
},
{
"url": "https://example.org/last",
"expanded_url": None,
"timestamp": "2026-11-04 13:00:00",
"source": "sms",
},
]
@pytest.mark.parametrize("message_type", ["sms", "mms"])
@pytest.mark.parametrize("with_indicators", [False, True])
def test_blank_message_backup(
self,
tmp_path,
caplog,
monkeypatch,
indicators_factory,
message_type,
with_indicators,
):
monkeypatch.setattr(settings, "NETWORK_ACCESS_ALLOWED", False)
backup_path = tmp_path / "backup"
relative_path = (
f"apps/com.android.providers.telephony/d_f/000000_{message_type}_backup"
)
message_file = backup_path / relative_path
message_file.parent.mkdir(parents=True)
body_key = "body" if message_type == "sms" else "mms_body"
messages = [
{
body_key: body,
"date": str(1735689600000 + index * 1000),
"date_sent": "0",
}
for index, body in enumerate(
[
"",
"https://suspicious.example.org/first",
"ordinary message",
" \t ",
"",
"https://suspicious.example.org/last",
]
)
]
message_file.write_bytes(zlib.compress(json.dumps(messages).encode()))
results_path = tmp_path / "results"
results_path.mkdir()
mod = SMS(target_path=str(backup_path), results_path=str(results_path))
mod.from_dir(str(backup_path), [relative_path])
if with_indicators:
mod.indicators = indicators_factory(domains=["suspicious.example.org"])
with caplog.at_level(logging.ERROR):
run_module(mod)
assert not [
record for record in caplog.records if record.levelno == logging.ERROR
]
assert len(mod.results) == len(messages)
assert mod.results[0]["links"] is None
assert mod.url_results == [
{
"url": f"https://suspicious.example.org/{suffix}",
"expanded_url": None,
"timestamp": mod.results[index]["isodate"],
"source": "sms",
}
for index, suffix in [(1, "first"), (5, "last")]
]
assert len(mod.alertstore.alerts) == (2 if with_indicators else 0)
if with_indicators:
assert [alert.event for alert in mod.alertstore.alerts] == [
mod.results[1],
mod.results[5],
]
assert json.loads((results_path / "sms.json").read_text()) == mod.results
def test_module_folder(self):
backup_path = get_android_backup_folder()
mod = SMS(target_path=backup_path)
Whitespace-only changes.
@@ -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
@@ -0,0 +1 @@
5test/build/fingerprint:13/TEST/V2.0:user/release-keys test-rev-002(”M0•M8‘NBu:r:untrusted_app:s0J-/data/app/com.example.app/lib/arm64/libapp.so xRSIGABRT"SI_TKILL‚•M•M ExampleThread
@@ -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