Skip to content

Commit 3ccc76f

Browse files
authored
gh-143228: Fix UAF in perf trampoline during finalization (#143233)
1 parent 836b281 commit 3ccc76f

File tree

5 files changed

+66
-11
lines changed

5 files changed

+66
-11
lines changed

Include/internal/pycore_ceval.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ extern int _PyPerfTrampoline_SetCallbacks(_PyPerf_Callbacks *);
103103
extern void _PyPerfTrampoline_GetCallbacks(_PyPerf_Callbacks *);
104104
extern int _PyPerfTrampoline_Init(int activate);
105105
extern int _PyPerfTrampoline_Fini(void);
106-
extern void _PyPerfTrampoline_FreeArenas(void);
107106
extern int _PyIsPerfTrampolineActive(void);
108107
extern PyStatus _PyPerfTrampoline_AfterFork_Child(void);
109108
#ifdef PY_HAVE_PERF_TRAMPOLINE

Include/internal/pycore_interp_structs.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ struct _ceval_runtime_state {
8787
struct trampoline_api_st trampoline_api;
8888
FILE *map_file;
8989
Py_ssize_t persist_after_fork;
90-
_PyFrameEvalFunction prev_eval_frame;
90+
_PyFrameEvalFunction prev_eval_frame;
91+
Py_ssize_t trampoline_refcount;
92+
int code_watcher_id;
9193
#else
9294
int _not_used;
9395
#endif
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix use-after-free in perf trampoline when toggling profiling while
2+
threads are running or during interpreter finalization with daemon threads
3+
active. The fix uses reference counting to ensure trampolines are not freed
4+
while any code object could still reference them. Pach by Pablo Galindo

Python/perf_trampoline.c

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,42 @@ enum perf_trampoline_type {
204204
#define persist_after_fork _PyRuntime.ceval.perf.persist_after_fork
205205
#define perf_trampoline_type _PyRuntime.ceval.perf.perf_trampoline_type
206206
#define prev_eval_frame _PyRuntime.ceval.perf.prev_eval_frame
207+
#define trampoline_refcount _PyRuntime.ceval.perf.trampoline_refcount
208+
#define code_watcher_id _PyRuntime.ceval.perf.code_watcher_id
209+
210+
static void free_code_arenas(void);
211+
212+
static void
213+
perf_trampoline_reset_state(void)
214+
{
215+
free_code_arenas();
216+
if (code_watcher_id >= 0) {
217+
PyCode_ClearWatcher(code_watcher_id);
218+
code_watcher_id = -1;
219+
}
220+
extra_code_index = -1;
221+
}
222+
223+
static int
224+
perf_trampoline_code_watcher(PyCodeEvent event, PyCodeObject *co)
225+
{
226+
if (event != PY_CODE_EVENT_DESTROY) {
227+
return 0;
228+
}
229+
if (extra_code_index == -1) {
230+
return 0;
231+
}
232+
py_trampoline f = NULL;
233+
int ret = _PyCode_GetExtra((PyObject *)co, extra_code_index, (void **)&f);
234+
if (ret != 0 || f == NULL) {
235+
return 0;
236+
}
237+
trampoline_refcount--;
238+
if (trampoline_refcount == 0) {
239+
perf_trampoline_reset_state();
240+
}
241+
return 0;
242+
}
207243

208244
static void
209245
perf_map_write_entry(void *state, const void *code_addr,
@@ -407,6 +443,7 @@ py_trampoline_evaluator(PyThreadState *ts, _PyInterpreterFrame *frame,
407443
perf_code_arena->code_size, co);
408444
_PyCode_SetExtra((PyObject *)co, extra_code_index,
409445
(void *)new_trampoline);
446+
trampoline_refcount++;
410447
f = new_trampoline;
411448
}
412449
assert(f != NULL);
@@ -433,6 +470,7 @@ int PyUnstable_PerfTrampoline_CompileCode(PyCodeObject *co)
433470
}
434471
trampoline_api.write_state(trampoline_api.state, new_trampoline,
435472
perf_code_arena->code_size, co);
473+
trampoline_refcount++;
436474
return _PyCode_SetExtra((PyObject *)co, extra_code_index,
437475
(void *)new_trampoline);
438476
}
@@ -487,6 +525,10 @@ _PyPerfTrampoline_Init(int activate)
487525
{
488526
#ifdef PY_HAVE_PERF_TRAMPOLINE
489527
PyThreadState *tstate = _PyThreadState_GET();
528+
if (code_watcher_id == 0) {
529+
// Initialize to -1 since 0 is a valid watcher ID
530+
code_watcher_id = -1;
531+
}
490532
if (!activate) {
491533
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame);
492534
perf_status = PERF_STATUS_NO_INIT;
@@ -504,6 +546,13 @@ _PyPerfTrampoline_Init(int activate)
504546
if (new_code_arena() < 0) {
505547
return -1;
506548
}
549+
code_watcher_id = PyCode_AddWatcher(perf_trampoline_code_watcher);
550+
if (code_watcher_id < 0) {
551+
PyErr_FormatUnraisable("Failed to register code watcher for perf trampoline");
552+
free_code_arenas();
553+
return -1;
554+
}
555+
trampoline_refcount = 1; // Base refcount held by the system
507556
perf_status = PERF_STATUS_OK;
508557
}
509558
#endif
@@ -525,17 +574,19 @@ _PyPerfTrampoline_Fini(void)
525574
trampoline_api.free_state(trampoline_api.state);
526575
perf_trampoline_type = PERF_TRAMPOLINE_UNSET;
527576
}
528-
extra_code_index = -1;
577+
578+
// Prevent new trampolines from being created
529579
perf_status = PERF_STATUS_NO_INIT;
530-
#endif
531-
return 0;
532-
}
533580

534-
void _PyPerfTrampoline_FreeArenas(void) {
535-
#ifdef PY_HAVE_PERF_TRAMPOLINE
536-
free_code_arenas();
581+
// Decrement base refcount. If refcount reaches 0, all code objects are already
582+
// dead so clean up now. Otherwise, watcher remains active to clean up when last
583+
// code object dies; extra_code_index stays valid so watcher can identify them.
584+
trampoline_refcount--;
585+
if (trampoline_refcount == 0) {
586+
perf_trampoline_reset_state();
587+
}
537588
#endif
538-
return;
589+
return 0;
539590
}
540591

541592
int

Python/pylifecycle.c

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1944,7 +1944,6 @@ finalize_interp_clear(PyThreadState *tstate)
19441944
_PyArg_Fini();
19451945
_Py_ClearFileSystemEncoding();
19461946
_PyPerfTrampoline_Fini();
1947-
_PyPerfTrampoline_FreeArenas();
19481947
}
19491948

19501949
finalize_interp_types(tstate->interp);

0 commit comments

Comments
 (0)