Compare commits

..
Author SHA1 Message Date
Donncha Ó Cearbhaill 06cea62a40 Record wall time and peak memory growth per module
Command.run() times every module and measures how much the process's
peak resident set size grew while it ran. Both are logged at INFO in
one line per module and written to info.json under module_stats, in
run order, so any run says which module cost what and CI can compare a
branch against main.

ru_maxrss is the process peak, so a module's growth is how much it
raised the high-water mark, which is what matters on a host without
swap; a module running after a bigger one reports 0. Where the resource
module is unavailable the growth is reported as 0.
2026-09-16 00:45:47 +02:00
8 changed files with 54 additions and 70 deletions

No files matched your search

+1 -1
View File
@@ -24,7 +24,7 @@ dependencies = [
"simplejson==4.1.1",
"packaging==26.3",
"appdirs==1.4.4",
"iphone_backup_decrypt==0.10.0",
"iphone_backup_decrypt==0.9.0",
"pycryptodome>=3.20.0",
"adb-shell[usb]==0.4.4",
"libusb1==3.4.0",
+2 -13
View File
@@ -136,10 +136,6 @@ class Mounts(AndroidArtifact):
mount_options = fields[5].split(",")
super_options = fields[separator + 3].split(",")
options = list(dict.fromkeys(mount_options + super_options))
# fields[5] are the per-mount (VFS) flags, the field after the
# separator the superblock's; a mount is writable only if both
# allow it. The merged list made every read-only overlay a HIGH.
is_read_write = "rw" in mount_options and "rw" in super_options
mount_point = fields[4].replace("\\040", " ")
device = fields[separator + 2].replace("\\040", " ")
filesystem_type = fields[separator + 1]
@@ -160,7 +156,7 @@ class Mounts(AndroidArtifact):
"options_list": options,
"optional_fields": fields[6:separator],
"is_system_partition": is_system,
"is_read_write": is_read_write,
"is_read_write": "rw" in options,
"process_ids": [process_id],
}
)
@@ -195,14 +191,7 @@ class Mounts(AndroidArtifact):
)
# Check for other suspicious mount options
# `rw` sitting in the merged option list is not evidence of
# writability (see is_read_write in parse_mountinfo); every other
# suspicious option is meaningful in either list.
suspicious_opts = [
opt
for opt in options
if opt in SUSPICIOUS_OPTIONS and (opt != "rw" or mount["is_read_write"])
]
suspicious_opts = [opt for opt in options if opt in SUSPICIOUS_OPTIONS]
if suspicious_opts and mount["is_system_partition"]:
if (
"noatime" in mount["mount_options"]
+31
View File
@@ -7,6 +7,7 @@ import json
import logging
import os
import sys
import time
from datetime import datetime
from functools import cached_property
from heapq import heappop, heappush
@@ -36,6 +37,17 @@ from .utils import (
from .version import MVT_VERSION
def _peak_rss_mb() -> float:
"""The process's peak resident set size so far, in MB (0 where unknown)."""
try:
import resource
except ImportError:
return 0.0
peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
# ru_maxrss is in bytes on macOS and in kilobytes elsewhere.
return peak / (1024 * 1024 if sys.platform == "darwin" else 1024)
class Command:
def __init__(
self,
@@ -77,6 +89,9 @@ class Command:
# declaring `replaces` to the module which took their place.
self.module_replacements: dict[type[MVTModule], type[MVTModule]] = {}
# Wall time and peak-RSS growth of every module run, in run order.
self.module_stats: list[dict[str, Any]] = []
# This list will contain all executed modules.
# We can use this to reference e.g. self.executed[0].results.
self.executed: list[MVTModule] = []
@@ -190,6 +205,7 @@ class Command:
"date": convert_datetime_to_iso(datetime.now()),
"ioc_files": [],
"hashes": [],
"module_stats": self.module_stats,
}
for coll in self.iocs.ioc_collections:
@@ -766,6 +782,8 @@ class Command:
except NotImplementedError:
pass
started = time.perf_counter()
peak_before = _peak_rss_mb()
try:
run_module(m)
except EncryptedBackupError:
@@ -775,6 +793,19 @@ class Command:
)
return
stats = {
"module": module.__name__,
"seconds": round(time.perf_counter() - started, 2),
"peak_rss_growth_mb": round(_peak_rss_mb() - peak_before),
}
self.module_stats.append(stats)
self.log.info(
"Module %s ran in %.1fs and raised the peak memory by %d MB",
stats["module"],
stats["seconds"],
stats["peak_rss_growth_mb"],
)
self.executed.append(m)
executed_by_type[module] = m
self.timeline.extend(m.timeline)
-8
View File
@@ -1271,13 +1271,5 @@
{
"version": "26.6.1",
"build": "23G83"
},
{
"version": "26.6.2",
"build": "23G90"
},
{
"version": "27.0",
"build": "24A437"
}
]
+1 -1
View File
@@ -52,7 +52,7 @@ class MVTEncryptedBackup(EncryptedBackup):
NOTE: This subclass relies on internal APIs of iphone_backup_decrypt
(specifically _read_and_unlock_keybag, _keybag, and the Keybag class
internals). Pinned to iphone_backup_decrypt==0.10.0.
internals). Pinned to iphone_backup_decrypt==0.9.0.
"""
def __init__(self, *, backup_directory, passphrase=None, derived_key=None):
-43
View File
@@ -1,43 +0,0 @@
# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2026 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/
"""A mount is writable only if both mountinfo option layers allow it."""
from mvt.android.artifacts.mounts import Mounts
from mvt.common.alerts import AlertLevel
# fields[5] is the per-mount (VFS) layer, the field after "-" the superblock.
RO_OVERLAY = (
"304 166 0:130 / /product/usr rw,relatime shared:63 - overlay overlay "
"ro,seclabel,lowerdir=/mnt/vendor/ext/product/usr:/product/usr"
)
RW_SYSTEM = (
"305 166 0:131 / /system rw,relatime shared:64 - ext4 /dev/block/dm-1 "
"rw,seclabel,errors=panic"
)
def _run(line):
mounts = Mounts()
mounts.results = Mounts.parse_mountinfo(line, 1)
mounts.check_indicators()
return mounts
class TestMountsReadWriteLayers:
def test_rw_vfs_over_ro_superblock_is_not_writable(self):
mounts = _run(RO_OVERLAY)
assert mounts.results[0]["is_read_write"] is False
assert mounts.alertstore.count(AlertLevel.HIGH) == 0
assert mounts.alertstore.count(AlertLevel.MEDIUM) == 0
def test_rw_in_both_layers_still_alerts(self):
mounts = _run(RW_SYSTEM)
assert mounts.results[0]["is_read_write"] is True
assert mounts.alertstore.count(AlertLevel.HIGH) == 1
def test_both_option_layers_are_still_reported(self):
mounts = _run(RO_OVERLAY)
options = mounts.results[0]["options_list"]
assert "rw" in options and "ro" in options
+15
View File
@@ -298,6 +298,21 @@ class TestCommand:
alerts = json.loads((tmp_path / "alerts.json").read_text())
assert alerts[0]["event"]["payload"] == "\\xa8\\xa9"
def test_records_time_and_memory_per_module(self, tmp_path):
cmd = RecordingCommand(results_path=str(tmp_path))
cmd.modules = [FirstModule, SecondModule]
cmd.run()
info = json.loads((tmp_path / "info.json").read_text())
assert [stat["module"] for stat in info["module_stats"]] == [
"FirstModule",
"SecondModule",
]
for stat in info["module_stats"]:
assert stat["seconds"] >= 0
assert stat["peak_rss_growth_mb"] >= 0
def test_stores_collected_urls(self, tmp_path):
cmd = RecordingCommand(results_path=str(tmp_path))
cmd.modules = [URLRecordingModule]
Generated
+4 -4
View File
@@ -625,14 +625,14 @@ wheels = [
[[package]]
name = "iphone-backup-decrypt"
version = "0.10.0"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycryptodome" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/b8/a2ec2b10d792403d36bb6661fea6357179c31ece1ed3285c442e636d6335/iphone_backup_decrypt-0.10.0.tar.gz", hash = "sha256:5f2799e58f908cce28d7929dd82231abc8acd57c25b37bce6a2d6a82e018d343", size = 20000, upload-time = "2026-09-17T21:46:44Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6f/e7/bcdacdec21d628122ba240e7f742ab2175149e58672be63af55ff37a0f28/iphone_backup_decrypt-0.9.0.tar.gz", hash = "sha256:13b18fef3c8e3af627914f8c1a429bbc5555dfb0505239ba49efe99984cc0c96", size = 16125, upload-time = "2024-09-18T15:50:12.179Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/89/10/349b2a720e012921d1457e1d1c2b921b16300a5f3570c4c64d0d3787539f/iphone_backup_decrypt-0.10.0-py3-none-any.whl", hash = "sha256:1ac4345a07b104a3eacab20932b8621cd1e0164e0f03d913e66eed6ea5ee173e", size = 17400, upload-time = "2026-09-17T21:46:42Z" },
{ url = "https://files.pythonhosted.org/packages/b8/94/64a31be93f72e0a254bde68e4cf7d24aef37a0a985754a197fa1b028a665/iphone_backup_decrypt-0.9.0-py3-none-any.whl", hash = "sha256:55b5adfafac757f58aa6444b83a4cc2c20cdd699c6ff1d2f4b549936a5dad92c", size = 15767, upload-time = "2024-09-18T15:50:10.537Z" },
]
[[package]]
@@ -1068,7 +1068,7 @@ requires-dist = [
{ name = "betterproto2", specifier = "==0.10.0" },
{ name = "click", specifier = "==8.4.2" },
{ name = "cryptography", specifier = "==50.0.0" },
{ name = "iphone-backup-decrypt", specifier = "==0.10.0" },
{ name = "iphone-backup-decrypt", specifier = "==0.9.0" },
{ name = "libusb1", specifier = "==3.4.0" },
{ name = "nskeyedunarchiver", specifier = "==1.5.2" },
{ name = "packaging", specifier = "==26.3" },