Skip to content

Commit 0dfe40c

Browse files
committed
gh-143228: Fix UAF in perf trampoline during finalization
When toggling perf trampoline while threads are running, or during interpreter finalization with daemon threads active, a use-after-free occurs. The munmap call in free_code_arenas releases executable memory while other threads may still be executing within trampolines or unwinding through them, causing SIGSEGV or SystemError. The fix uses reference counting with a code watcher. Each code object that receives a trampoline increments a refcount. When code objects are destroyed, the watcher decrements the refcount and frees arenas only when it reaches zero. This ensures trampolines are never freed while any code object could still reference them.
1 parent 61ee048 commit 0dfe40c

File tree

4 files changed

+44
-10
lines changed

4 files changed

+44
-10
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

Python/perf_trampoline.c

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,31 @@ 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 int
213+
perf_trampoline_code_watcher(PyCodeEvent event, PyCodeObject *co)
214+
{
215+
if (event != PY_CODE_EVENT_DESTROY) {
216+
return 0;
217+
}
218+
if (extra_code_index == -1) {
219+
return 0;
220+
}
221+
py_trampoline f = NULL;
222+
int ret = _PyCode_GetExtra((PyObject *)co, extra_code_index, (void **)&f);
223+
if (ret != 0 || f == NULL) {
224+
return 0;
225+
}
226+
trampoline_refcount--;
227+
if (trampoline_refcount == 0) {
228+
free_code_arenas();
229+
}
230+
return 0;
231+
}
207232

208233
static void
209234
perf_map_write_entry(void *state, const void *code_addr,
@@ -407,6 +432,7 @@ py_trampoline_evaluator(PyThreadState *ts, _PyInterpreterFrame *frame,
407432
perf_code_arena->code_size, co);
408433
_PyCode_SetExtra((PyObject *)co, extra_code_index,
409434
(void *)new_trampoline);
435+
trampoline_refcount++;
410436
f = new_trampoline;
411437
}
412438
assert(f != NULL);
@@ -433,6 +459,7 @@ int PyUnstable_PerfTrampoline_CompileCode(PyCodeObject *co)
433459
}
434460
trampoline_api.write_state(trampoline_api.state, new_trampoline,
435461
perf_code_arena->code_size, co);
462+
trampoline_refcount++;
436463
return _PyCode_SetExtra((PyObject *)co, extra_code_index,
437464
(void *)new_trampoline);
438465
}
@@ -487,6 +514,10 @@ _PyPerfTrampoline_Init(int activate)
487514
{
488515
#ifdef PY_HAVE_PERF_TRAMPOLINE
489516
PyThreadState *tstate = _PyThreadState_GET();
517+
if (code_watcher_id == 0) {
518+
// Initialize to -1 since 0 is a valid watcher ID
519+
code_watcher_id = -1;
520+
}
490521
if (!activate) {
491522
_PyInterpreterState_SetEvalFrameFunc(tstate->interp, prev_eval_frame);
492523
perf_status = PERF_STATUS_NO_INIT;
@@ -504,6 +535,12 @@ _PyPerfTrampoline_Init(int activate)
504535
if (new_code_arena() < 0) {
505536
return -1;
506537
}
538+
code_watcher_id = PyCode_AddWatcher(perf_trampoline_code_watcher);
539+
if (code_watcher_id < 0) {
540+
PyErr_FormatUnraisable("Failed to register code watcher for perf trampoline");
541+
free_code_arenas();
542+
return -1;
543+
}
507544
perf_status = PERF_STATUS_OK;
508545
}
509546
#endif
@@ -525,19 +562,16 @@ _PyPerfTrampoline_Fini(void)
525562
trampoline_api.free_state(trampoline_api.state);
526563
perf_trampoline_type = PERF_TRAMPOLINE_UNSET;
527564
}
565+
if (code_watcher_id >= 0) {
566+
PyCode_ClearWatcher(code_watcher_id);
567+
code_watcher_id = -1;
568+
}
528569
extra_code_index = -1;
529570
perf_status = PERF_STATUS_NO_INIT;
530571
#endif
531572
return 0;
532573
}
533574

534-
void _PyPerfTrampoline_FreeArenas(void) {
535-
#ifdef PY_HAVE_PERF_TRAMPOLINE
536-
free_code_arenas();
537-
#endif
538-
return;
539-
}
540-
541575
int
542576
PyUnstable_PerfTrampoline_SetPersistAfterFork(int enable){
543577
#ifdef PY_HAVE_PERF_TRAMPOLINE

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)