Skip to content

gh-157660: Fix stale TLBC caches in _remote_debugging - #157732

Merged
pablogsal merged 2 commits into
python:mainfrom
liuzhijie-0614:codex/gh-157660-refresh-tlbc-cache
Sep 27, 2026
Merged

pablogsal merged 2 commits into
python:mainfrom
liuzhijie-0614:codex/gh-157660-refresh-tlbc-cache

Conversation

@liuzhijie-0614

@liuzhijie-0614 liuzhijie-0614 commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #157660.

In free-threaded builds, TLBC arrays can grow or gain entries without changing tlbc_generation, leaving _remote_debugging with stale cached data. This can cause persistent Invalid tlbc_index errors or incorrect line numbers.

Refresh the cached array when a frame’s index exceeds its capacity or points to a cached NULL slot, while retaining the existing bounds checks.

Adds regression tests based on both reproducers in the issue.

@python-cla-bot

python-cla-bot Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

All commit authors signed the Contributor License Agreement.

CLA signed

@liuzhijie-0614

Copy link
Copy Markdown
Contributor Author

This is ready for review. The TLBC regression test passes locally, including repeated runs with frame caching enabled and disabled. The two failing CI jobs appear unrelated to this change, as noted above.

@liuzhijie-0614
liuzhijie-0614 marked this pull request as draft September 21, 2026 09:09

@pablogsal pablogsal left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I think the approach is the right one: the frame's own tlbc_index is enough evidence that the cached array is stale, so refreshing lazily and keeping the bounds check is fine.

But this only fixes the first case in the issue. The second reproducer (slot going from NULL to a pointer with no growth) still gives lineno -1 with this branch. Same mechanism covers it with one more condition, see inline. This also needs a backport to 3.14.

Not from this PR, but _Py_hashtable_set(self->tlbc_cache, key, NULL) in get_tlbc_cache_entry asserts the key is absent in debug builds and leaks the old entry. It is unreachable today because a generation change clears the table, but since you are here, maybe use the same _Py_hashtable_steal + destroy pattern you added?

tlbc_entry = get_tlbc_cache_entry(unwinder, real_address, unwinder->tlbc_generation);
}

if (tlbc_entry && ctx->tlbc_index >= tlbc_entry->tlbc_array_size) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to refresh also when the cached slot at a nonzero index is NULL. A live frame with tlbc_index != 0 always has a populated slot (_PyFrame_InitializeTLBC sets the index to 0 when there is no copy yet), so a NULL here means the cache predates the slot being filled. Otherwise we fall back to the main bytecode with an instr_ptr from the TLBC copy and report garbage line numbers.

Something like (hoisting entries above this check):

if (tlbc_entry && ctx->tlbc_index >= 0 &&
    (ctx->tlbc_index >= tlbc_entry->tlbc_array_size ||
     entries[ctx->tlbc_index] == 0)) {

I checked locally and this makes both reproducers from the issue pass.

int
cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr, uintptr_t tlbc_array_addr, uint32_t generation)
cache_tlbc_array(RemoteUnwinderObject *unwinder, uintptr_t code_addr,
uintptr_t tlbc_array_addr, uint32_t generation, bool force_refresh)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need force_refresh. The page cache is cleared at the end of every get_stack_trace, so across samples paged reads are already fresh, and within a sample the initial load has the same window. Let's keep the old signature.

Comment thread Lib/test/test_external_inspection.py Outdated
sys.platform == "linux" and not PROCESS_VM_READV_SUPPORTED,
"Requires process_vm_readv",
)
def test_tlbc_cache_refresh_after_growth(self):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This covers growth only. Can you add a test for the slot-fill case as well? Two workers is enough, it is the second script in the issue and it fails on this branch today.

Small nit: this runs both cache_frames modes, so TestFrameCaching is a bit of an odd home for it.

@@ -0,0 +1,2 @@
Fix persistent sampling errors in :mod:`!_remote_debugging` when a code

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the NULL slot case is in, this should mention incorrect line numbers too, and better to name the user-facing module:

Fix :mod:`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.

@liuzhijie-0614 liuzhijie-0614 changed the title gh-157660: Refresh cached TLBC arrays after growth gh-157660: Refresh cached TLBC arrays after growth or gains entries Sep 21, 2026
@liuzhijie-0614
liuzhijie-0614 force-pushed the codex/gh-157660-refresh-tlbc-cache branch from 99b225b to 69a4191 Compare September 21, 2026 14:46
@liuzhijie-0614 liuzhijie-0614 changed the title gh-157660: Refresh cached TLBC arrays after growth or gains entries gh-157660: Fix stale TLBC caches in _remote_debugging Sep 21, 2026
@liuzhijie-0614
liuzhijie-0614 force-pushed the codex/gh-157660-refresh-tlbc-cache branch from 69a4191 to ad535c6 Compare September 21, 2026 14:50
@liuzhijie-0614
liuzhijie-0614 marked this pull request as ready for review September 21, 2026 14:56
@liuzhijie-0614
liuzhijie-0614 force-pushed the codex/gh-157660-refresh-tlbc-cache branch 2 times, most recently from 4da4976 to f9611ac Compare September 21, 2026 15:05
@liuzhijie-0614

Copy link
Copy Markdown
Contributor Author

@pablogsal Thank you very much for your guidance! It has resolved my confusion.

I completed the code modification based on the suggestions. Please review it again :)

Comment thread Lib/test/test_external_inspection.py Outdated
Comment thread Lib/test/test_external_inspection.py Outdated
Comment thread Modules/_remote_debugging/_remote_debugging.h Outdated
@pablogsal

Copy link
Copy Markdown
Member

Thanks! The cache changes address my earlier comments. Can we replace the sleeps in the tests with worker synchronization? The Android and iOS failures are unrelated download failures. We also still need to arrange the 3.14 backport.

`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.
@bedevere-app

bedevere-app Bot commented Sep 23, 2026

Copy link
Copy Markdown

GH-158007 is a backport of this pull request to the 3.14 branch.

@liuzhijie-0614

liuzhijie-0614 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks! The cache changes address my earlier comments. Can we replace the sleeps in the tests with worker synchronization? The Android and iOS failures are unrelated download failures. We also still need to arrange the 3.14 backport.

Thanks for your feedback! I’ve replaced time.sleep with sleeping_retry to wait until the expected leaf frames appear. I’ve also backported the fix to 3.14. Is a backport to 3.15 still needed?

3.14 backport PR:#158007

@liuzhijie-0614

Copy link
Copy Markdown
Contributor Author

@pablogsal I investigated the Windows ARM64 free-threading CI failure and reproduced the same error locally. It looks related to #157605.

After go.set(), the second thread wakes up and enters Condition._acquire_restore(). The sampler can read current_frame while it points to that frame, but by the time it reads the frame contents, the function has returned and f_executable has been cleared. Frame parsing then produces no Python frame, triggering RuntimeError: Failed to parse initial frame in chain.

@liuzhijie-0614
liuzhijie-0614 force-pushed the codex/gh-157660-refresh-tlbc-cache branch from 039483a to a7e7c06 Compare September 27, 2026 04:51
@bedevere-app

bedevere-app Bot commented Sep 27, 2026

Copy link
Copy Markdown

GH-158302 is a backport of this pull request to the 3.15 branch.

@pablogsal pablogsal left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@pablogsal
pablogsal enabled auto-merge (squash) September 27, 2026 19:47
@pablogsal

Copy link
Copy Markdown
Member

Great work on the TLBC cache fix, @liuzhijie-0614! Thanks a lot! ❤️

pablogsal added a commit that referenced this pull request Sep 27, 2026
…) (#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>
@pablogsal
pablogsal merged commit ca5277c into python:main Sep 27, 2026
59 checks passed
pablogsal added a commit that referenced this pull request Sep 27, 2026
…) (#158302)

Co-authored-by: liuzhijie-0614 <75884011+liuzhijie-0614@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

_remote_debugging retains stale TLBC arrays in some cases

2 participants