https://github.com/python/cpython/commit/0a905eb8f121c9aef6e15e6f260678d0bd66a184
commit: 0a905eb8f121c9aef6e15e6f260678d0bd66a184
branch: 3.14
author: liuzhijie-0614 <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-09-27T20:14:43Z
summary:

[3.14] gh-157660: Fix stale TLBC caches in _remote_debugging (GH-157732) 
(#158007)

* 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.

* gh-157660: Retry transient sampling races in TLBC tests

---------

Co-authored-by: Pablo Galindo Salgado <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-09-18-12-00-00.gh-issue-157660.tlbc.rst
M Lib/test/test_external_inspection.py
M Modules/_remote_debugging_module.c

diff --git a/Lib/test/test_external_inspection.py 
b/Lib/test/test_external_inspection.py
index eea796d74a7974..766dfce64872bc 100644
--- a/Lib/test/test_external_inspection.py
+++ b/Lib/test/test_external_inspection.py
@@ -1317,6 +1317,125 @@ 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",
+                ):
+                    try:
+                        traces = u.get_stack_trace()
+                    except RuntimeError as exc:
+                        if str(exc) != "Failed to parse initial frame in 
chain":
+                            raise
+                        continue
+                    count = sum(
+                        f.funcname == "leaf"
+                        for t in traces 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",
+                ):
+                    try:
+                        traces = u.get_stack_trace()
+                    except RuntimeError as exc:
+                        if str(exc) != "Failed to parse initial frame in 
chain":
+                            raise
+                        continue
+                    result = sorted(
+                        f.lineno
+                        for t in traces for f in t.frame_info
+                        if f.funcname == "leaf"
+                    )
+                    # A new frame can still point at the function definition.
+                    if (len(result) == expected_count and
+                        leaf.__code__.co_firstlineno not in result):
+                        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,

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to