@@ -147,50 +147,119 @@ frame_cache_invalidate_stale(RemoteUnwinderObject *unwinder, PyObject *result)
147147 Py_CLEAR (unwinder -> frame_cache [i ].frame_list );
148148 unwinder -> frame_cache [i ].thread_id = 0 ;
149149 unwinder -> frame_cache [i ].thread_state_addr = 0 ;
150+ unwinder -> frame_cache [i ].last_profiled_frame_seq = 0 ;
150151 unwinder -> frame_cache [i ].num_addrs = 0 ;
151152 STATS_INC (unwinder , stale_cache_invalidations );
152153 }
153154 }
154155}
155156
157+ static int
158+ read_last_profiled_anchor (RemoteUnwinderObject * unwinder ,
159+ uintptr_t thread_state_addr ,
160+ FrameCacheAnchor * anchor )
161+ {
162+ uintptr_t frame_offset = (uintptr_t )unwinder -> debug_offsets .thread_state .last_profiled_frame ;
163+ uintptr_t seq_offset = (uintptr_t )unwinder -> debug_offsets .thread_state .last_profiled_frame_seq ;
164+
165+ // These fields are adjacent in PyThreadState. Read them together when the
166+ // layout allows it so validation uses a pointer and sequence from the same
167+ // remote-memory read.
168+ if (seq_offset == frame_offset + sizeof (uintptr_t )) {
169+ uintptr_t live_anchor [2 ];
170+ if (_Py_RemoteDebug_ReadRemoteMemory (& unwinder -> handle ,
171+ thread_state_addr + frame_offset ,
172+ sizeof (live_anchor ),
173+ live_anchor ) < 0 ) {
174+ return -1 ;
175+ }
176+ anchor -> frame = live_anchor [0 ];
177+ anchor -> seq = live_anchor [1 ];
178+ return 0 ;
179+ }
180+
181+ if (read_ptr (unwinder , thread_state_addr + frame_offset , & anchor -> frame ) < 0 ) {
182+ return -1 ;
183+ }
184+ return read_ptr (unwinder , thread_state_addr + seq_offset , & anchor -> seq );
185+ }
186+
187+ static Py_ssize_t
188+ find_cached_frame_addr (const FrameCacheEntry * entry , uintptr_t frame_addr ,
189+ uintptr_t * real_pops )
190+ {
191+ * real_pops = 0 ;
192+ for (Py_ssize_t i = 0 ; i < entry -> num_addrs ; i ++ ) {
193+ if (entry -> addrs [i ] == frame_addr ) {
194+ return i ;
195+ }
196+ if (entry -> addrs [i ] != 0 ) {
197+ (* real_pops )++ ;
198+ }
199+ }
200+ return -1 ;
201+ }
202+
203+ int
204+ frame_cache_anchor_matches (
205+ RemoteUnwinderObject * unwinder ,
206+ uintptr_t thread_state_addr ,
207+ FrameCacheAnchor anchor )
208+ {
209+ FrameCacheAnchor live_anchor = {0 , 0 };
210+ if (read_last_profiled_anchor (unwinder , thread_state_addr , & live_anchor ) < 0 ) {
211+ PyErr_Clear ();
212+ return 0 ;
213+ }
214+ return live_anchor .frame == anchor .frame && live_anchor .seq == anchor .seq ;
215+ }
216+
156217// Find last_profiled_frame in cache and extend frame_info with cached continuation
157218// If frame_addrs is provided (not NULL), also extends it with cached addresses
158219int
159220frame_cache_lookup_and_extend (
160221 RemoteUnwinderObject * unwinder ,
161222 uint64_t thread_id ,
162- uintptr_t last_profiled_frame ,
223+ uintptr_t thread_state_addr ,
224+ FrameCacheAnchor anchor ,
163225 PyObject * frame_info ,
164226 uintptr_t * frame_addrs ,
165227 Py_ssize_t * num_addrs ,
166228 Py_ssize_t max_addrs )
167229{
168- if (!unwinder -> frame_cache || last_profiled_frame == 0 ) {
230+ if (!unwinder -> frame_cache || anchor . frame == 0 ) {
169231 return 0 ;
170232 }
171233
172234 FrameCacheEntry * entry = frame_cache_find (unwinder , thread_id );
173235 if (!entry || !entry -> frame_list ) {
174236 return 0 ;
175237 }
238+ if (entry -> thread_state_addr != thread_state_addr ) {
239+ return 0 ;
240+ }
176241
177242 assert (entry -> num_addrs >= 0 && entry -> num_addrs <= FRAME_CACHE_MAX_FRAMES );
178243
179- // Find the index where last_profiled_frame matches
180- Py_ssize_t start_idx = -1 ;
181- for (Py_ssize_t i = 0 ; i < entry -> num_addrs ; i ++ ) {
182- if (entry -> addrs [i ] == last_profiled_frame ) {
183- start_idx = i ;
184- break ;
185- }
186- }
187-
244+ uintptr_t real_pops = 0 ;
245+ Py_ssize_t start_idx = find_cached_frame_addr (entry , anchor .frame , & real_pops );
188246 if (start_idx < 0 ) {
189247 return 0 ; // Not found
190248 }
191249 assert (start_idx < entry -> num_addrs );
192250
251+ // Synthetic marker frames (<native>/<GC>) are stored as addr-0 entries but
252+ // never increment last_profiled_frame_seq in the target (only real frame
253+ // pops do). Count the real frames before start_idx so the sequence check is
254+ // not thrown off by markers sitting between the leaf and the anchor.
255+ if (entry -> last_profiled_frame_seq + real_pops != anchor .seq ) {
256+ return 0 ;
257+ }
258+
193259 Py_ssize_t num_frames = PyList_GET_SIZE (entry -> frame_list );
260+ if (start_idx >= num_frames ) {
261+ return 0 ;
262+ }
194263
195264 // Extend frame_info with frames ABOVE start_idx (not including it).
196265 // The frame at start_idx (last_profiled_frame) was the executing frame
@@ -200,6 +269,9 @@ frame_cache_lookup_and_extend(
200269 if (cache_start >= num_frames ) {
201270 return 0 ; // Nothing above last_profiled_frame to extend with
202271 }
272+ if (!frame_cache_anchor_matches (unwinder , thread_state_addr , anchor )) {
273+ return 0 ;
274+ }
203275
204276 PyObject * slice = PyList_GetSlice (entry -> frame_list , cache_start , num_frames );
205277 if (!slice ) {
@@ -235,6 +307,7 @@ frame_cache_store(
235307 const uintptr_t * addrs ,
236308 Py_ssize_t num_addrs ,
237309 uintptr_t thread_state_addr ,
310+ uintptr_t last_profiled_frame_seq ,
238311 uintptr_t base_frame_addr ,
239312 uintptr_t last_frame_visited )
240313{
@@ -277,6 +350,7 @@ frame_cache_store(
277350 }
278351 entry -> thread_id = thread_id ;
279352 entry -> thread_state_addr = thread_state_addr ;
353+ entry -> last_profiled_frame_seq = last_profiled_frame_seq ;
280354 if (entry -> thread_id_obj == NULL ) {
281355 entry -> thread_id_obj = PyLong_FromUnsignedLongLong (thread_id );
282356 if (entry -> thread_id_obj == NULL ) {
0 commit comments