Skip to content

Commit 5fecd44

Browse files
maurycypablogsal
andauthored
gh-155811: Add a seqcount to gc_stats to prevent torn reads (#155828)
* update_seq * no need for XCHGL, MOVL is enough? * gh-155811: Retry an inconsistent GC snapshot once --------- Co-authored-by: Pablo Galindo Salgado <Pablogsal@gmail.com>
1 parent b93fb19 commit 5fecd44

5 files changed

Lines changed: 55 additions & 8 deletions

File tree

‎Include/internal/pycore_interp_structs.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ struct gc_old_stats_buffer {
219219
struct gc_stats {
220220
struct gc_young_stats_buffer young;
221221
struct gc_old_stats_buffer old[2];
222+
uint32_t update_seq;
222223
};
223224

224225
struct _gc_runtime_state {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add a sequence counter to GC statistics to prevent :mod:`!_remote_debugging`
2+
returning inconsistent snapshots caused by non-atomic reads. Patch by Maurycy
3+
Pawłowski-Wieroński.

‎Modules/_remote_debugging/gc_stats.c‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,42 @@ get_gc_stats_from_interpreter_state(RuntimeOffsets *offsets,
103103
}
104104

105105
struct gc_stats stats;
106-
if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle,
107-
gc_stats_addr,
108-
sizeof(stats),
109-
&stats) < 0) {
110-
set_exception_cause(offsets, PyExc_RuntimeError, "Failed to read GC state");
111-
return -1;
106+
uintptr_t sequence_address = gc_stats_addr
107+
+ offsetof(struct gc_stats, update_seq);
108+
/* A short GC update may finish before a second attempt. */
109+
for (int attempt = 0; attempt < 2; attempt++) {
110+
uint32_t before;
111+
if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle,
112+
sequence_address,
113+
sizeof(before), &before) < 0) {
114+
set_exception_cause(offsets, PyExc_RuntimeError,
115+
"Failed to read GC update sequence");
116+
return -1;
117+
}
118+
if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle,
119+
gc_stats_addr,
120+
sizeof(stats),
121+
&stats) < 0) {
122+
set_exception_cause(offsets, PyExc_RuntimeError, "Failed to read GC state");
123+
return -1;
124+
}
125+
126+
uint32_t after;
127+
if (_Py_RemoteDebug_ReadRemoteMemory(&offsets->handle,
128+
sequence_address,
129+
sizeof(after), &after) < 0) {
130+
set_exception_cause(offsets, PyExc_RuntimeError,
131+
"Failed to read GC update sequence");
132+
return -1;
133+
}
134+
if (before == after && before == stats.update_seq && !(after & 1)) {
135+
break;
136+
}
137+
if (attempt == 1) {
138+
PyErr_SetString(PyExc_RuntimeError,
139+
"GC stats changed while being read; retry later");
140+
return -1;
141+
}
112142
}
113143

114144
if (read_gc_stats(&stats, iid, ctx->result,

‎Python/gc.c‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,13 @@ gc_get_prev_stats(GCState *gcstate, int gen)
13991399
static void
14001400
add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats)
14011401
{
1402+
struct gc_stats *generation_stats = gcstate->generation_stats;
1403+
uint32_t seq = _Py_atomic_load_uint32_relaxed(&generation_stats->update_seq);
1404+
assert((seq & 1) == 0);
1405+
/* Odd seq tells the reader that an update is in progress. */
1406+
_Py_atomic_store_uint32_relaxed(&generation_stats->update_seq, seq + 1);
1407+
_Py_atomic_fence_seq_cst();
1408+
14021409
struct gc_generation_stats *prev_stats = gc_get_prev_stats(gcstate, gen);
14031410
struct gc_generation_stats *cur_stats = gc_get_stats(gcstate, gen);
14041411

@@ -1412,9 +1419,8 @@ add_stats(GCState *gcstate, int gen, struct gc_generation_stats *stats)
14121419

14131420
cur_stats->duration += stats->duration;
14141421
cur_stats->heap_size = stats->heap_size;
1415-
/* Publish ts_stop last so remote readers do not select a partially
1416-
updated stats record as the latest collection. */
14171422
cur_stats->ts_stop = stats->ts_stop;
1423+
_Py_atomic_store_uint32_release(&generation_stats->update_seq, seq + 2);
14181424
}
14191425

14201426
/* This is the main function. Read this to understand how the

‎Python/gc_free_threading.c‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,6 +2287,12 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
22872287

22882288
/* Update stats. */
22892289
PyMutex_Lock(&gcstate->stats_mutex);
2290+
struct gc_stats *generation_stats = gcstate->generation_stats;
2291+
uint32_t seq = _Py_atomic_load_uint32_relaxed(&generation_stats->update_seq);
2292+
assert((seq & 1) == 0);
2293+
/* Odd seq tells the reader that an update is in progress. */
2294+
_Py_atomic_store_uint32_relaxed(&generation_stats->update_seq, seq + 1);
2295+
_Py_atomic_fence_seq_cst();
22902296
struct gc_generation_stats *stats = get_stats(gcstate, generation);
22912297
stats->ts_start = start;
22922298
stats->ts_stop = stop;
@@ -2295,6 +2301,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
22952301
stats->uncollectable += n;
22962302
stats->duration += duration;
22972303
stats->candidates += state.candidates;
2304+
_Py_atomic_store_uint32_release(&generation_stats->update_seq, seq + 2);
22982305
PyMutex_Unlock(&gcstate->stats_mutex);
22992306

23002307
GC_STAT_ADD(generation, objects_collected, m);

0 commit comments

Comments
 (0)