Skip to content

Commit 0193cb6

Browse files
committed
Fix GDB JIT unwind info and harden the tests
The GDB CFI hand-rolled in jit_unwind.c couldn't correctly describe the compiled shim's prologue on AArch64 and relied on hardcoded offsets that would silently invalidate under any compiler/flag change. Tools/jit now compiles shim.c with -fasynchronous-unwind-tables, extracts its .eh_frame at build time, and ships the CIE/FDE CFI bytes as a blob in jit_stencils.h; jit_unwind.c splices those bytes into the synthetic EH frame at runtime, so whatever prologue clang emits is described accurately. Executor regions keep the hand-written steady-state rule, which is our pinned-frame-pointer invariant (enforced by Tools/jit/_optimizers.py _validate()), not a guess at compiler output. Also: "Backtrace stopped: frame did not save the PC" is now a hard AssertionError instead of a silent skip (it always indicates a real unwind bug), and the three JIT tests share a get_stack_trace override that opts out of the generic "?? ()" skip so unrelated libc-without- debug-info frames don't mask a passing test.
1 parent 6ee9fbc commit 0193cb6

File tree

10 files changed

+374
-91
lines changed

10 files changed

+374
-91
lines changed

Include/internal/pycore_jit_unwind.h

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,32 @@
55
# error "this header requires Py_BUILD_CORE define"
66
#endif
77

8-
#if defined(PY_HAVE_PERF_TRAMPOLINE) || (defined(__linux__) && defined(__ELF__))
9-
108
#include <stddef.h>
9+
#include <stdint.h>
10+
11+
/*
12+
* Compiler-emitted CFI for the shim region (GDB path only).
13+
*
14+
* Captured at build time by Tools/jit from the shim's compiled .eh_frame
15+
* so the runtime CIE/FDE can describe whatever prologue the compiler
16+
* chose, without hand-rolling DWARF. Executors pass NULL and fall back
17+
* to the invariant-based steady-state rule that the CIE emits by hand.
18+
*
19+
* The struct is defined unconditionally so jit_record_code() in Python/jit.c
20+
* has a valid pointer type on every platform — callers on non-(Linux+ELF)
21+
* always pass NULL, matching jit_record_code()'s internal #ifdef.
22+
*/
23+
typedef struct {
24+
const uint8_t *cie_init_cfi;
25+
size_t cie_init_cfi_size;
26+
const uint8_t *fde_cfi;
27+
size_t fde_cfi_size;
28+
uint32_t code_align;
29+
int32_t data_align;
30+
uint32_t ra_column;
31+
} _PyJitUnwind_ShimCfi;
32+
33+
#if defined(PY_HAVE_PERF_TRAMPOLINE) || (defined(__linux__) && defined(__ELF__))
1134

1235
/* DWARF exception-handling pointer encodings shared by JIT unwind users. */
1336
enum {
@@ -35,22 +58,30 @@ enum {
3558
};
3659

3760
/* Return the size of the generated .eh_frame data for the given encoding. */
38-
size_t _PyJitUnwind_EhFrameSize(int absolute_addr);
61+
size_t _PyJitUnwind_EhFrameSize(int absolute_addr,
62+
const _PyJitUnwind_ShimCfi *shim_cfi);
3963

4064
/*
4165
* Build DWARF .eh_frame data for JIT code; returns size written or 0 on error.
4266
* absolute_addr selects the FDE address encoding:
4367
* - 0: PC-relative offsets (perf jitdump synthesized DSO).
4468
* - nonzero: absolute addresses (GDB JIT in-memory ELF).
69+
*
70+
* shim_cfi selects which JIT region the CFI describes (GDB path only):
71+
* - NULL: executor trace; steady-state rule in the CIE applies at every PC.
72+
* - non-NULL: compile_shim() output; the captured CIE/FDE CFI bytes are
73+
* spliced in so unwinding is valid at every PC in the shim.
4574
*/
4675
size_t _PyJitUnwind_BuildEhFrame(uint8_t *buffer, size_t buffer_size,
4776
const void *code_addr, size_t code_size,
48-
int absolute_addr);
77+
int absolute_addr,
78+
const _PyJitUnwind_ShimCfi *shim_cfi);
4979

5080
void *_PyJitUnwind_GdbRegisterCode(const void *code_addr,
5181
size_t code_size,
5282
const char *entry,
53-
const char *filename);
83+
const char *filename,
84+
const _PyJitUnwind_ShimCfi *shim_cfi);
5485

5586
void _PyJitUnwind_GdbUnregisterCode(void *handle);
5687

Lib/test/test_gdb/test_jit.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ def setUpModule():
8585
@unittest.skipUnless(hasattr(sys, "_jit") and sys._jit.is_enabled(),
8686
"requires a JIT-enabled build with JIT execution active")
8787
class JitBacktraceTests(DebuggerTests):
88+
def get_stack_trace(self, **kwargs):
89+
# These tests validate the JIT-relevant part of the backtrace via
90+
# _assert_jit_backtrace_shape, so an unrelated "?? ()" frame below
91+
# the JIT/eval segment (e.g. libc without debug info) is tolerable.
92+
kwargs.setdefault("skip_on_truncation", False)
93+
return super().get_stack_trace(**kwargs)
94+
8895
def _extract_backtrace_frames(self, gdb_output):
8996
frames = BACKTRACE_FRAME_RE.findall(gdb_output)
9097
self.assertGreater(
@@ -135,6 +142,20 @@ def _assert_jit_backtrace_shape(self, gdb_output, *, anchor_at_top):
135142
f"expected an eval frame after the JIT frame\n"
136143
f"backtrace:\n{backtrace}",
137144
)
145+
relevant_end = max(
146+
i
147+
for i, frame in enumerate(frames)
148+
if "py::jit_entry:<jit>" in frame or re.search(EVAL_FRAME_RE, frame)
149+
)
150+
truncated_frames = [
151+
frame for frame in frames[: relevant_end + 1]
152+
if " ?? ()" in frame
153+
]
154+
self.assertFalse(
155+
truncated_frames,
156+
"unexpected truncated frame before the validated JIT/eval segment\n"
157+
f"backtrace:\n{backtrace}",
158+
)
138159
if anchor_at_top:
139160
self.assertRegex(
140161
frames[0],

Lib/test/test_gdb/util.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@
2020

2121
PYTHONHASHSEED = '123'
2222

23+
# gh-91960, bpo-40019: gdb reports these when the optimizer has dropped
24+
# python-frame debug info; the test can't read what's not there.
25+
_OPTIMIZED_OUT_PATTERNS = (
26+
'(frame information optimized out)',
27+
'Unable to read information on python frame',
28+
'(unable to read python frame information)',
29+
)
30+
# gdb prints this when the unwinder genuinely failed to walk a frame —
31+
# i.e. the CFI (ours or a library's) is wrong. Treat as a hard failure,
32+
# not a skip, so regressions in our own unwind info don't hide.
33+
_UNWIND_FAILURE_PATTERNS = (
34+
'Backtrace stopped: frame did not save the PC',
35+
)
36+
# gh-104736: " ?? ()" in the bt usually means the unwinder bailed early,
37+
# but can also be unrelated frames without debug info (e.g. libc). Tests
38+
# that validate the JIT-relevant part of the backtrace themselves can
39+
# opt out via skip_on_truncation=False.
40+
_TRUNCATED_BACKTRACE_PATTERNS = (
41+
' ?? ()',
42+
)
43+
2344

2445
def clean_environment():
2546
# Remove PYTHON* environment variables such as PYTHONHOME
@@ -161,6 +182,7 @@ def get_stack_trace(self, source=None, script=None,
161182
cmds_after_breakpoint=None,
162183
import_site=False,
163184
ignore_stderr=False,
185+
skip_on_truncation=True,
164186
**env_vars):
165187
'''
166188
Run 'python -c SOURCE' under gdb with a breakpoint.
@@ -256,26 +278,20 @@ def get_stack_trace(self, source=None, script=None,
256278
" because the Program Counter is"
257279
" not present")
258280

281+
for pattern in _UNWIND_FAILURE_PATTERNS:
282+
if pattern in out:
283+
raise AssertionError(
284+
f"gdb unwinder failed ({pattern!r}) — CFI bug in our "
285+
f"generated code or in a linked library.\n"
286+
f"Full gdb output:\n{out}"
287+
)
288+
259289
# bpo-40019: Skip the test if gdb failed to read debug information
260290
# because the Python binary is optimized.
261-
for pattern in (
262-
'(frame information optimized out)',
263-
'Unable to read information on python frame',
264-
265-
# gh-91960: On Python built with "clang -Og", gdb gets
266-
# "frame=<optimized out>" for _PyEval_EvalFrameDefault() parameter
267-
'(unable to read python frame information)',
268-
269-
# gh-104736: On Python built with "clang -Og" on ppc64le,
270-
# "py-bt" displays a truncated or not traceback, but "where"
271-
# logs this error message:
272-
'Backtrace stopped: frame did not save the PC',
273-
274-
# gh-104736: When "bt" command displays something like:
275-
# "#1 0x0000000000000000 in ?? ()", the traceback is likely
276-
# truncated or wrong.
277-
' ?? ()',
278-
):
291+
patterns = _OPTIMIZED_OUT_PATTERNS
292+
if skip_on_truncation:
293+
patterns = patterns + _TRUNCATED_BACKTRACE_PATTERNS
294+
for pattern in patterns:
279295
if pattern in out:
280296
raise unittest.SkipTest(f"{pattern!r} found in gdb output")
281297

Python/jit.c

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ jit_error(const char *message)
6363

6464
static void *
6565
jit_record_code(const void *code_addr, size_t code_size,
66-
const char *entry, const char *filename)
66+
const char *entry, const char *filename,
67+
const _PyJitUnwind_ShimCfi *shim_cfi)
6768
{
6869
#ifdef PY_HAVE_PERF_TRAMPOLINE
6970
_PyPerf_Callbacks callbacks;
@@ -77,12 +78,13 @@ jit_record_code(const void *code_addr, size_t code_size,
7778

7879
#if defined(__linux__) && defined(__ELF__)
7980
return _PyJitUnwind_GdbRegisterCode(
80-
code_addr, code_size, entry, filename);
81+
code_addr, code_size, entry, filename, shim_cfi);
8182
#else
8283
(void)code_addr;
8384
(void)code_size;
8485
(void)entry;
8586
(void)filename;
87+
(void)shim_cfi;
8688
return NULL;
8789
#endif
8890
}
@@ -762,7 +764,8 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz
762764
executor->jit_gdb_handle = jit_record_code(memory,
763765
code_size + state.trampolines.size,
764766
"jit_entry",
765-
"<jit>");
767+
"<jit>",
768+
/*shim_cfi=*/NULL);
766769
return 0;
767770
}
768771

@@ -813,10 +816,29 @@ compile_shim(void)
813816
return NULL;
814817
}
815818
_Py_jit_shim_size = total_size;
819+
/* GDB JIT unwind info (and the captured-.eh_frame blob that feeds it)
820+
* is only wired up on Linux+ELF — see jit_record_code() below. Even
821+
* there, user-provided JIT CFLAGS can suppress the shim's .eh_frame,
822+
* so jit_stencils.h advertises whether the captured CFI blob exists. */
823+
#if defined(__linux__) && defined(__ELF__) && _Py_JIT_HAS_SHIM_CFI
824+
static const _PyJitUnwind_ShimCfi shim_cfi = {
825+
.cie_init_cfi = _Py_jit_shim_cie_init_cfi,
826+
.cie_init_cfi_size = sizeof(_Py_jit_shim_cie_init_cfi),
827+
.fde_cfi = _Py_jit_shim_fde_cfi,
828+
.fde_cfi_size = sizeof(_Py_jit_shim_fde_cfi),
829+
.code_align = _Py_jit_shim_code_align,
830+
.data_align = _Py_jit_shim_data_align,
831+
.ra_column = _Py_jit_shim_ra_column,
832+
};
833+
const _PyJitUnwind_ShimCfi *shim_cfi_ptr = &shim_cfi;
834+
#else
835+
const _PyJitUnwind_ShimCfi *shim_cfi_ptr = NULL;
836+
#endif
816837
_Py_jit_shim_gdb_handle = jit_record_code(memory,
817838
code_size + state.trampolines.size,
818839
"jit_entry",
819-
"<jit>");
840+
"<jit>",
841+
shim_cfi_ptr);
820842
return (_PyJitEntryFuncPtr)memory;
821843
}
822844

0 commit comments

Comments
 (0)