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..8fe0eef3 100644 --- a/src/pystack/__main__.py +++ b/src/pystack/__main__.py @@ -356,10 +356,25 @@ def process_core(parser: argparse.ArgumentParser, args: argparse.Namespace) -> N print(format_psinfo_information(corefile_analyzer.extract_ps_info())) 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" + if args.executable is not None and not is_elf(executable): + 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, + )