From 056b93c1c9692747aaa7b41af187744f2908f6fc Mon Sep 17 00:00:00 2001 From: Pablo Galindo Date: Fri, 24 May 2024 15:23:02 +0100 Subject: [PATCH] Fallback to the first executable map if the given executable is invalid When a core file is created from a Python script that's executed directly via shebang, the reported executable will be the shell script. This is a problem because when we proceed to analyse the core file pystack fails as this is not the correct binary that was used to generate the core. To avoid this problem, detect when this happens and fall back to the executable reported in the first map that has a path in the core, which is generally the real executable that we need. Signed-off-by: Pablo Galindo --- news/184.bugfix.rst | 1 + src/pystack/__main__.py | 19 +++++++++-- src/pystack/_pystack.pyi | 3 +- src/pystack/_pystack.pyx | 2 +- tests/unit/test_main.py | 69 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 news/184.bugfix.rst diff --git a/news/184.bugfix.rst b/news/184.bugfix.rst new file mode 100644 index 00000000..1eee3f4a --- /dev/null +++ b/news/184.bugfix.rst @@ -0,0 +1 @@ +Fix a bug that was causing Python scripts executed directly via shebang to report the shell script as the executable. diff --git a/src/pystack/__main__.py b/src/pystack/__main__.py index 46724194..c83ddab6 100644 --- a/src/pystack/__main__.py +++ b/src/pystack/__main__.py @@ -357,9 +357,24 @@ def process_core(parser: argparse.ArgumentParser, args: argparse.Namespace) -> N print(format_failureinfo_information(corefile_analyzer.extract_failure_info())) if not is_elf(executable): - raise errors.InvalidExecutable( - f"The provided executable ({executable}) doesn't have an executable format" + first_map = next( + (map for map in corefile_analyzer.extract_maps() if map.path is not None), + None, ) + if ( + first_map is not None + and first_map.path is not None + and is_elf(first_map.path) + ): + executable = first_map.path + LOGGER.info( + "Setting executable automatically to the first map in the core: %s", + executable, + ) + else: + raise errors.InvalidExecutable( + f"The provided executable ({executable}) doesn't have an executable format" + ) if args.native_mode != NativeReportingMode.OFF: for module in corefile_analyzer.missing_modules(): diff --git a/src/pystack/_pystack.pyi b/src/pystack/_pystack.pyi index 36417c8a..f6096b69 100644 --- a/src/pystack/_pystack.pyi +++ b/src/pystack/_pystack.pyi @@ -8,6 +8,7 @@ from typing import Optional from typing import Tuple from typing import Union +from .maps import VirtualMap from .types import PyThread class CoreFileAnalyzer: @@ -17,7 +18,7 @@ class CoreFileAnalyzer: def extract_build_ids(self) -> Iterable[Tuple[str, str, str]]: ... def extract_executable(self) -> pathlib.Path: ... def extract_failure_info(self) -> Dict[str, Any]: ... - def extract_maps(self) -> Iterable[Dict[str, Any]]: ... + def extract_maps(self) -> Iterable[VirtualMap]: ... def extract_pid(self) -> int: ... def extract_ps_info(self) -> Dict[str, Any]: ... def missing_modules(self) -> List[str]: ... diff --git a/src/pystack/_pystack.pyx b/src/pystack/_pystack.pyx index a949a1ac..a64f05fb 100644 --- a/src/pystack/_pystack.pyx +++ b/src/pystack/_pystack.pyx @@ -212,7 +212,7 @@ cdef class CoreFileAnalyzer: self._core_analyzer = make_shared[CoreFileExtractor](analyzer) @intercept_runtime_errors(EngineError) - def extract_maps(self) -> Iterable[Dict[str, Any]]: + def extract_maps(self) -> Iterable[VirtualMap]: mapped_files = self._core_analyzer.get().extractMappedFiles() memory_maps = self._core_analyzer.get().MemoryMaps() return generate_maps_from_core_data(mapped_files, memory_maps) diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 17b848a3..1fa788c2 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -27,6 +27,7 @@ from pystack.errors import InvalidPythonProcess from pystack.errors import MissingExecutableMaps from pystack.errors import NotEnoughInformation +from pystack.maps import VirtualMap def test_error_message_wih_permission_error_text(): @@ -1299,3 +1300,71 @@ def test_core_file_missing_build_ids_are_logged(caplog, native): else [] ) assert record_messages == expected + + +def test_executable_is_not_elf_uses_the_first_map(): + # GIVEN + + argv = ["pystack", "core", "corefile", "executable"] + + # WHEN + real_executable = Path("/foo/bar/executable") + + with patch( + "pystack.__main__.get_process_threads_for_core" + ) as get_process_threads_mock, patch("pystack.__main__.print_thread"), patch( + "pystack.__main__.is_elf", lambda x: x == real_executable + ), patch( + "pystack.__main__.is_gzip", return_value=False + ), patch( + "sys.argv", argv + ), patch( + "pathlib.Path.exists", return_value=True + ), patch( + "pystack.__main__.CoreFileAnalyzer" + ) as core_analyzer_test: + core_analyzer_test().extract_executable.return_value = "extracted_executable" + core_analyzer_test().extract_maps.return_value = [ + VirtualMap( + start=0x1000, + end=0x2000, + flags="r-xp", + offset=0, + device="00:00", + inode=0, + filesize=0, + path=None, + ), + VirtualMap( + start=0x2000, + end=0x3000, + flags="rw-p", + offset=0, + device="00:00", + inode=0, + filesize=0, + path=Path("/foo/bar/executable"), + ), + VirtualMap( + start=0x3000, + end=0x4000, + flags="r--p", + offset=0, + device="00:00", + inode=0, + filesize=0, + path=None, + ), + ] + main() + + # THEN + + get_process_threads_mock.assert_called_with( + Path("corefile"), + real_executable, + library_search_path="", + native_mode=NativeReportingMode.OFF, + locals=False, + method=StackMethod.AUTO, + )