Skip to content

_remote_debugging retains stale TLBC arrays in some cases #157660

Description

@liuzhijie-0614

Bug description

I found stale TLBC arrays in some cases, which may result in incorrect LocationInfo.

The main issue is that tlbc_generation is incremented when thread TLBC indices are allocated or released, but when a TLBC array is modified, the profiler cannot detect the change. This may cause some problems:

  1. If the TLBC array grows, a new tlbc_index may keep triggering this error:
PyErr_Format(PyExc_RuntimeError,
    "Invalid tlbc_index %d (array size %zd, corrupted remote memory)",
    tlbc_index, tlbc_entry->tlbc_array_size);
  1. When a slot in the TLBC array changes from NULL to tlbc_bytecode_addr, the profiler cannot read the updated slot from its cache. A similar issue occurs when a slot is cleared.
# Linux: ./python -X gil=0 -X tlbc=1 repro_tlbc_bounds.py
import os, threading, time
from _remote_debugging import RemoteUnwinder

go = threading.Event()
stop = threading.Event()

def leaf():
    stop.wait()

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()

time.sleep(0.1)
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
u.get_stack_trace()
go.set()
time.sleep(0.1)
u.get_stack_trace()  # RuntimeError: Invalid tlbc_index 18 (array size 16, ...)
import os, threading, time
from _remote_debugging import RemoteUnwinder

go = threading.Event()
stop = threading.Event()

def leaf():
    stop.wait()

def lines(u):
    return sorted(f.location.lineno for i in u.get_stack_trace()
                  for t in i.threads for f in t.frame_info
                  if f.funcname == "leaf")

threading.Thread(target=leaf, daemon=True).start()
threading.Thread(target=lambda: (go.wait(), leaf()), daemon=True).start()
time.sleep(0.1)
u = RemoteUnwinder(os.getpid(), all_threads=True, cache_frames=False)
print("Before:", lines(u))
go.set()
time.sleep(0.1)
print("Cached:", lines(u))
print("Fresh: ", lines(RemoteUnwinder(
    os.getpid(), all_threads=True, cache_frames=False)))

Before: [8]
Cached: [-1, 8]
Fresh:  [8, 8]

CPython versions tested on

3.15

Operating systems tested on

Linux

Linked PRs

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions