From 7179c4cb069bb570f4433a94028262d4afbd83a5 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Thu, 2 Jul 2026 13:11:33 +0200 Subject: [PATCH] apt_dpkg: discard cached contents mapping on EOFError test_retrace_jammy_sandbox might fail in a autopkgtest VM: ``` Traceback (most recent call last): File "/usr/bin/apport-retrace", line 713, in sys.exit(main(sys.argv[1:])) ~~~~^^^^^^^^^^^^^^ File "/usr/bin/apport-retrace", line 542, in main sandbox, cache, outdated_msg = apport.sandboxutils.make_sandbox( ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ report, ^^^^^^^ ...<6 lines>... options.dynamic_origins, ^^^^^^^^^^^^^^^^^^^^^^^^ ) ^ File "/usr/lib/python3/dist-packages/apport/sandboxutils.py", line 274, in make_sandbox pkgs = needed_runtime_packages(report, pkgmap_cache_dir, pkg_versions, verbose) File "/usr/lib/python3/dist-packages/apport/sandboxutils.py", line 103, in needed_runtime_packages pkg = packaging.get_file_package( line, ...<3 lines>... arch=report.get("Architecture"), ) File "/usr/lib/python3/dist-packages/apport/packaging_impl/apt_dpkg.py", line 800, in get_file_package return self._search_contents(file, map_cachedir, release, arch) ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3/dist-packages/apport/packaging_impl/apt_dpkg.py", line 1752, in _search_contents contents_mapping = self._get_file2pkg_mapping(map_cachedir, release, arch) File "/usr/lib/python3/dist-packages/apport/packaging_impl/apt_dpkg.py", line 1726, in _get_file2pkg_mapping file2pkg = self._contents_mapping(map_cachedir, release, arch) File "/usr/lib/python3/dist-packages/apport/packaging_impl/apt_dpkg.py", line 340, in _contents_mapping self._contents_mapping_obj = pickle.load(fp) ~~~~~~~~~~~^^^^ EOFError: Ran out of input ``` Discard the cache in those cases and just rebuild them. Bug: https://launchpad.net/bugs/2149911 --- apport/packaging_impl/apt_dpkg.py | 4 +-- tests/unit/test_packaging_apt_dpkg.py | 51 +++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/apport/packaging_impl/apt_dpkg.py b/apport/packaging_impl/apt_dpkg.py index 593c3ee06..55f69e0e3 100644 --- a/apport/packaging_impl/apt_dpkg.py +++ b/apport/packaging_impl/apt_dpkg.py @@ -346,7 +346,7 @@ def _virtual_mapping(self, configdir: str) -> dict[str, set[str]]: with open(mapping_file, "rb") as fp: self._virtual_mapping_obj = pickle.load(fp) assert isinstance(self._virtual_mapping_obj, dict) - except (AssertionError, FileNotFoundError): + except (AssertionError, EOFError, FileNotFoundError, pickle.UnpicklingError): self._virtual_mapping_obj = {} return self._virtual_mapping_obj @@ -378,7 +378,7 @@ def _contents_mapping( assert isinstance(self._contents_mapping_obj, dict) # Discard files from Apport < 2.35.0 assert isinstance(next(iter(self._contents_mapping_obj)), str) - except (AssertionError, FileNotFoundError): + except (AssertionError, EOFError, FileNotFoundError, pickle.UnpicklingError): self._contents_mapping_obj = {"release": release, "arch": arch} return self._contents_mapping_obj diff --git a/tests/unit/test_packaging_apt_dpkg.py b/tests/unit/test_packaging_apt_dpkg.py index 5692021ba..68c7ca3a0 100644 --- a/tests/unit/test_packaging_apt_dpkg.py +++ b/tests/unit/test_packaging_apt_dpkg.py @@ -40,11 +40,15 @@ def setUp(self) -> None: # Clear APT cache to allow mocking it # pylint: disable-next=protected-access impl._apt_cache = None + # pylint: disable-next=protected-access + impl._contents_mapping_obj = None def tearDown(self) -> None: # Clear APT cache to clear cached mocks # pylint: disable-next=protected-access impl._apt_cache = None + # pylint: disable-next=protected-access + impl._contents_mapping_obj = None @unittest.mock.patch("apt.Cache", spec=apt.Cache) def test_is_distro_package_no_candidate(self, apt_cache_mock: MagicMock) -> None: @@ -210,6 +214,29 @@ def test_get_file_package_uninstalled_usrmerge( "/map_cachedir", impl.get_distro_codename(), "amd64" ) + def test_contents_mapping(self) -> None: + """Test _contents_mapping() loading and saving.""" + # pylint: disable=protected-access + with tempfile.TemporaryDirectory() as workdir: + # Test non-existing cache file + file2pkg = impl._contents_mapping(workdir, "resolute", "amd64") + self.assertEqual(file2pkg, {"release": "resolute", "arch": "amd64"}) + + # Modify and save cache + file2pkg["path/to/file"] = "package1" + impl._save_contents_mapping(workdir, "resolute", "amd64") + + # Test opening different release/arch + file2pkg = impl._contents_mapping(workdir, "noble", "amd64") + self.assertEqual(file2pkg, {"release": "noble", "arch": "amd64"}) + + # Test loading contents mapping from cache file + file2pkg = impl._contents_mapping(workdir, "resolute", "amd64") + self.assertEqual( + file2pkg, + {"release": "resolute", "arch": "amd64", "path/to/file": "package1"}, + ) + def test_contents_mapping_load_legacy_resolute(self) -> None: """Test _contents_mapping() discarding cache file from Apport 2.34.0.""" with tempfile.TemporaryDirectory() as workdir: @@ -228,6 +255,30 @@ def test_contents_mapping_load_legacy_resolute(self) -> None: file2pkg = impl._contents_mapping(workdir, "resolute", "amd64") self.assertEqual(file2pkg, {"release": "resolute", "arch": "amd64"}) + def test_contents_mapping_truncated_file(self) -> None: + """Test _contents_mapping() reading truncated files.""" + # pylint: disable=protected-access + with tempfile.TemporaryDirectory() as workdir: + file2pkg = impl._contents_mapping(workdir, "resolute", "amd64") + file2pkg["path/to/file"] = "package1" + impl._save_contents_mapping(workdir, "resolute", "amd64") + mapping_path = ( + pathlib.Path(workdir) / "contents_mapping-resolute-amd64.pickle" + ) + saved_mapping = mapping_path.read_bytes() + self.assertGreater(len(saved_mapping), 32) + for truncate_length in (2, 32): + with self.subTest(truncate_length=truncate_length): + # Clear cache + impl._contents_mapping(workdir, "noble", "amd64") + + mapping_path.write_bytes(saved_mapping[0:truncate_length]) + self.assertEqual(mapping_path.stat().st_size, truncate_length) + + # Test ignoring truncated file + file2pkg = impl._contents_mapping(workdir, "resolute", "amd64") + self.assertEqual(file2pkg, {"release": "resolute", "arch": "amd64"}) + def test_contents_skip_xenial_header(self) -> None: """Test _update_given_file2pkg_mapping skipping xenial Contents header.""" # Header taken from