From 92f780df5a58b79981b68850587e993f81dc2c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donncha=20=C3=93=20Cearbhaill?= Date: Mon, 7 Sep 2026 23:58:43 +0100 Subject: [PATCH] Report an unreadable sysdiagnose archive instead of aborting silently (#920) A sysdiagnose tarball whose download stopped halfway ends in an EOFError from gzip while check-sysdiagnose extracts it. Click turns EOFError into click.Abort, so the command printed nothing but "Aborted!", even with -v. The extraction now catches the read errors an archive can raise, names the file and the reason at critical level, says the file may be truncated or not a gzip tarball, and exits 1. A plain .tar given to the gzip reader gets the same message rather than a traceback. --- src/mvt/ios/cmd_check_sysdiagnose.py | 17 +++++++++++++++-- tests/test_check_ios_sysdiagnose.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/mvt/ios/cmd_check_sysdiagnose.py b/src/mvt/ios/cmd_check_sysdiagnose.py index 3450a03f..88a12db0 100644 --- a/src/mvt/ios/cmd_check_sysdiagnose.py +++ b/src/mvt/ios/cmd_check_sysdiagnose.py @@ -7,7 +7,9 @@ import json import logging import os import shutil +import sys import tarfile +import zlib from pathlib import Path, PurePosixPath from tempfile import TemporaryDirectory from typing import Any, Optional @@ -97,8 +99,19 @@ class CmdIOSCheckSysdiagnose(Command): self.log.info("Parsing sysdiagnose archive. This might take a while...") self.sysdiagnose_format = "tar" - self.sysdiagnose_archive = tarfile.open(self.target_path, "r:gz") - self._extract_sysdiagnose_archive() + try: + self.sysdiagnose_archive = tarfile.open(self.target_path, "r:gz") + self._extract_sysdiagnose_archive() + except (tarfile.ReadError, EOFError, zlib.error, OSError) as exc: + # A truncated archive ends in EOFError from gzip, which Click would + # otherwise report as a bare "Aborted!" with no reason. + self.log.critical( + "Unable to read the sysdiagnose archive %s: %s. " + "The file may be truncated or not a gzip-compressed tarball.", + self.target_path, + exc, + ) + sys.exit(1) def _extract_sysdiagnose_archive(self) -> None: archive = self.sysdiagnose_archive diff --git a/tests/test_check_ios_sysdiagnose.py b/tests/test_check_ios_sysdiagnose.py index 0cd4221a..c81e213a 100644 --- a/tests/test_check_ios_sysdiagnose.py +++ b/tests/test_check_ios_sysdiagnose.py @@ -1,4 +1,6 @@ import logging +import os +import tarfile from click.testing import CliRunner @@ -62,3 +64,29 @@ def test_check_sysdiagnose_warns_without_a_custom_module(tmp_path, caplog): assert result.exit_code == 0 assert "No forensic sysdiagnose modules have been loaded" in caplog.text + + +def _create_truncated_sysdiagnose_archive(tmp_path): + folder = tmp_path / "sysdiagnose_2026.01.01_00-00-00+0000_iPhone-OS_iPhone_23A000" + folder.mkdir() + (folder / "sysdiagnose.log").write_bytes(os.urandom(200_000)) + archive = tmp_path / "sysdiagnose.tar.gz" + with tarfile.open(archive, "w:gz") as tar: + tar.add(folder, arcname=folder.name) + data = archive.read_bytes() + archive.write_bytes(data[: len(data) // 2]) + return archive + + +def test_check_sysdiagnose_reports_a_truncated_archive(tmp_path, caplog): + # A download that stopped halfway ends in EOFError from gzip, which Click + # would otherwise turn into a bare "Aborted!" with no reason given. + archive = _create_truncated_sysdiagnose_archive(tmp_path) + + with caplog.at_level(logging.CRITICAL, logger="mvt"): + result = CliRunner().invoke(check_sysdiagnose, [str(archive)]) + + assert result.exit_code == 1 + assert "Unable to read the sysdiagnose archive" in caplog.text + assert "truncated" in caplog.text + assert "Aborted!" not in result.output