Skip to content

Commit c609173

Browse files
committed
pythongh-124785: alternative immortal string cleanup fix
The pythongh-124646 fix has the issue that it breaks the trace-refs debug build. This is a simpler fix that avoids the use-after-free crashes by just leaking immortal interned strings allocated by legacy subinterpreters.
1 parent 082aae0 commit c609173

File tree

3 files changed

+50
-0
lines changed

3 files changed

+50
-0
lines changed

Include/internal/pycore_interp.h

+4
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ struct _is {
123123
/* Has been fully initialized via pylifecycle.c. */
124124
int _ready;
125125
int finalizing;
126+
/* Set if a subinterpreter imports basic single-phase extensions and
127+
* therefore needs to leak interned strings (it's too tricky to safely
128+
* free them). */
129+
int leak_interned_strings;
126130

127131
uintptr_t last_restart_version;
128132
struct pythreads {

Objects/unicodeobject.c

+41
Original file line numberDiff line numberDiff line change
@@ -15296,6 +15296,42 @@ PyUnicode_InternFromString(const char *cp)
1529615296
return s;
1529715297
}
1529815298

15299+
void
15300+
clear_interned_dict_legacy(PyInterpreterState *interp)
15301+
{
15302+
// See GH-116510 for why this extra care is needed. Immortal interned
15303+
// strings can be shared between subinterpreters in this case and we
15304+
// can't safely free them without a potential use-after-free crash.
15305+
PyObject *interned = get_interned_dict(interp);
15306+
Py_ssize_t pos = 0;
15307+
PyObject *s, *ignored_value;
15308+
while (PyDict_Next(interned, &pos, &s, &ignored_value)) {
15309+
assert(PyUnicode_IS_READY(s));
15310+
switch (PyUnicode_CHECK_INTERNED(s)) {
15311+
case SSTATE_INTERNED_IMMORTAL:
15312+
case SSTATE_INTERNED_IMMORTAL_STATIC:
15313+
// immortal strings leak since we can't safely free them
15314+
break;
15315+
case SSTATE_INTERNED_MORTAL:
15316+
// Restore 2 references held by the interned dict; these will
15317+
// be decref'd by clear_interned_dict's PyDict_Clear.
15318+
Py_SET_REFCNT(s, Py_REFCNT(s) + 2);
15319+
#ifdef Py_REF_DEBUG
15320+
/* let's be pedantic with the ref total */
15321+
_Py_IncRefTotal(_PyThreadState_GET());
15322+
_Py_IncRefTotal(_PyThreadState_GET());
15323+
#endif
15324+
break;
15325+
case SSTATE_NOT_INTERNED:
15326+
/* fall through */
15327+
default:
15328+
Py_UNREACHABLE();
15329+
}
15330+
_PyUnicode_STATE(s).interned = SSTATE_NOT_INTERNED;
15331+
}
15332+
PyDict_Clear(interned);
15333+
_Py_INTERP_CACHED_OBJECT(interp, interned_strings) = NULL;
15334+
}
1529915335

1530015336
void
1530115337
_PyUnicode_ClearInterned(PyInterpreterState *interp)
@@ -15306,6 +15342,11 @@ _PyUnicode_ClearInterned(PyInterpreterState *interp)
1530615342
}
1530715343
assert(PyDict_CheckExact(interned));
1530815344

15345+
if (interp->leak_interned_strings) {
15346+
clear_interned_dict_legacy(interp);
15347+
return;
15348+
}
15349+
1530915350
#ifdef INTERNED_STATS
1531015351
fprintf(stderr, "releasing %zd interned strings\n",
1531115352
PyDict_GET_SIZE(interned));

Python/import.c

+5
Original file line numberDiff line numberDiff line change
@@ -1897,6 +1897,11 @@ import_find_extension(PyThreadState *tstate,
18971897
return NULL;
18981898
}
18991899

1900+
/* Interned strings used by this module can be shared between
1901+
* subinterpreters, due to the PyDict_Update() call in basic single-phase
1902+
* module import case. */
1903+
tstate->interp->leak_interned_strings = 1;
1904+
19001905
PyObject *mod = reload_singlephase_extension(tstate, cached, info);
19011906
if (mod == NULL) {
19021907
return NULL;

0 commit comments

Comments
 (0)