diff --git a/Lib/test/test_clinic_functionality/__init__.py b/Lib/test/test_clinic_functionality/__init__.py new file mode 100644 index 00000000000000..3ad4812cda5e16 --- /dev/null +++ b/Lib/test/test_clinic_functionality/__init__.py @@ -0,0 +1 @@ +from .test_clinic_functionality import * diff --git a/Lib/test/test_clinic_functionality/__main__.py b/Lib/test/test_clinic_functionality/__main__.py new file mode 100644 index 00000000000000..e3847f31fd6c91 --- /dev/null +++ b/Lib/test/test_clinic_functionality/__main__.py @@ -0,0 +1,3 @@ +import unittest + +unittest.main('test.test_clinic_functionality') diff --git a/Lib/test/test_clinic_functionality/clinic/test_clinic_functionality.c.h b/Lib/test/test_clinic_functionality/clinic/test_clinic_functionality.c.h new file mode 100644 index 00000000000000..46a6aa9903ac48 --- /dev/null +++ b/Lib/test/test_clinic_functionality/clinic/test_clinic_functionality.c.h @@ -0,0 +1,165 @@ +/*[clinic input] +preserve +[clinic start generated code]*/ + +#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head +# include "pycore_runtime.h" // _Py_ID() +#endif + + +PyDoc_STRVAR(gh_32092_oob__doc__, +"gh_32092_oob($module, /, pos1, pos2, *varargs, kw1=None, kw2=None)\n" +"--\n" +"\n" +"Proof-of-concept of GH-32092 OOB bug.\n" +"\n" +"Array index out-of-bound bug in function\n" +"`_PyArg_UnpackKeywordsWithVararg` .\n" +"\n" +"Calling this function by gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6)\n" +"to trigger this bug (crash).\n" +"Expected return: (1, 2, (3, 4), 5, 6)"); + +#define GH_32092_OOB_METHODDEF \ + {"gh_32092_oob", _PyCFunction_CAST(gh_32092_oob), METH_FASTCALL|METH_KEYWORDS, gh_32092_oob__doc__}, + +static PyObject * +gh_32092_oob_impl(PyObject *module, PyObject *pos1, PyObject *pos2, + PyObject *varargs, PyObject *kw1, PyObject *kw2); + +static PyObject * +gh_32092_oob(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 4 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(pos1), &_Py_ID(pos2), &_Py_ID(kw1), &_Py_ID(kw2), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"pos1", "pos2", "kw1", "kw2", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "gh_32092_oob", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[5]; + Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + PyObject *pos1; + PyObject *pos2; + PyObject *varargs = NULL; + PyObject *kw1 = Py_None; + PyObject *kw2 = Py_None; + + args = _PyArg_UnpackKeywordsWithVararg(args, nargs, NULL, kwnames, &_parser, 2, 2, 0, 2, argsbuf); + if (!args) { + goto exit; + } + pos1 = args[0]; + pos2 = args[1]; + varargs = args[2]; + if (!noptargs) { + goto skip_optional_kwonly; + } + if (args[3]) { + kw1 = args[3]; + if (!--noptargs) { + goto skip_optional_kwonly; + } + } + kw2 = args[4]; +skip_optional_kwonly: + return_value = gh_32092_oob_impl(module, pos1, pos2, varargs, kw1, kw2); + +exit: + Py_XDECREF(varargs); + return return_value; +} + +PyDoc_STRVAR(gh_32092_kw_pass__doc__, +"gh_32092_kw_pass($module, /, pos, *args, kw=None)\n" +"--\n" +"\n" +"Proof-of-concept of GH-32092 keyword args passing bug.\n" +"\n" +"The calculation of `noptargs` in AC-generated function\n" +"`builtin_kw_pass_poc` is incorrect.\n" +"\n" +"Calling this function by gh_32092_kw_pass(1, 2, 3)\n" +"to trigger this bug (crash).\n" +"Expected return: (1, (2, 3))"); + +#define GH_32092_KW_PASS_METHODDEF \ + {"gh_32092_kw_pass", _PyCFunction_CAST(gh_32092_kw_pass), METH_FASTCALL|METH_KEYWORDS, gh_32092_kw_pass__doc__}, + +static PyObject * +gh_32092_kw_pass_impl(PyObject *module, PyObject *pos, PyObject *args, + PyObject *kw); + +static PyObject * +gh_32092_kw_pass(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(pos), &_Py_ID(kw), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"pos", "kw", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "gh_32092_kw_pass", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 1; + PyObject *pos; + PyObject *__clinic_args = NULL; + PyObject *kw = Py_None; + + args = _PyArg_UnpackKeywordsWithVararg(args, nargs, NULL, kwnames, &_parser, 1, 1, 0, 1, argsbuf); + if (!args) { + goto exit; + } + pos = args[0]; + __clinic_args = args[1]; + if (!noptargs) { + goto skip_optional_kwonly; + } + kw = args[2]; +skip_optional_kwonly: + return_value = gh_32092_kw_pass_impl(module, pos, __clinic_args, kw); + +exit: + Py_XDECREF(__clinic_args); + return return_value; +} +/*[clinic end generated code: output=cebc2a5a0048c387 input=a9049054013a1b77]*/ diff --git a/Lib/test/test_clinic_functionality/setup.py b/Lib/test/test_clinic_functionality/setup.py new file mode 100644 index 00000000000000..d5891a465cfefc --- /dev/null +++ b/Lib/test/test_clinic_functionality/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup, Extension +from test import support + + +def main(): + SOURCE = support.findfile('test_clinic_functionality.c', subdir='test_clinic_functionality') + module = Extension('test_clinic_functionality', sources=[SOURCE]) + setup(name='test_clinic_functionality', version='0.0', ext_modules=[module]) + + +if __name__ == '__main__': + main() diff --git a/Lib/test/test_clinic_functionality/test_clinic_functionality.c b/Lib/test/test_clinic_functionality/test_clinic_functionality.c new file mode 100644 index 00000000000000..0ba688e3d58199 --- /dev/null +++ b/Lib/test/test_clinic_functionality/test_clinic_functionality.c @@ -0,0 +1,97 @@ +#define PY_SSIZE_T_CLEAN +#include +#include "clinic/test_clinic_functionality.c.h" + + +/*[clinic input] +module clinic_functional_tester +[clinic start generated code]*/ +/*[clinic end generated code: output=da39a3ee5e6b4b0d input=2ee8b0b242501b11]*/ + +/*[clinic input] +gh_32092_oob + + pos1: object + pos2: object + *varargs: object + kw1: object = None + kw2: object = None + +Proof-of-concept of GH-32092 OOB bug. + +Array index out-of-bound bug in function +`_PyArg_UnpackKeywordsWithVararg` . + +Calling this function by gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6) +to trigger this bug (crash). +Expected return: (1, 2, (3, 4), 5, 6) + +[clinic start generated code]*/ + +static PyObject * +gh_32092_oob_impl(PyObject *module, PyObject *pos1, PyObject *pos2, + PyObject *varargs, PyObject *kw1, PyObject *kw2) +/*[clinic end generated code: output=ee259c130054653f input=91d8e227acf93b02]*/ +{ + PyObject *tuple = PyTuple_New(5);; + PyTuple_SET_ITEM(tuple, 0, Py_NewRef(pos1)); + PyTuple_SET_ITEM(tuple, 1, Py_NewRef(pos2)); + PyTuple_SET_ITEM(tuple, 2, Py_NewRef(varargs)); + PyTuple_SET_ITEM(tuple, 3, Py_NewRef(kw1)); + PyTuple_SET_ITEM(tuple, 4, Py_NewRef(kw2)); + return tuple; +} + +/*[clinic input] +gh_32092_kw_pass + + pos: object + *args: object + kw: object = None + +Proof-of-concept of GH-32092 keyword args passing bug. + +The calculation of `noptargs` in AC-generated function +`builtin_kw_pass_poc` is incorrect. + +Calling this function by gh_32092_kw_pass(1, 2, 3) +to trigger this bug (crash). +Expected return: (1, (2, 3)) + +[clinic start generated code]*/ + +static PyObject * +gh_32092_kw_pass_impl(PyObject *module, PyObject *pos, PyObject *args, + PyObject *kw) +/*[clinic end generated code: output=4a2bbe4f7c8604e9 input=c51b7572ac09f193]*/ +{ + PyObject *tuple = PyTuple_New(3);; + PyTuple_SET_ITEM(tuple, 0, Py_NewRef(pos)); + PyTuple_SET_ITEM(tuple, 1, Py_NewRef(args)); + PyTuple_SET_ITEM(tuple, 2, Py_NewRef(kw)); + return tuple; +} + +static PyMethodDef tester_methods[] = { + GH_32092_OOB_METHODDEF + GH_32092_KW_PASS_METHODDEF + {NULL, NULL} +}; + +static struct PyModuleDef clinic_functional_tester_module = { + PyModuleDef_HEAD_INIT, + "clinic_functional_tester", + NULL, + 0, + tester_methods, + NULL, + NULL, + NULL, + NULL +}; + +PyMODINIT_FUNC +PyInit_test_clinic_functionality(void) +{ + return PyModule_Create(&clinic_functional_tester_module); +} diff --git a/Lib/test/test_clinic_functionality/test_clinic_functionality.py b/Lib/test/test_clinic_functionality/test_clinic_functionality.py new file mode 100644 index 00000000000000..53cad36bfb3b29 --- /dev/null +++ b/Lib/test/test_clinic_functionality/test_clinic_functionality.py @@ -0,0 +1,128 @@ +import os.path +import sys +import unittest +import subprocess +import textwrap +from test import support +from test.support import os_helper + + +MS_WINDOWS = (sys.platform == 'win32') +SETUP = support.findfile('setup.py', subdir='test_clinic_functionality') +MOD_NAME = 'test_clinic_functionality' + + +def _run_cmd(operation, cmd, verbose=False): + if verbose: + print(f'{operation}:', ' '.join(cmd)) + subprocess.run(cmd, check=True) + else: + proc = subprocess.run(cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True) + if proc.returncode: + print(proc.stdout, end='') + self.fail(f'{operation} failed with exit code {proc.returncode}') + + +class EnvInitializer: + """Build the module before test and clear the files after test.""" + python = '' + verbose = support.verbose + + def run_cmd(self, operation, cmd): + _run_cmd(operation, cmd, self.verbose) + + def _setup_env(self): + venv_dir = 'env' + + # Create virtual environment to get setuptools + cmd = [sys.executable, '-X', 'dev', '-m', 'venv', venv_dir] + self.run_cmd('Setup venv', cmd) + + # Get the Python executable of the venv + python_exe = 'python' + if sys.executable.endswith('.exe'): + python_exe += '.exe' + if MS_WINDOWS: + self.python = os.path.join(venv_dir, 'Scripts', python_exe) + else: + self.python = os.path.join(venv_dir, 'bin', python_exe) + + # Build module + cmd = [self.python, '-X', 'dev', SETUP, 'build_ext', '--verbose'] + self.run_cmd('Build', cmd) + + # Install module + cmd = [self.python, '-X', 'dev', SETUP, 'install'] + self.run_cmd('Install', cmd) + + def setup_env(self): + # Build in a temporary directory + with os_helper.temp_cwd(): + self._setup_env() + yield + + def __init__(self): + if self.verbose: + print() + print('Setup test environment') + self.env = self.setup_env() + next(self.env) + + def __del__(self): + if self.verbose: + print('Clear test environment') + try: + next(self.env) + except StopIteration: + pass + except Exception as e: + raise e + + +env = None + + +def setUpModule(): + global env + env = EnvInitializer() + + +def tearDownModule(): + global env + del env + + +@support.requires_subprocess() +@support.requires_venv_with_pip() +class TestClinicFunctional(unittest.TestCase): + verbose = support.verbose + + def run_cmd(self, operation, cmd): + _run_cmd(operation, cmd, self.verbose) + + def exec_script(self, script): + script = textwrap.dedent(script) + global env + python = env.python + cmd = [python, '-c', script] + self.run_cmd('Test', cmd) + + def assert_func_result(self, func_str, expected_result): + script = f'''\ + import {MOD_NAME} as mod + assert(mod.{func_str} == {expected_result}) + ''' + self.exec_script(script) + + def test_gh_32092_oob(self): + self.assert_func_result('gh_32092_oob(1, 2, 3, 4, kw1=5, kw2=6)', '(1, 2, (3, 4), 5, 6))') + + def test_gh_32092_kw_pass(self): + self.assert_func_result('gh_32092_kw_pass(1, 2, 3)', '(1, (2, 3))') + + +if __name__ == "__main__": + unittest.main()