From 3358e2d05f5dd1b6710cbc9199af0d9ab2548a80 Mon Sep 17 00:00:00 2001 From: Diego Russo Date: Thu, 7 May 2026 13:09:00 +0100 Subject: [PATCH 1/3] Skip GNU backtrace test on Arm 32-bit without unwind tables backtrace() on 32-bit ARM EABI depends on runtime unwind tables emitted as .ARM.exidx/.ARM.extab. Without -funwind-tables, glibc can return an empty stack even though GDB can still unwind using debug-only .debug_frame data. Only skip the GNU backtrace unwind test on Arm 32-bit builds that lack -funwind-tables, instead of skipping all Arm 32-bit builds. --- Lib/test/test_frame_pointer_unwind.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Lib/test/test_frame_pointer_unwind.py b/Lib/test/test_frame_pointer_unwind.py index faa012c9c00d8f..8ffd64000f89cc 100644 --- a/Lib/test/test_frame_pointer_unwind.py +++ b/Lib/test/test_frame_pointer_unwind.py @@ -89,6 +89,21 @@ def _frame_pointers_expected(machine): return None +def _gnu_backtrace_requires_unwind_tables(machine): + if not (sys.maxsize < 2**32 and machine.startswith("arm")): + return False + + cflags = " ".join( + value for value in ( + sysconfig.get_config_var("PY_CFLAGS"), + sysconfig.get_config_var("PY_CORE_CFLAGS"), + sysconfig.get_config_var("CFLAGS"), + ) + if value + ) + return "-funwind-tables" not in cflags.split() + + def _build_stack_and_unwind(unwinder): import operator @@ -295,6 +310,10 @@ def test_manual_unwind_respects_frame_pointers(self): @support.requires_gil_enabled("test requires the GIL enabled") @unittest.skipIf(support.is_wasi, "test not supported on WASI") @unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux") +@unittest.skipIf( + _gnu_backtrace_requires_unwind_tables(platform.machine().lower()), + "GNU backtrace unwinding on Arm 32-bit requires -funwind-tables", +) class GnuBacktraceUnwindTests(unittest.TestCase): def setUp(self): From cb52a24b980f376055b6618923690b9683abddd9 Mon Sep 17 00:00:00 2001 From: Diego Russo Date: Thu, 7 May 2026 13:09:00 +0100 Subject: [PATCH 2/3] Refine ARM GNU backtrace unwind-table detection --- Lib/test/test_frame_pointer_unwind.py | 74 ++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_frame_pointer_unwind.py b/Lib/test/test_frame_pointer_unwind.py index 8ffd64000f89cc..e33b07e4b8a011 100644 --- a/Lib/test/test_frame_pointer_unwind.py +++ b/Lib/test/test_frame_pointer_unwind.py @@ -1,6 +1,7 @@ import json import os import platform +import shlex import subprocess import sys import sysconfig @@ -89,19 +90,76 @@ def _frame_pointers_expected(machine): return None -def _gnu_backtrace_requires_unwind_tables(machine): - if not (sys.maxsize < 2**32 and machine.startswith("arm")): +def _is_arm32_build(): + if sys.maxsize >= 2**32: return False - cflags = " ".join( + abi = " ".join( + value for value in ( + sysconfig.get_config_var("MULTIARCH"), + sysconfig.get_config_var("HOST_GNU_TYPE"), + sysconfig.get_config_var("SOABI"), + ) + if value + ).lower() + return "arm" in abi + + +def _cflags_have_unwind_tables(cflags): + unwind_tables = False + asynchronous_unwind_tables = False + exceptions = False + + try: + options = shlex.split(cflags) + except ValueError: + options = cflags.split() + + for option in options: + if option == "-funwind-tables": + unwind_tables = True + elif option == "-fno-unwind-tables": + unwind_tables = False + elif option == "-fasynchronous-unwind-tables": + asynchronous_unwind_tables = True + elif option == "-fno-asynchronous-unwind-tables": + asynchronous_unwind_tables = False + elif option == "-fexceptions": + exceptions = True + elif option == "-fno-exceptions": + exceptions = False + + return unwind_tables or asynchronous_unwind_tables or exceptions + + +def _build_has_unwind_tables(): + cflags = [ value for value in ( - sysconfig.get_config_var("PY_CFLAGS"), sysconfig.get_config_var("PY_CORE_CFLAGS"), - sysconfig.get_config_var("CFLAGS"), + sysconfig.get_config_var("PY_STDMODULE_CFLAGS"), ) if value + ] + if not cflags: + cflags = [ + value for value in ( + sysconfig.get_config_var("PY_CFLAGS"), + sysconfig.get_config_var("CFLAGS"), + ) + if value + ] + + return ( + bool(cflags) + and all(_cflags_have_unwind_tables(value) for value in cflags) ) - return "-funwind-tables" not in cflags.split() + + +def _gnu_backtrace_requires_unwind_tables(): + if not _is_arm32_build(): + return False + + return not _build_has_unwind_tables() def _build_stack_and_unwind(unwinder): @@ -311,8 +369,8 @@ def test_manual_unwind_respects_frame_pointers(self): @unittest.skipIf(support.is_wasi, "test not supported on WASI") @unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux") @unittest.skipIf( - _gnu_backtrace_requires_unwind_tables(platform.machine().lower()), - "GNU backtrace unwinding on Arm 32-bit requires -funwind-tables", + _gnu_backtrace_requires_unwind_tables(), + "GNU backtrace unwinding on Arm 32-bit requires unwind tables", ) class GnuBacktraceUnwindTests(unittest.TestCase): From 61015e1651bc434daa3c9375eaf8bab5c9c1df73 Mon Sep 17 00:00:00 2001 From: Diego Russo Date: Thu, 7 May 2026 16:23:33 +0100 Subject: [PATCH 3/3] Skip GNU backtrace tests on 32-bit Arm GNU backtrace() can return an empty stack on 32-bit Arm when the build lacks suitable runtime unwind tables. Rather than trying to infer the effective unwind-table compiler flags in the test, skip the GNU backtrace tests on all 32-bit Arm builds. --- Lib/test/test_frame_pointer_unwind.py | 62 +-------------------------- 1 file changed, 2 insertions(+), 60 deletions(-) diff --git a/Lib/test/test_frame_pointer_unwind.py b/Lib/test/test_frame_pointer_unwind.py index e33b07e4b8a011..1cf5083fd0fdcf 100644 --- a/Lib/test/test_frame_pointer_unwind.py +++ b/Lib/test/test_frame_pointer_unwind.py @@ -1,7 +1,6 @@ import json import os import platform -import shlex import subprocess import sys import sysconfig @@ -105,63 +104,6 @@ def _is_arm32_build(): return "arm" in abi -def _cflags_have_unwind_tables(cflags): - unwind_tables = False - asynchronous_unwind_tables = False - exceptions = False - - try: - options = shlex.split(cflags) - except ValueError: - options = cflags.split() - - for option in options: - if option == "-funwind-tables": - unwind_tables = True - elif option == "-fno-unwind-tables": - unwind_tables = False - elif option == "-fasynchronous-unwind-tables": - asynchronous_unwind_tables = True - elif option == "-fno-asynchronous-unwind-tables": - asynchronous_unwind_tables = False - elif option == "-fexceptions": - exceptions = True - elif option == "-fno-exceptions": - exceptions = False - - return unwind_tables or asynchronous_unwind_tables or exceptions - - -def _build_has_unwind_tables(): - cflags = [ - value for value in ( - sysconfig.get_config_var("PY_CORE_CFLAGS"), - sysconfig.get_config_var("PY_STDMODULE_CFLAGS"), - ) - if value - ] - if not cflags: - cflags = [ - value for value in ( - sysconfig.get_config_var("PY_CFLAGS"), - sysconfig.get_config_var("CFLAGS"), - ) - if value - ] - - return ( - bool(cflags) - and all(_cflags_have_unwind_tables(value) for value in cflags) - ) - - -def _gnu_backtrace_requires_unwind_tables(): - if not _is_arm32_build(): - return False - - return not _build_has_unwind_tables() - - def _build_stack_and_unwind(unwinder): import operator @@ -369,8 +311,8 @@ def test_manual_unwind_respects_frame_pointers(self): @unittest.skipIf(support.is_wasi, "test not supported on WASI") @unittest.skipUnless(sys.platform == "linux", "GNU backtrace unwinding test requires Linux") @unittest.skipIf( - _gnu_backtrace_requires_unwind_tables(), - "GNU backtrace unwinding on Arm 32-bit requires unwind tables", + _is_arm32_build(), + "GNU backtrace unwinding skipped on Arm 32-bit", ) class GnuBacktraceUnwindTests(unittest.TestCase):