Skip to content

Commit 5985fcb

Browse files
[3.15] gh-157660: Fix stale TLBC caches in _remote_debugging (GH-157732) (#158302)
Co-authored-by: liuzhijie-0614 <75884011+liuzhijie-0614@users.noreply.github.com>
1 parent cde2cd5 commit 5985fcb

3 files changed

Lines changed: 145 additions & 3 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,6 +2253,127 @@ def get_trace_with_opcodes(pid):
22532253
location.col_offset, location.end_col_offset)
22542254
self.assertIn(actual, valid_locations)
22552255

2256+
@skip_if_not_supported
2257+
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
2258+
@unittest.skipIf(
2259+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2260+
"Requires process_vm_readv",
2261+
)
2262+
def test_tlbc_cache_refresh_after_growth(self):
2263+
# Reproducer from gh-157660.
2264+
script = textwrap.dedent("""\
2265+
import os, threading
2266+
from _remote_debugging import RemoteUnwinder
2267+
from test import support
2268+
2269+
go = threading.Event()
2270+
stop = threading.Event()
2271+
2272+
def leaf():
2273+
stop.wait()
2274+
2275+
def wait_for_leaf_frames(u, expected_count):
2276+
for _ in support.sleeping_retry(
2277+
support.SHORT_TIMEOUT,
2278+
f"Expected {expected_count} leaf frames",
2279+
):
2280+
try:
2281+
traces = u.get_stack_trace()
2282+
except RuntimeError as exc:
2283+
if str(exc) != "Failed to parse initial frame in chain":
2284+
raise
2285+
continue
2286+
count = sum(
2287+
f.funcname == "leaf"
2288+
for i in traces
2289+
for t in i.threads for f in t.frame_info
2290+
)
2291+
if count == expected_count:
2292+
return
2293+
2294+
threading.Thread(target=leaf, daemon=True).start()
2295+
for _ in range(16):
2296+
threading.Thread(target=stop.wait, daemon=True).start()
2297+
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
2298+
2299+
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
2300+
wait_for_leaf_frames(u, 1)
2301+
go.set()
2302+
wait_for_leaf_frames(u, 2)
2303+
""")
2304+
result = subprocess.run(
2305+
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
2306+
capture_output=True,
2307+
text=True,
2308+
timeout=SHORT_TIMEOUT,
2309+
)
2310+
self.assertEqual(
2311+
result.returncode, 0,
2312+
f"stdout: {result.stdout}\nstderr: {result.stderr}",
2313+
)
2314+
2315+
@skip_if_not_supported
2316+
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
2317+
@unittest.skipIf(
2318+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
2319+
"Requires process_vm_readv",
2320+
)
2321+
def test_tlbc_cache_refresh_after_slot_fill(self):
2322+
# Reproducer from gh-157660.
2323+
script = textwrap.dedent("""\
2324+
import os, threading
2325+
from _remote_debugging import RemoteUnwinder
2326+
2327+
go = threading.Event()
2328+
stop = threading.Event()
2329+
2330+
def leaf():
2331+
stop.wait()
2332+
2333+
from test import support
2334+
2335+
def lines(u, expected_count):
2336+
for _ in support.sleeping_retry(
2337+
support.SHORT_TIMEOUT,
2338+
f"Expected {expected_count} leaf frames",
2339+
):
2340+
try:
2341+
traces = u.get_stack_trace()
2342+
except RuntimeError as exc:
2343+
if str(exc) != "Failed to parse initial frame in chain":
2344+
raise
2345+
continue
2346+
result = sorted(
2347+
f.location.lineno
2348+
for i in traces
2349+
for t in i.threads for f in t.frame_info
2350+
if f.funcname == "leaf"
2351+
)
2352+
# A new frame can still point at the function definition.
2353+
if (len(result) == expected_count and
2354+
leaf.__code__.co_firstlineno not in result):
2355+
return result
2356+
2357+
threading.Thread(target=leaf, daemon=True).start()
2358+
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
2359+
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
2360+
before = lines(u, 1)
2361+
assert before == [8], before
2362+
go.set()
2363+
cached = lines(u, 2)
2364+
assert cached == [8, 8], cached
2365+
""")
2366+
result = subprocess.run(
2367+
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
2368+
capture_output=True,
2369+
text=True,
2370+
timeout=SHORT_TIMEOUT,
2371+
)
2372+
self.assertEqual(
2373+
result.returncode, 0,
2374+
f"stdout: {result.stdout}\nstderr: {result.stderr}",
2375+
)
2376+
22562377

22572378
class TestUnsupportedPlatformHandling(unittest.TestCase):
22582379
@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)