From ac01f07a32e42fe65837de21d7c50c2a3c1a5194 Mon Sep 17 00:00:00 2001 From: liuzhijie-0614 <75884011+liuzhijie-0614@users.noreply.github.com> Date: Fri, 18 Sep 2026 11:59:04 +0800 Subject: [PATCH] gh-157660: Fix stale TLBC caches in _remote_debugging `profiling.sampling` reporting errors or incorrect line numbers in free-threaded builds when a thread-local bytecode array grows or gains entries after being cached. --- Lib/test/test_external_inspection.py | 105 ++++++++++++++++++ ...26-09-18-12-00-00.gh-issue-157660.tlbc.rst | 2 + Modules/_remote_debugging_module.c | 25 ++++- 3 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-18-12-00-00.gh-issue-157660.tlbc.rst diff --git a/Lib/test/test_external_inspection.py b/Lib/test/test_external_inspection.py index eea796d74a7974..a4be3f5d0be1a9 100644 --- a/Lib/test/test_external_inspection.py +++ b/Lib/test/test_external_inspection.py @@ -1317,6 +1317,111 @@ def main_work(): "GIL holder should be among all threads", ) + @skip_if_not_supported + @unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Requires process_vm_readv", + ) + def test_tlbc_cache_refresh_after_growth(self): + # Reproducer from gh-157660. + script = textwrap.dedent("""\ + import os, threading + from _remote_debugging import RemoteUnwinder + from test import support + + go = threading.Event() + stop = threading.Event() + + def leaf(): + stop.wait() + + def wait_for_leaf_frames(u, expected_count): + for _ in support.sleeping_retry( + support.SHORT_TIMEOUT, + f"Expected {expected_count} leaf frames", + ): + count = sum( + f.funcname == "leaf" + for t in u.get_stack_trace() for f in t.frame_info + ) + if count == expected_count: + return + + threading.Thread(target=leaf, daemon=True).start() + for _ in range(16): + threading.Thread(target=stop.wait, daemon=True).start() + threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start() + + u = RemoteUnwinder(os.getpid(), all_threads=True) + wait_for_leaf_frames(u, 1) + go.set() + wait_for_leaf_frames(u, 2) + """) + result = subprocess.run( + [sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + result.returncode, 0, + f"stdout: {result.stdout}\nstderr: {result.stderr}", + ) + + @skip_if_not_supported + @unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading") + @unittest.skipIf( + sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED, + "Requires process_vm_readv", + ) + def test_tlbc_cache_refresh_after_slot_fill(self): + # Reproducer from gh-157660. + script = textwrap.dedent("""\ + import os, threading + from _remote_debugging import RemoteUnwinder + + go = threading.Event() + stop = threading.Event() + + def leaf(): + stop.wait() + + from test import support + + def lines(u, expected_count): + for _ in support.sleeping_retry( + support.SHORT_TIMEOUT, + f"Expected {expected_count} leaf frames", + ): + result = sorted( + f.lineno + for t in u.get_stack_trace() for f in t.frame_info + if f.funcname == "leaf" + ) + if len(result) == expected_count: + return result + + threading.Thread(target=leaf, daemon=True).start() + threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start() + u = RemoteUnwinder(os.getpid(), all_threads=True) + before = lines(u, 1) + assert before == [8], before + go.set() + cached = lines(u, 2) + assert cached == [8, 8], cached + """) + result = subprocess.run( + [sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script], + capture_output=True, + text=True, + timeout=SHORT_TIMEOUT, + ) + self.assertEqual( + result.returncode, 0, + f"stdout: {result.stdout}\nstderr: {result.stderr}", + ) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-09-18-12-00-00.gh-issue-157660.tlbc.rst b/Misc/NEWS.d/next/Library/2026-09-18-12-00-00.gh-issue-157660.tlbc.rst new file mode 100644 index 00000000000000..846c5fdc8ebd5d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-18-12-00-00.gh-issue-157660.tlbc.rst @@ -0,0 +1,2 @@ +Fix ``_remote_debugging`` reporting errors or incorrect line numbers in free-threaded +builds when a thread-local bytecode array grows or gains entries after being cached. diff --git a/Modules/_remote_debugging_module.c b/Modules/_remote_debugging_module.c index b51ed4dcb7951c..3c0841c2a307ed 100644 --- a/Modules/_remote_debugging_module.c +++ b/Modules/_remote_debugging_module.c @@ -2086,9 +2086,12 @@ get_tlbc_cache_entry(RemoteUnwinderObject *self, uintptr_t code_addr, uint32_t c TLBCCacheEntry *entry = _Py_hashtable_get(self->tlbc_cache, key); if (entry && entry->generation != current_generation) { - // Entry is stale, remove it by setting to NULL - _Py_hashtable_set(self->tlbc_cache, key, NULL); - entry = NULL; + // Entry is stale, remove it from the cache and destroy it + TLBCCacheEntry *old = _Py_hashtable_steal(self->tlbc_cache, key); + if (old != NULL) { + tlbc_cache_entry_destroy(old); + } + return NULL; } return entry; @@ -2361,6 +2364,22 @@ parse_code_object(RemoteUnwinderObject *unwinder, tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation); } + if (tlbc_entry && tlbc_index >= 0) { + uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + sizeof(Py_ssize_t)); + if (tlbc_index >= tlbc_entry->tlbc_array_size || + entries[tlbc_index] == 0) { + TLBCCacheEntry *old = _Py_hashtable_steal(unwinder->tlbc_cache, (void *)real_address); + if (old != NULL) { + tlbc_cache_entry_destroy(old); + } + if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc, + unwinder->tlbc_generation)) { + goto error; + } + tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation); + } + } + if (tlbc_entry) { if (tlbc_index < 0 || tlbc_index >= tlbc_entry->tlbc_array_size) { PyErr_Format(PyExc_RuntimeError,