From ae6d6b94585b3fb86246a5a8a9c44e3f0954a6d3 Mon Sep 17 00:00:00 2001 From: Shamil Abdulaev Date: Sat, 10 Oct 2026 15:35:00 +0300 Subject: [PATCH] fix(jit): avoid crashes with UBSan --- Include/internal/pycore_jit.h | 8 ++++++++ Lib/test/test_sys.py | 15 +++++++++++++++ ...26-10-10-15-10-00.gh-issue-158961.jitubsan.rst | 3 +++ Python/ceval_macros.h | 2 +- Python/jit.c | 15 ++++++++------- 5 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-15-10-00.gh-issue-158961.jitubsan.rst diff --git a/Include/internal/pycore_jit.h b/Include/internal/pycore_jit.h index 8a88cbf607ba4bc..dfbfc9a4cfa75f0 100644 --- a/Include/internal/pycore_jit.h +++ b/Include/internal/pycore_jit.h @@ -17,6 +17,14 @@ extern "C" { typedef _Py_CODEUNIT *(*jit_func)(_PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate); +// JIT code has no function type metadata for UBSan to check. +static inline _Py_CODEUNIT* _Py_NO_SANITIZE_UNDEFINED +_PyJIT_Entry(jit_func jitted, _PyInterpreterFrame *frame, + _PyStackRef *stack_pointer, PyThreadState *tstate) +{ + return jitted(frame, stack_pointer, tstate); +} + int _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction *trace, size_t length); void _PyJIT_Free(_PyExecutorObject *executor); diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py index 0eb348e2292fc16..8389c8630459cd8 100644 --- a/Lib/test/test_sys.py +++ b/Lib/test/test_sys.py @@ -2423,6 +2423,21 @@ def test_jit_is_enabled(self): assert_python_ok("-c", script.format(enabled=False), PYTHON_JIT="0") assert_python_ok("-c", script.format(enabled=available), PYTHON_JIT="1") + @unittest.skipUnless(sys._jit.is_available(), "requires JIT") + def test_jit_compile_regex(self): + script = textwrap.dedent(r""" + import re + + for _ in range(2): + re.purge() + pattern = re.compile(r"[\x00-\uffff]") + assert pattern.fullmatch("\x00") is not None + assert pattern.fullmatch("\uffff") is not None + assert pattern.fullmatch("\U00010000") is None + """) + _, _, err = assert_python_ok("-c", script, PYTHON_JIT="1") + self.assertEqual(err, b"") + def test_jit_is_active(self): available = sys._jit.is_available() script = textwrap.dedent( diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-15-10-00.gh-issue-158961.jitubsan.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-15-10-00.gh-issue-158961.jitubsan.rst new file mode 100644 index 000000000000000..9224fae14fc0841 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-15-10-00.gh-issue-158961.jitubsan.rst @@ -0,0 +1,3 @@ +Fix crashes when entering JIT-compiled code with UndefinedBehaviorSanitizer +and unaligned memory accesses when patching JIT code. +Patch by Shamil Abdulaev. diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h index c75c0b2410c6771..49063bb8f334b33 100644 --- a/Python/ceval_macros.h +++ b/Python/ceval_macros.h @@ -392,7 +392,7 @@ do { \ jit_func jitted = _executor->jit_code; \ /* Keep the shim frame alive via the executor: */ \ Py_INCREF(_executor); \ - next_instr = jitted(frame, stack_pointer, tstate); \ + next_instr = _PyJIT_Entry(jitted, frame, stack_pointer, tstate); \ Py_DECREF(_executor); \ frame = tstate->current_frame; \ stack_pointer = _PyFrame_GetStackPointer(frame); \ diff --git a/Python/jit.c b/Python/jit.c index 35ff9a663cd140d..1a25d019bc8ba3e 100644 --- a/Python/jit.c +++ b/Python/jit.c @@ -202,30 +202,29 @@ set_bits(uint32_t *loc, uint8_t loc_start, uint64_t value, uint8_t value_start, void patch_32(unsigned char *location, uint64_t value) { - uint32_t *loc32 = (uint32_t *)location; // Check that we're not out of range of 32 unsigned bits: assert(value < (1ULL << 32)); - *loc32 = (uint32_t)value; + uint32_t final_value = (uint32_t)value; + memcpy(location, &final_value, sizeof(final_value)); } // 32-bit relative address. void patch_32r(unsigned char *location, uint64_t value) { - uint32_t *loc32 = (uint32_t *)location; value -= (uintptr_t)location; // Check that we're not out of range of 32 signed bits: assert((int64_t)value >= -(1LL << 31)); assert((int64_t)value < (1LL << 31)); - *loc32 = (uint32_t)value; + uint32_t final_value = (uint32_t)value; + memcpy(location, &final_value, sizeof(final_value)); } // 64-bit absolute address. void patch_64(unsigned char *location, uint64_t value) { - uint64_t *loc64 = (uint64_t *)location; - *loc64 = value; + memcpy(location, &value, sizeof(value)); } // 12-bit low part of an absolute address. Pairs nicely with patch_aarch64_21r @@ -329,7 +328,9 @@ patch_x86_64_32rx(unsigned char *location, uint64_t value) { uint8_t *loc8 = (uint8_t *)location; // Try to relax the GOT load into an immediate value: - uint64_t relaxed = *(uint64_t *)(value + 4) - 4; + uint64_t relaxed; + memcpy(&relaxed, (void *)(value + 4), sizeof(relaxed)); + relaxed -= 4; if ((int64_t)relaxed - (int64_t)location >= -(1LL << 31) && (int64_t)relaxed - (int64_t)location + 1 < (1LL << 31)) {