From ab48a48a68e9ef0a16ff704908a60f9f68af31ae Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Wed, 29 Apr 2026 17:56:54 +0200 Subject: [PATCH 1/2] apt_dpkg: fix GDB command for amd64 sandbox on non-amd64 `test_retrace_jammy_sandbox` fails on non-amd64 (e.g. on arm64): ``` $ pytest tests/system/test_apport_retrace.py::test_retrace_jammy_sandbox [...] /usr/lib/python3.14/subprocess.py:578: CalledProcessError --------------------------- Captured stdout call --------------------------- Get:1 http://de.archive.ubuntu.com/ubuntu jammy InRelease [270 kB] Get:2 http://de.archive.ubuntu.com/ubuntu jammy/main Sources [1340 kB] Get:3 http://de.archive.ubuntu.com/ubuntu jammy/main amd64 Packages [1395 kB] Get:4 http://de.archive.ubuntu.com/ubuntu jammy/main amd64 Components [423 kB] Get:5 http://de.archive.ubuntu.com/ubuntu jammy/main amd64 c-n-f Metadata [30.3 kB] Fetched 3458 kB in 0s (0 B/s) Fetched 0 B in 0s (0 B/s) Extracting downloaded debs... dynamically loaded /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 needs package libc6, queueing dynamically loaded /usr/lib/x86_64-linux-gnu/libc.so.6 needs package libc6, queueing Installing extra package coreutils to get ExecutablePath Get:1 http://de.archive.ubuntu.com/ubuntu jammy/main amd64 libc6 amd64 2.35-0ubuntu3 [3235 kB] Get:2 http://de.archive.ubuntu.com/ubuntu jammy/main amd64 coreutils amd64 8.32-4.1ubuntu1 [1438 kB] Get:3 http://de.archive.ubuntu.com/ubuntu jammy/main amd64 libc6-dbg amd64 2.35-0ubuntu3 [13.9 MB] Get:4 [6087 kB] Fetched 24.7 MB in 0s (0 B/s) Extracting downloaded debs... --------------------------- Captured stderr call --------------------------- ERROR: [Errno 2] No such file or directory: '/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2' ``` Avoid calling binaries from other architectures to determine the library search path. So add a hard-coded mapping from the Debian architecture to the multi-arch triplet instead. Extent `get_library_paths` to take an optional `architecture` parameter which returns the default value from ld.so. The dpkg-dev dependency can be dropped for the integration tests, because `dpkg-architecture` is not called any more. Bug: https://launchpad.net/bugs/2150427 --- .github/workflows/ci.yaml | 2 +- apport/package_info.py | 8 ++-- apport/packaging_impl/apt_dpkg.py | 25 +++++++++-- apport/report.py | 29 ++++-------- tests/integration/test_packaging_apt_dpkg.py | 1 - tests/unit/test_packaging_apt_dpkg.py | 5 +++ tests/unit/test_report.py | 46 -------------------- 7 files changed, 42 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 72712747f..70e30d75b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ env: kde_deps: procps pyqt6-dev-tools python3-pyqt6 unit_deps: locales python3-systemd python3-zstandard integration_deps: > - binutils dpkg-dev gcc gdb kmod libc6-dev libglib2.0-dev libxml2-utils + binutils gcc gdb kmod libc6-dev libglib2.0-dev libxml2-utils polkitd python3-systemd valgrind xterm system_deps: > chaos-marmosets dbus dbus-x11 dirmngr dpkg-dev gcc gdb gvfs-daemons psmisc diff --git a/apport/package_info.py b/apport/package_info.py index 72a2db7d2..7507487c3 100644 --- a/apport/package_info.py +++ b/apport/package_info.py @@ -157,14 +157,16 @@ def get_native_multiarch_triplet() -> str: "this method must be implemented by a concrete subclass if applicable" ) - def get_library_paths(self) -> str: - """Return a list of default library search paths. + def get_library_paths(self, architecture: str | None = None) -> str: + """Return a list of default library search paths for the architecture. + + If architecture is not specified, the system archictecture is used. The entries should be separated with a colon ':', like for $LD_LIBRARY_PATH. This needs to take any multiarch directories into account. """ - # simple default implementation, pylint: disable=no-self-use + # simple default implementation, pylint: disable=no-self-use,unused-argument return "/lib:/usr/lib" def set_mirror(self, url: str) -> None: diff --git a/apport/packaging_impl/apt_dpkg.py b/apport/packaging_impl/apt_dpkg.py index 83ec35674..849a52c6a 100644 --- a/apport/packaging_impl/apt_dpkg.py +++ b/apport/packaging_impl/apt_dpkg.py @@ -57,6 +57,15 @@ import apport.logging from apport.package_info import PackageInfo +_ARCH_MULTIARCH_TRIPLET_MAPPING = { + "amd64": "x86_64-linux-gnu", + "arm64": "aarch64-linux-gnu", + "armhf": "arm-linux-gnueabihf", + "i386": "i386-linux-gnu", + "ppc64el": "powerpc64le-linux-gnu", + "riscv64": "riscv64-linux-gnu", + "s390x": "s390x-linux-gnu", +} # The Contents-*.gz files are huge. Loading all data into memory would result in a # dictionary with millions of entries consuming several gigabytes of memory. # Therefore exclude unneeded paths with 100k entries or more. @@ -881,14 +890,24 @@ def get_native_multiarch_triplet() -> str: ) return dpkg.stdout.strip() - def get_library_paths(self) -> str: - """Return a list of default library search paths. + def get_library_paths(self, architecture: str | None = None) -> str: + """Return a list of default library search paths for the architecture. + + If architecture is not specified, the system archictecture is used. The entries should be separated with a colon ':', like for $LD_LIBRARY_PATH. This needs to take any multiarch directories into account. """ - return f"/lib/{self.get_native_multiarch_triplet()}:/lib" + if architecture is None: + architecture = self.get_system_architecture() + multiarch_triplet = _ARCH_MULTIARCH_TRIPLET_MAPPING.get(architecture) + if multiarch_triplet is None: + raise NotImplementedError( + f"Missing multi-arch triplet information" + f" for architecture {architecture}" + ) + return f"/lib/{multiarch_triplet}:/usr/lib/{multiarch_triplet}:/lib:/usr/lib" def set_mirror(self, url: str) -> None: """Explicitly set a distribution mirror URL for operations that need to diff --git a/apport/report.py b/apport/report.py index c828c8b63..f349f2ca1 100644 --- a/apport/report.py +++ b/apport/report.py @@ -341,21 +341,6 @@ def _get_gdb_path(gdb_sandbox: str | None, same_arch: bool) -> str: return gdb_path -def _get_ld_search_paths(ld_so: str = "ld.so") -> list[str]: - """Query the given ld.so binary for the shared library search paths.""" - diagnostics = subprocess.run( - [ld_so, "--list-diagnostics"], check=True, stdout=subprocess.PIPE, text=True - ) - # Example matching line: path.system_dirs[0x0]="/lib/x86_64-linux-gnu/" - matches = re.findall( - r'^path\.system_dirs\[(0x[0-9a-f]+)\]="([^"]+)"$', - diagnostics.stdout, - flags=re.MULTILINE, - ) - ld_search_paths = {int(index, 0): path for (index, path) in matches} - return [path for (index, path) in sorted(ld_search_paths.items())] - - def _run_hook(report, ui, hook): if not os.path.exists(hook): return False @@ -2009,10 +1994,13 @@ def gdb_command( environ: dict[str, str] = {} if sandbox: - host_library_paths = _get_ld_search_paths( - "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2" - ) - sandbox_library_paths = [f"{sandbox}{path}" for path in host_library_paths] + arch = self.get("PackageArchitecture") + if not arch or arch == "all": + arch = self.get("Architecture") + sandbox_library_paths = [ + f"{sandbox}{path}" + for path in packaging.get_library_paths(arch).split(":") + ] # N.B. set solib-absolute-prefix is an alias for set sysroot command += [ "--ex", @@ -2027,7 +2015,8 @@ def gdb_command( ] if gdb_sandbox: gdb_sandbox_library_paths = [ - f"{gdb_sandbox}{path}" for path in host_library_paths + f"{gdb_sandbox}{path}" + for path in packaging.get_library_paths().split(":") ] ld_lib_path = ":".join(gdb_sandbox_library_paths) pyhome = f"{gdb_sandbox}/usr" diff --git a/tests/integration/test_packaging_apt_dpkg.py b/tests/integration/test_packaging_apt_dpkg.py index a9af7a4f7..d1c62fb7c 100644 --- a/tests/integration/test_packaging_apt_dpkg.py +++ b/tests/integration/test_packaging_apt_dpkg.py @@ -525,7 +525,6 @@ def test_get_system_architecture(self) -> None: self.assertNotEqual(arch, "") self.assertNotIn("\n", arch) - @skip_if_command_is_missing("dpkg-architecture") def test_get_library_paths(self) -> None: """get_library_paths().""" paths = impl.get_library_paths() diff --git a/tests/unit/test_packaging_apt_dpkg.py b/tests/unit/test_packaging_apt_dpkg.py index 6c9c77618..16d0d9d35 100644 --- a/tests/unit/test_packaging_apt_dpkg.py +++ b/tests/unit/test_packaging_apt_dpkg.py @@ -102,6 +102,11 @@ def test_is_distro_package_system_image( getitem_mock.assert_called_once_with("apport") exists_mock.assert_called_once_with("/etc/system-image/channel.ini") + def test_get_library_paths_unknown_arch(self) -> None: + """get_library_paths() for unknown architecture.""" + with self.assertRaisesRegex(NotImplementedError, "architecture osprey64"): + impl.get_library_paths("osprey64") + def test_map_mirror_to_arch_ports_to_primary(self) -> None: """Test _map_mirror_to_arch() to map ports to primary.""" self.assertEqual( diff --git a/tests/unit/test_report.py b/tests/unit/test_report.py index bfc6e3b40..b0ce6b588 100644 --- a/tests/unit/test_report.py +++ b/tests/unit/test_report.py @@ -1644,49 +1644,3 @@ def test_add_kernel_crash_info_fail(self, run_mock: MagicMock) -> None: tmpfile = called_cmd[-1] self.assertRegex(tmpfile, r"^/.*/apport_vmcore_\w{8}$") self.assertFalse(pathlib.Path(tmpfile).exists()) - - @unittest.mock.patch("subprocess.run") - def test_get_ld_search_paths(self, run_mock: MagicMock) -> None: - run_mock.return_value = subprocess.CompletedProcess( - args=MagicMock(), - returncode=0, - stdout=textwrap.dedent("""\ - path.prefix="/usr" - path.sysconfdir="/etc" - path.system_dirs[0x0]="/lib/x86_64-linux-gnu/" - path.system_dirs[0x1]="/usr/lib/x86_64-linux-gnu/" - path.system_dirs[0x2]="/lib/" - path.system_dirs[0x3]="/usr/lib/" - """), - ) - ld_search_paths = apport.report._get_ld_search_paths() - self.assertEqual( - ld_search_paths, - [ - "/lib/x86_64-linux-gnu/", - "/usr/lib/x86_64-linux-gnu/", - "/lib/", - "/usr/lib/", - ], - ) - run_mock.assert_called_once_with( - ["ld.so", "--list-diagnostics"], - check=True, - stdout=subprocess.PIPE, - text=True, - ) - - @unittest.mock.patch("subprocess.run") - def test_get_ld_search_paths_sorting(self, run_mock: MagicMock) -> None: - run_mock.return_value = subprocess.CompletedProcess( - args=MagicMock(), - returncode=0, - stdout=textwrap.dedent("""\ - path.system_dirs[0x2]="third" - path.system_dirs[0x1]="second" - path.system_dirs[0x0]="first" - """), - ) - ld_search_paths = apport.report._get_ld_search_paths("ld.so") - self.assertEqual(ld_search_paths, ["first", "second", "third"]) - run_mock.assert_called_once() From 2c69dc55b979252e7258ea3db0ba956f48eab929 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Wed, 29 Apr 2026 17:57:21 +0200 Subject: [PATCH 2/2] packaging: drop get_native_multiarch_triplet method The method `get_native_multiarch_triplet` was introduced in commit bd9e8620049 ("report: gdb_command: don't hardcode the GNU triplet for the search paths"). `get_native_multiarch_triplet` and `gdb_command` do not use this method any more. Therefore `get_native_multiarch_triplet` can be dropped. --- apport/package_info.py | 9 --------- apport/packaging_impl/apt_dpkg.py | 12 ------------ 2 files changed, 21 deletions(-) diff --git a/apport/package_info.py b/apport/package_info.py index 7507487c3..57217f9c1 100644 --- a/apport/package_info.py +++ b/apport/package_info.py @@ -148,15 +148,6 @@ def get_system_architecture() -> str: "this method must be implemented by a concrete subclass" ) - @staticmethod - def get_native_multiarch_triplet() -> str: - """Return the GNU multiarch triplet for of the system architecture, if - applicable, raises NotImplementedError otherwise. - """ - raise NotImplementedError( - "this method must be implemented by a concrete subclass if applicable" - ) - def get_library_paths(self, architecture: str | None = None) -> str: """Return a list of default library search paths for the architecture. diff --git a/apport/packaging_impl/apt_dpkg.py b/apport/packaging_impl/apt_dpkg.py index 849a52c6a..05f5fb84d 100644 --- a/apport/packaging_impl/apt_dpkg.py +++ b/apport/packaging_impl/apt_dpkg.py @@ -878,18 +878,6 @@ def get_system_architecture() -> str: assert arch return arch - @staticmethod - @functools.cache - def get_native_multiarch_triplet() -> str: - """Return the multiarch triplet for the system architecture""" - dpkg = subprocess.run( - ["dpkg-architecture", "-qDEB_HOST_MULTIARCH"], - check=True, - text=True, - stdout=subprocess.PIPE, - ) - return dpkg.stdout.strip() - def get_library_paths(self, architecture: str | None = None) -> str: """Return a list of default library search paths for the architecture.