Skip to content

Commit 0a905eb

Browse files
[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 <Pablogsal@gmail.com>
1 parent e4e5835 commit 0a905eb

3 files changed

Lines changed: 143 additions & 3 deletions

File tree

‎Lib/test/test_external_inspection.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,125 @@ def main_work():
13171317
"GIL holder should be among all threads",
13181318
)
13191319

1320+
@skip_if_not_supported
1321+
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
1322+
@unittest.skipIf(
1323+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1324+
"Requires process_vm_readv",
1325+
)
1326+
def test_tlbc_cache_refresh_after_growth(self):
1327+
# Reproducer from gh-157660.
1328+
script = textwrap.dedent("""\
1329+
import os, threading
1330+
from _remote_debugging import RemoteUnwinder
1331+
from test import support
1332+
1333+
go = threading.Event()
1334+
stop = threading.Event()
1335+
1336+
def leaf():
1337+
stop.wait()
1338+
1339+
def wait_for_leaf_frames(u, expected_count):
1340+
for _ in support.sleeping_retry(
1341+
support.SHORT_TIMEOUT,
1342+
f"Expected {expected_count} leaf frames",
1343+
):
1344+
try:
1345+
traces = u.get_stack_trace()
1346+
except RuntimeError as exc:
1347+
if str(exc) != "Failed to parse initial frame in chain":
1348+
raise
1349+
continue
1350+
count = sum(
1351+
f.funcname == "leaf"
1352+
for t in traces for f in t.frame_info
1353+
)
1354+
if count == expected_count:
1355+
return
1356+
1357+
threading.Thread(target=leaf, daemon=True).start()
1358+
for _ in range(16):
1359+
threading.Thread(target=stop.wait, daemon=True).start()
1360+
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
1361+
1362+
u = RemoteUnwinder(os.getpid(), all_threads=True)
1363+
wait_for_leaf_frames(u, 1)
1364+
go.set()
1365+
wait_for_leaf_frames(u, 2)
1366+
""")
1367+
result = subprocess.run(
1368+
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
1369+
capture_output=True,
1370+
text=True,
1371+
timeout=SHORT_TIMEOUT,
1372+
)
1373+
self.assertEqual(
1374+
result.returncode, 0,
1375+
f"stdout: {result.stdout}\nstderr: {result.stderr}",
1376+
)
1377+
1378+
@skip_if_not_supported
1379+
@unittest.skipIf(sys._is_gil_enabled(), "Requires free-threading")
1380+
@unittest.skipIf(
1381+
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
1382+
"Requires process_vm_readv",
1383+
)
1384+
def test_tlbc_cache_refresh_after_slot_fill(self):
1385+
# Reproducer from gh-157660.
1386+
script = textwrap.dedent("""\
1387+
import os, threading
1388+
from _remote_debugging import RemoteUnwinder
1389+
1390+
go = threading.Event()
1391+
stop = threading.Event()
1392+
1393+
def leaf():
1394+
stop.wait()
1395+
1396+
from test import support
1397+
1398+
def lines(u, expected_count):
1399+
for _ in support.sleeping_retry(
1400+
support.SHORT_TIMEOUT,
1401+
f"Expected {expected_count} leaf frames",
1402+
):
1403+
try:
1404+
traces = u.get_stack_trace()
1405+
except RuntimeError as exc:
1406+
if str(exc) != "Failed to parse initial frame in chain":
1407+
raise
1408+
continue
1409+
result = sorted(
1410+
f.lineno
1411+
for t in traces for f in t.frame_info
1412+
if f.funcname == "leaf"
1413+
)
1414+
# A new frame can still point at the function definition.
1415+
if (len(result) == expected_count and
1416+
leaf.__code__.co_firstlineno not in result):
1417+
return result
1418+
1419+
threading.Thread(target=leaf, daemon=True).start()
1420+
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
1421+
u = RemoteUnwinder(os.getpid(), all_threads=True)
1422+
before = lines(u, 1)
1423+
assert before == [8], before
1424+
go.set()
1425+
cached = lines(u, 2)
1426+
assert cached == [8, 8], cached
1427+
""")
1428+
result = subprocess.run(
1429+
[sys.executable, "-X", "gil=0", "-X", "tlbc=1", "-c", script],
1430+
capture_output=True,
1431+
text=True,
1432+
timeout=SHORT_TIMEOUT,
1433+
)
1434+
self.assertEqual(
1435+
result.returncode, 0,
1436+
f"stdout: {result.stdout}\nstderr: {result.stderr}",
1437+
)
1438+
13201439

13211440
if __name__ == "__main__":
13221441
unittest.main()
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix ``_remote_debugging`` 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_module.c‎

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

20882088
if (entry && entry->generation != current_generation) {
2089-
// Entry is stale, remove it by setting to NULL
2090-
_Py_hashtable_set(self->tlbc_cache, key, NULL);
2091-
entry = NULL;
2089+
// Entry is stale, remove it from the cache and destroy it
2090+
TLBCCacheEntry *old = _Py_hashtable_steal(self->tlbc_cache, key);
2091+
if (old != NULL) {
2092+
tlbc_cache_entry_destroy(old);
2093+
}
2094+
return NULL;
20922095
}
20932096

20942097
return entry;
@@ -2361,6 +2364,22 @@ parse_code_object(RemoteUnwinderObject *unwinder,
23612364
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
23622365
}
23632366

2367+
if (tlbc_entry && tlbc_index >= 0) {
2368+
uintptr_t *entries = (uintptr_t *)((char *)tlbc_entry->tlbc_array + sizeof(Py_ssize_t));
2369+
if (tlbc_index >= tlbc_entry->tlbc_array_size ||
2370+
entries[tlbc_index] == 0) {
2371+
TLBCCacheEntry *old = _Py_hashtable_steal(unwinder->tlbc_cache, (void *)real_address);
2372+
if (old != NULL) {
2373+
tlbc_cache_entry_destroy(old);
2374+
}
2375+
if (!cache_tlbc_array(unwinder, real_address, real_address + unwinder->debug_offsets.code_object.co_tlbc,
2376+
unwinder->tlbc_generation)) {
2377+
goto error;
2378+
}
2379+
tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
2380+
}
2381+
}
2382+
23642383
if (tlbc_entry) {
23652384
if (tlbc_index < 0 || tlbc_index >= tlbc_entry->tlbc_array_size) {
23662385
PyErr_Format(PyExc_RuntimeError,

0 commit comments

Comments
 (0)