Skip to content

Commit a7e7c06

Browse files
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.
1 parent e2ff498 commit a7e7c06

3 files changed

Lines changed: 131 additions & 3 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2235,6 +2235,113 @@ def get_trace_with_opcodes(pid):
22352235
location.col_offset, location.end_col_offset)
22362236
self.assertIn(actual, valid_locations)
22372237

2238+
@skip_if_not_supported
2239+
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
2240+
@unittest.skipIf(
2241+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2242+
"Requires process_vm_readv",
2243+
)
2244+
def test_tlbc_cache_refresh_after_growth(self):
2245+
# Reproducer from gh-157660.
2246+
script = textwrap.dedent("""\
2247+
import os, threading
2248+
from _remote_debugging import RemoteUnwinder
2249+
from test import support
2250+
2251+
go = threading.Event()
2252+
stop = threading.Event()
2253+
2254+
def leaf():
2255+
stop.wait()
2256+
2257+
def wait_for_leaf_frames(u, expected_count):
2258+
for _ in support.sleeping_retry(
2259+
support.SHORT_TIMEOUT,
2260+
f"Expected {expected_count} leaf frames",
2261+
):
2262+
count = sum(
2263+
f.funcname == "leaf"
2264+
for i in u.get_stack_trace()
2265+
for t in i.threads for f in t.frame_info
2266+
)
2267+
if count == expected_count:
2268+
return
2269+
2270+
threading.Thread(target=leaf, daemon=True).start()
2271+
for _ in range(16):
2272+
threading.Thread(target=stop.wait, daemon=True).start()
2273+
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
2274+
2275+
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
2276+
wait_for_leaf_frames(u, 1)
2277+
go.set()
2278+
wait_for_leaf_frames(u, 2)
2279+
""")
2280+
result = subprocess.run(
2281+
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
2282+
capture_output=True,
2283+
text=True,
2284+
timeout=SHORT_TIMEOUT,
2285+
)
2286+
self.assertEqual(
2287+
result.returncode, 0,
2288+
f"stdout: {result.stdout}\nstderr: {result.stderr}",
2289+
)
2290+
2291+
@skip_if_not_supported
2292+
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
2293+
@unittest.skipIf(
2294+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2295+
"Requires process_vm_readv",
2296+
)
2297+
def test_tlbc_cache_refresh_after_slot_fill(self):
2298+
# Reproducer from gh-157660.
2299+
script = textwrap.dedent("""\
2300+
import os, threading
2301+
from _remote_debugging import RemoteUnwinder
2302+
2303+
go = threading.Event()
2304+
stop = threading.Event()
2305+
2306+
def leaf():
2307+
stop.wait()
2308+
2309+
from test import support
2310+
2311+
def lines(u, expected_count):
2312+
for _ in support.sleeping_retry(
2313+
support.SHORT_TIMEOUT,
2314+
f"Expected {expected_count} leaf frames",
2315+
):
2316+
result = sorted(
2317+
f.location.lineno
2318+
for i in u.get_stack_trace()
2319+
for t in i.threads for f in t.frame_info
2320+
if f.funcname == "leaf"
2321+
)
2322+
if len(result) == expected_count:
2323+
return result
2324+
2325+
threading.Thread(target=leaf, daemon=True).start()
2326+
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
2327+
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
2328+
before = lines(u, 1)
2329+
assert before == [8], before
2330+
go.set()
2331+
cached = lines(u, 2)
2332+
assert cached == [8, 8], cached
2333+
""")
2334+
result = subprocess.run(
2335+
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
2336+
capture_output=True,
2337+
text=True,
2338+
timeout=SHORT_TIMEOUT,
2339+
)
2340+
self.assertEqual(
2341+
result.returncode, 0,
2342+
f"stdout: {result.stdout}\nstderr: {result.stderr}",
2343+
)
2344+
22382345

22392346
class TestUnsupportedPlatformHandling(unittest.TestCase):
22402347
@unittest.skipIf(
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`profiling.sampling` reporting errors or incorrect line numbers in free-threaded
2+
builds when a thread-local bytecode array grows or gains entries after being cached.

‎Modules/_remote_debugging/code_objects.c‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,12 @@ get_tlbc_cache_entry(RemoteUnwinderObject *self, uintptr_t code_addr, uint32_t c
3232
TLBCCacheEntry *entry = _Py_hashtable_get(self->tlbc_cache, key);
3333

3434
if (entry && entry->generation != current_generation) {
35-
// Entry is stale, remove it by setting to NULL
36-
_Py_hashtable_set(self->tlbc_cache, key, NULL);
37-
entry = NULL;
35+
// Entry is stale, remove it from the cache and destroy it
36+
TLBCCacheEntry *old = _Py_hashtable_steal(self->tlbc_cache, key);
37+
if (old != NULL) {
38+
tlbc_cache_entry_destroy(old);
39+
}
40+
return NULL;
3841
}
3942

4043
return entry;
@@ -449,6 +452,22 @@ parse_code_object(RemoteUnwinderObject *unwinder,
449452
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
450453
}
451454

455+
if (tlbc_entry && ctx->tlbc_index >= 0) {
456+
uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + sizeof(Py_ssize_t));
457+
if (ctx->tlbc_index >= tlbc_entry->tlbc_array_size ||
458+
entries[ctx->tlbc_index] == 0) {
459+
TLBCCacheEntry *old = _Py_hashtable_steal(unwinder->tlbc_cache, (void *)real_address);
460+
if (old != NULL) {
461+
tlbc_cache_entry_destroy(old);
462+
}
463+
if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc,
464+
unwinder->tlbc_generation)) {
465+
goto error;
466+
}
467+
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
468+
}
469+
}
470+
452471
// Validate tlbc_index and check TLBC cache
453472
if (tlbc_entry) {
454473
// Validate index bounds (also catches negative values since tlbc_index is signed)

0 commit comments

Comments
 (0)