gh-148286: Track JIT-allocated memory with Valgrind client requests - #154926
Open
matthiasgoergens wants to merge 1 commit into
Open
gh-148286: Track JIT-allocated memory with Valgrind client requests#154926matthiasgoergens wants to merge 1 commit into
matthiasgoergens wants to merge 1 commit into
Conversation
matthiasgoergens
requested review from
brandtbucher,
diegorusso and
savannahostrowski
as code owners
July 30, 2026 13:22
…ests 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 pythongh-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.
matthiasgoergens
force-pushed
the
vgclient-jit-integration
branch
from
July 30, 2026 13:24
72c2f94 to
f3dec60
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Python/jit.c'sjit_alloc()andjit_free()mmapandmunmapthe JIT's machine-code buffers directly. Valgrind's leak checker does not track rawmmap'd memory as a "block", so a forgottenjit_free()— the shape of gh-142476, a real ASan-found leak inallocate_executor— is completely invisible to it.This adds
VALGRIND_MALLOCLIKE_BLOCK/VALGRIND_FREELIKE_BLOCKaround those two functions, guarded by the existing--with-valgrindconfigure option, mirroring the pattern vendored mimalloc already uses inInclude/internal/mimalloc/mimalloc/track.h.To measure the effect I injected a forgotten free into
jit_free()on a machine-code JIT build (--enable-experimental-jit, LLVM 21) and ran a script that creates eight executors and drops them:valgrind --leak-check=full--with-valgrinddefinitely lost: 0 bytes in 0 blocks--with-valgrinddefinitely lost: 139,264 bytes in 10 blocksand in the second case with a usable backtrace:
Background noise was byte-identical between the two runs (120 bytes possibly lost, 646 bytes still reachable), so the injected leak is the only difference. The injection is not part of this patch.
The include is
valgrind/valgrind.h— the headerconfigurealready probes for under--with-valgrind, and the one that defines both macros used here, so no build-system change is needed.Objects/obmalloc.cincludes the same header.On
VALGRIND_DISCARD_TRANSLATIONSI deliberately did not add any. It seemed like the obvious thing for a JIT, but it turns out to be unnecessary here.
Valgrind's
munmapandmprotectsyscall wrappers already discard cached translations for any range that previously held translated code, and that logic lives in architecture-independent code (coregrind/m_syswrap/syswrap-generic.c). I also checked it experimentally with a small driver that emits code, executes it enough times to guarantee a cached translation, changes it, and executes again — with a control case that genuinely does go stale so the harness is known to detect the failure. CPython'smmap→ write once →mprotect→ execute →munmap-whole-region pattern never executed a stale translation on either x86-64 (Valgrind 3.25.1) or aarch64 (Valgrind 3.19.0), in any--smc-checkmode includingnone.A side effect worth recording: since that is already true,
--smc-check=noneis safe for CPython today with no code change. On a tight JIT-compiled integer loop that is worth about 10% over theall-non-filedefault (median of three runs each: 11.33 s vs 10.17 s, ranges 11.22-11.45 and 10.12-10.36, so non-overlapping). That workload is close to a best case — it is almost entirely JIT-executed — so a realistic program should expect less.Testing
Builds cleanly both with and without
--with-valgrind;test_capiandtest_optimizerpass (1,501 tests) on the--with-valgrindbuild, and undervalgrindmemcheck.