Skip to content

Commit 8186500

Browse files
gh-119213: Be More Careful About _PyArg_Parser.kwtuple Across Interpreters (gh-119331)
_PyArg_Parser holds static global data generated for modules by Argument Clinic. The _PyArg_Parser.kwtuple field is a tuple object, even though it's stored within a static global. In some cases the tuple is statically allocated and thus it's okay that it gets shared by multiple interpreters. However, in other cases the tuple is set lazily, allocated from the heap using the active interprepreter at the point the tuple is needed. This is a problem once that interpreter is destroyed since _PyArg_Parser.kwtuple becomes at dangling pointer, leading to crashes. It isn't a problem if the tuple is allocated under the main interpreter, since its lifetime is bound to the lifetime of the runtime. The solution here is to temporarily switch to the main interpreter. The alternative would be to always statically allocate the tuple. This change also fixes a bug where only the most recent parser was added to the global linked list.
1 parent d472b4f commit 8186500

File tree

10 files changed

+144
-3
lines changed

10 files changed

+144
-3
lines changed

Include/internal/pycore_global_objects_fini_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_global_strings.h

+1
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,7 @@ struct _Py_global_strings {
711711
STRUCT_FOR_ID(sort)
712712
STRUCT_FOR_ID(source)
713713
STRUCT_FOR_ID(source_traceback)
714+
STRUCT_FOR_ID(spam)
714715
STRUCT_FOR_ID(src)
715716
STRUCT_FOR_ID(src_dir_fd)
716717
STRUCT_FOR_ID(stacklevel)

Include/internal/pycore_runtime_init_generated.h

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Include/internal/pycore_unicodeobject_generated.h

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_capi/test_getargs.py

+33
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@
44
import sys
55
from test import support
66
from test.support import import_helper
7+
from test.support import script_helper
78
from test.support import warnings_helper
89
# Skip this test if the _testcapi module isn't available.
910
_testcapi = import_helper.import_module('_testcapi')
1011
from _testcapi import getargs_keywords, getargs_keyword_only
1112

13+
try:
14+
import _testinternalcapi
15+
except ImportError:
16+
_testinternalcapi = NULL
17+
1218
# > How about the following counterproposal. This also changes some of
1319
# > the other format codes to be a little more regular.
1420
# >
@@ -1346,6 +1352,33 @@ def test_nested_tuple(self):
13461352
"argument 1 must be sequence of length 1, not 0"):
13471353
parse(((),), {}, '(' + f + ')', ['a'])
13481354

1355+
@unittest.skipIf(_testinternalcapi is None, 'needs _testinternalcapi')
1356+
def test_gh_119213(self):
1357+
rc, out, err = script_helper.assert_python_ok("-c", """if True:
1358+
from test import support
1359+
script = '''if True:
1360+
import _testinternalcapi
1361+
_testinternalcapi.gh_119213_getargs(spam='eggs')
1362+
'''
1363+
config = dict(
1364+
allow_fork=False,
1365+
allow_exec=False,
1366+
allow_threads=True,
1367+
allow_daemon_threads=False,
1368+
use_main_obmalloc=False,
1369+
gil=2,
1370+
check_multi_interp_extensions=True,
1371+
)
1372+
rc = support.run_in_subinterp_with_config(script, **config)
1373+
assert rc == 0
1374+
1375+
# The crash is different if the interpreter was not destroyed first.
1376+
#interpid = _testinternalcapi.create_interpreter()
1377+
#rc = _testinternalcapi.exec_interpreter(interpid, script)
1378+
#assert rc == 0
1379+
""")
1380+
self.assertEqual(rc, 0)
1381+
13491382

13501383
if __name__ == "__main__":
13511384
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Non-builtin modules built with argument clinic were crashing if used in a
2+
subinterpreter before the main interpreter. The objects that were causing
3+
the problem by leaking between interpreters carelessly have been fixed.

Modules/_testinternalcapi.c

+20
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,25 @@ has_inline_values(PyObject *self, PyObject *obj)
20062006
Py_RETURN_FALSE;
20072007
}
20082008

2009+
2010+
/*[clinic input]
2011+
gh_119213_getargs
2012+
2013+
spam: object = None
2014+
2015+
Test _PyArg_Parser.kwtuple
2016+
[clinic start generated code]*/
2017+
2018+
static PyObject *
2019+
gh_119213_getargs_impl(PyObject *module, PyObject *spam)
2020+
/*[clinic end generated code: output=d8d9c95d5b446802 input=65ef47511da80fc2]*/
2021+
{
2022+
// It must never have been called in the main interprer
2023+
assert(!_Py_IsMainInterpreter(PyInterpreterState_Get()));
2024+
return Py_NewRef(spam);
2025+
}
2026+
2027+
20092028
static PyMethodDef module_functions[] = {
20102029
{"get_configs", get_configs, METH_NOARGS},
20112030
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2096,6 +2115,7 @@ static PyMethodDef module_functions[] = {
20962115
#ifdef _Py_TIER2
20972116
{"uop_symbols_test", _Py_uop_symbols_test, METH_NOARGS},
20982117
#endif
2118+
GH_119213_GETARGS_METHODDEF
20992119
{NULL, NULL} /* sentinel */
21002120
};
21012121

Modules/clinic/_testinternalcapi.c.h

+61-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Python/getargs.c

+19-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "pycore_dict.h" // _PyDict_HasOnlyStringKeys()
88
#include "pycore_modsupport.h" // export _PyArg_NoKeywords()
99
#include "pycore_pylifecycle.h" // _PyArg_Fini
10+
#include "pycore_pystate.h" // _Py_IsMainInterpreter()
1011
#include "pycore_tuple.h" // _PyTuple_ITEMS()
1112
#include "pycore_pyerrors.h" // _Py_CalculateSuggestions()
1213

@@ -1947,7 +1948,23 @@ _parser_init(void *arg)
19471948
int owned;
19481949
PyObject *kwtuple = parser->kwtuple;
19491950
if (kwtuple == NULL) {
1951+
/* We may temporarily switch to the main interpreter to avoid
1952+
* creating a tuple that could outlive its owning interpreter. */
1953+
PyThreadState *save_tstate = NULL;
1954+
PyThreadState *temp_tstate = NULL;
1955+
if (!_Py_IsMainInterpreter(PyInterpreterState_Get())) {
1956+
temp_tstate = PyThreadState_New(_PyInterpreterState_Main());
1957+
if (temp_tstate == NULL) {
1958+
return -1;
1959+
}
1960+
save_tstate = PyThreadState_Swap(temp_tstate);
1961+
}
19501962
kwtuple = new_kwtuple(keywords, len, pos);
1963+
if (temp_tstate != NULL) {
1964+
PyThreadState_Clear(temp_tstate);
1965+
(void)PyThreadState_Swap(save_tstate);
1966+
PyThreadState_Delete(temp_tstate);
1967+
}
19511968
if (kwtuple == NULL) {
19521969
return -1;
19531970
}
@@ -1969,8 +1986,8 @@ _parser_init(void *arg)
19691986
parser->next = _Py_atomic_load_ptr(&_PyRuntime.getargs.static_parsers);
19701987
do {
19711988
// compare-exchange updates parser->next on failure
1972-
} while (_Py_atomic_compare_exchange_ptr(&_PyRuntime.getargs.static_parsers,
1973-
&parser->next, parser));
1989+
} while (!_Py_atomic_compare_exchange_ptr(&_PyRuntime.getargs.static_parsers,
1990+
&parser->next, parser));
19741991
return 0;
19751992
}
19761993

Tools/clinic/libclinic/parse_args.py

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ def declare_parser(
5151
#endif
5252
"""
5353
else:
54+
# XXX Why do we not statically allocate the tuple
55+
# for non-builtin modules?
5456
declarations = """
5557
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
5658

0 commit comments

Comments
 (0)