mirror of
https://github.com/mvt-project/mvt.git
synced 2026-10-05 15:37:13 +02:00
Merge pull request #944 from va-resident/fix/bugreport-oem-wrapper-archive
Descend into an OEM wrapper archive in check-bugreport
This commit is contained in:
3 files changed
+115
-5
No files matched your search
@@ -3,14 +3,19 @@
|
|||||||
# 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 fnmatch
|
||||||
|
import io
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from zipfile import ZipFile
|
from zipfile import BadZipFile, ZipFile
|
||||||
|
|
||||||
from mvt.android.artifacts.getprop import GetProp
|
from mvt.android.artifacts.getprop import GetProp
|
||||||
from mvt.android.modules.bugreport.base import BugReportModule
|
from mvt.android.modules.bugreport.base import (
|
||||||
|
DUMPSTATE_ENTRY_POINTS,
|
||||||
|
BugReportModule,
|
||||||
|
)
|
||||||
from mvt.common.command import Command
|
from mvt.common.command import Command
|
||||||
from mvt.common.indicators import Indicators
|
from mvt.common.indicators import Indicators
|
||||||
from mvt.common.module import MVTModule
|
from mvt.common.module import MVTModule
|
||||||
@@ -88,6 +93,48 @@ class CmdAndroidCheckBugreport(Command):
|
|||||||
for file_name in self.__zip.namelist():
|
for file_name in self.__zip.namelist():
|
||||||
self.__files.append(file_name)
|
self.__files.append(file_name)
|
||||||
|
|
||||||
|
if not self._has_dumpstate(self.__files):
|
||||||
|
nested = self._nested_bugreport(bugreport_zip)
|
||||||
|
if nested:
|
||||||
|
self.__zip = nested
|
||||||
|
self.__files = list(nested.namelist())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _has_dumpstate(file_names: List[str]) -> bool:
|
||||||
|
"""Whether these members carry any of the entry points the bug report
|
||||||
|
modules read (see `BugReportModule._get_dumpstate_file`)."""
|
||||||
|
return any(
|
||||||
|
fnmatch.filter(file_names, pattern) for pattern in DUMPSTATE_ENTRY_POINTS
|
||||||
|
)
|
||||||
|
|
||||||
|
def _nested_bugreport(self, outer: ZipFile) -> Optional[ZipFile]:
|
||||||
|
"""Descend one level into an OEM wrapper archive.
|
||||||
|
|
||||||
|
MIUI / HyperOS hands out a zip of app logs, ANR traces and tcpdump
|
||||||
|
captures with the real `bugreport-<device>-<timestamp>.zip` nested
|
||||||
|
inside. Without this descent the outer archive has no entry point,
|
||||||
|
every module reports it found no files, and the command still exits 0
|
||||||
|
with an empty result.
|
||||||
|
"""
|
||||||
|
candidates = [
|
||||||
|
name for name in outer.namelist() if name.lower().endswith(".zip")
|
||||||
|
]
|
||||||
|
candidates.sort(
|
||||||
|
key=lambda name: (
|
||||||
|
"bugreport" not in name.lower() and "dumpstate" not in name.lower()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for name in candidates:
|
||||||
|
try:
|
||||||
|
inner = ZipFile(io.BytesIO(outer.read(name)))
|
||||||
|
except (BadZipFile, OSError):
|
||||||
|
continue
|
||||||
|
if self._has_dumpstate(inner.namelist()):
|
||||||
|
log.info("Found the bug report nested inside the archive: %s", name)
|
||||||
|
return inner
|
||||||
|
inner.close()
|
||||||
|
return None
|
||||||
|
|
||||||
def init(self) -> None:
|
def init(self) -> None:
|
||||||
if self.target_path:
|
if self.target_path:
|
||||||
self.log.info("Checking Android bug report at path: %s", self.target_path)
|
self.log.info("Checking Android bug report at path: %s", self.target_path)
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ from mvt.common.module import ModuleResults, MVTModule
|
|||||||
# section finishes and not necessarily between two sections.
|
# section finishes and not necessarily between two sections.
|
||||||
SECTION_DURATION = re.compile(r"^-{3,}\s*[0-9.]+s was the duration of", re.IGNORECASE)
|
SECTION_DURATION = re.compile(r"^-{3,}\s*[0-9.]+s was the duration of", re.IGNORECASE)
|
||||||
|
|
||||||
|
# The members a bug report archive can be entered through, in the order
|
||||||
|
# _get_dumpstate_file() tries them. An archive carrying none of them is not a
|
||||||
|
# bug report at this level (see CmdAndroidCheckBugreport._has_dumpstate).
|
||||||
|
DUMPSTATE_ENTRY_POINTS = ("main_entry.txt", "dumpState_*.log", "*/dumpsys.txt")
|
||||||
|
|
||||||
|
|
||||||
class BugReportModule(MVTModule):
|
class BugReportModule(MVTModule):
|
||||||
"""This class provides a base for all Android Bug Report modules."""
|
"""This class provides a base for all Android Bug Report modules."""
|
||||||
@@ -92,7 +97,9 @@ class BugReportModule(MVTModule):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def _get_dumpstate_file(self) -> Optional[bytes]:
|
def _get_dumpstate_file(self) -> Optional[bytes]:
|
||||||
main = self._get_files_by_pattern("main_entry.txt")
|
main_entry, dumpstate_log, dumpsys_txt = DUMPSTATE_ENTRY_POINTS
|
||||||
|
|
||||||
|
main = self._get_files_by_pattern(main_entry)
|
||||||
if main:
|
if main:
|
||||||
main_content = self._get_file_content(main[0])
|
main_content = self._get_file_content(main[0])
|
||||||
try:
|
try:
|
||||||
@@ -100,11 +107,11 @@ class BugReportModule(MVTModule):
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
dumpstate_logs = self._get_files_by_pattern("dumpState_*.log")
|
dumpstate_logs = self._get_files_by_pattern(dumpstate_log)
|
||||||
if dumpstate_logs:
|
if dumpstate_logs:
|
||||||
return self._get_file_content(dumpstate_logs[0])
|
return self._get_file_content(dumpstate_logs[0])
|
||||||
|
|
||||||
dumpsys_files = self._get_files_by_pattern("*/dumpsys.txt")
|
dumpsys_files = self._get_files_by_pattern(dumpsys_txt)
|
||||||
if dumpsys_files:
|
if dumpsys_files:
|
||||||
return self._get_file_content(dumpsys_files[0])
|
return self._get_file_content(dumpsys_files[0])
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Mobile Verification Toolkit (MVT)
|
||||||
|
# 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/
|
||||||
|
"""An OEM wrapper archive must not read as an empty bug report.
|
||||||
|
|
||||||
|
MIUI / HyperOS hands out a zip of app logs with the real
|
||||||
|
`bugreport-<device>-<timestamp>.zip` nested inside. The outer archive has
|
||||||
|
none of the entry points the modules read, so every module
|
||||||
|
reported it found nothing and the command still exited 0 — an empty analysis
|
||||||
|
that looks like a finished one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from mvt.android.cmd_check_bugreport import CmdAndroidCheckBugreport
|
||||||
|
|
||||||
|
DUMPSTATE = "== dumpstate: 2026-01-01 00:00:00\nDUMP OF SERVICE package:\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _inner_zip() -> bytes:
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buffer, "w") as inner:
|
||||||
|
inner.writestr("main_entry.txt", "bugreport-test-2026-01-01-00-00-00.txt")
|
||||||
|
inner.writestr("bugreport-test-2026-01-01-00-00-00.txt", DUMPSTATE)
|
||||||
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def _wrapper_zip() -> zipfile.ZipFile:
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buffer, "w") as outer:
|
||||||
|
outer.writestr("app_logs/hilog.txt", "unrelated OEM log\n")
|
||||||
|
outer.writestr("bugreport-test-2026-01-01-00-00-00.zip", _inner_zip())
|
||||||
|
return zipfile.ZipFile(io.BytesIO(buffer.getvalue()))
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckBugreportWrapper:
|
||||||
|
def test_nested_bugreport_is_used(self, tmp_path):
|
||||||
|
cmd = CmdAndroidCheckBugreport(results_path=str(tmp_path))
|
||||||
|
cmd.from_zip(_wrapper_zip())
|
||||||
|
module = cmd.modules[0](results_path=str(tmp_path))
|
||||||
|
cmd.module_init(module)
|
||||||
|
assert "main_entry.txt" in module.zip_files
|
||||||
|
|
||||||
|
def test_plain_bugreport_is_left_alone(self, tmp_path):
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buffer, "w") as archive:
|
||||||
|
archive.writestr("main_entry.txt", "bugreport.txt")
|
||||||
|
archive.writestr("bugreport.txt", DUMPSTATE)
|
||||||
|
archive.writestr("attachments/extra.zip", _inner_zip())
|
||||||
|
cmd = CmdAndroidCheckBugreport(results_path=str(tmp_path))
|
||||||
|
cmd.from_zip(zipfile.ZipFile(io.BytesIO(buffer.getvalue())))
|
||||||
|
module = cmd.modules[0](results_path=str(tmp_path))
|
||||||
|
cmd.module_init(module)
|
||||||
|
assert "attachments/extra.zip" in module.zip_files
|
||||||
Reference in new issue
Block a user