From f3dec60a7777cb8089e4bb78c1813cf3dca38dab Mon Sep 17 00:00:00 2001 From: Matthias Goergens Date: Thu, 30 Jul 2026 17:05:41 +0800 Subject: [PATCH] gh-148286: Track JIT-allocated memory with Valgrind client requests Python/jit.c's jit_alloc()/jit_free() mmap and munmap the JIT's machine-code buffers directly. Valgrind's leak checker does not track raw mmap'd memory as a "block", so a forgotten jit_free() -- the shape of gh-142476, a real ASan-found leak in allocate_executor -- is completely invisible to it. Measured on a machine-code JIT build (--enable-experimental-jit, LLVM 21) with a forgotten-free deliberately injected into jit_free() and eight executors created and dropped: without --with-valgrind: definitely lost: 0 bytes in 0 blocks with --with-valgrind: definitely lost: 139,264 bytes in 10 blocks and in the second case with a backtrace through jit_alloc -> _PyJIT_Compile -> make_executor_from_uops -> stop_tracing_and_jit. Background noise was byte-identical between the two runs (120 bytes possibly lost, 646 still reachable), so the only difference is the injected JIT leak. The injection was reverted before committing. Guarded by the existing --with-valgrind configure option, and includes valgrind/valgrind.h -- the header that configure already probes for and that defines both macros used here -- mirroring Objects/obmalloc.c. No VALGRIND_DISCARD_TRANSLATIONS is added. Valgrind's munmap and mprotect syscall wrappers already discard cached translations for any range that held translated code, in architecture-independent code (coregrind/m_syswrap/syswrap-generic.c). Confirmed experimentally on both x86-64 and aarch64: with a control that does go stale, CPython's mmap/write-once/mprotect/munmap pattern never executes a stale translation, even under --smc-check=none. --- ...26-07-30-18-30-00.gh-issue-148286.Qv8Rk3.rst | 5 +++++ Python/jit.c | 17 +++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-18-30-00.gh-issue-148286.Qv8Rk3.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-18-30-00.gh-issue-148286.Qv8Rk3.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-18-30-00.gh-issue-148286.Qv8Rk3.rst new file mode 100644 index 000000000000000..487a5ee912877d3 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-30-18-30-00.gh-issue-148286.Qv8Rk3.rst @@ -0,0 +1,5 @@ +Track JIT-allocated machine-code buffers with Valgrind client requests, so +that memcheck's leak checker can see them. ``jit_alloc()`` and ``jit_free()`` +use ``mmap`` and ``munmap`` directly, which Valgrind does not track as +allocations, making a forgotten free invisible. Guarded by the existing +:option:`--with-valgrind` configure option. diff --git a/Python/jit.c b/Python/jit.c index 50e606a7a0963f6..d63080104e74a5a 100644 --- a/Python/jit.c +++ b/Python/jit.c @@ -39,6 +39,13 @@ #include #endif +#ifdef WITH_VALGRIND + // VALGRIND_MALLOCLIKE_BLOCK and VALGRIND_FREELIKE_BLOCK live in + // valgrind.h, which is the header configure already probes for + // --with-valgrind (see also Objects/obmalloc.c). + #include +#endif + static size_t get_page_size(void) { @@ -139,6 +146,13 @@ jit_alloc(size_t size) jit_error("unable to allocate memory"); return NULL; } +#ifdef WITH_VALGRIND + // Let memcheck track this JIT code region like a heap allocation, so its + // leak checker and addressability/definedness tracking apply to it. The + // whole region is always freed in one jit_free() call (never partially), + // matching the "one MALLOCLIKE_BLOCK per FREELIKE_BLOCK" contract. + VALGRIND_MALLOCLIKE_BLOCK(memory, size, /*redzone=*/0, /*is_zeroed=*/0); +#endif return memory; } @@ -147,6 +161,9 @@ jit_free(unsigned char *memory, size_t size) { assert(size); assert(size % get_page_size() == 0); +#ifdef WITH_VALGRIND + VALGRIND_FREELIKE_BLOCK(memory, /*redzone=*/0); +#endif #ifdef MS_WINDOWS int failed = !VirtualFree(memory, 0, MEM_RELEASE); #else