diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py index 7c1ef42a4970d7..5e1444a92635a7 100644 --- a/Lib/test/support/__init__.py +++ b/Lib/test/support/__init__.py @@ -901,6 +901,21 @@ def check_sizeof(test, o, size): % (type(o), result, size) test.assertEqual(result, size, msg) + +def get_frame_specials_size(): + """Compute the C defined constant FRAME_SPECIALS_SIZE in codeobject.c.""" + try: + import _testinternalcapi + except ImportError: + raise unittest.SkipTest("_testinternalcapi required") + + c = (lambda: ...).__code__ + # co_framesize = co_stacksize + co_nlocalsplus + FRAME_SPECIALS_SIZE + co_framesize = _testinternalcapi.get_co_framesize(c) + co_nlocalsplus = len({*c.co_varnames, *c.co_cellvars, *c.co_freevars}) + return co_framesize - c.co_stacksize - co_nlocalsplus + + #======================================================================= # Decorator/context manager for running a code in a different locale, # correctly resetting it afterwards. diff --git a/Lib/test/test_code.py b/Lib/test/test_code.py index 93c65a82508dcb..5981b8a075b88c 100644 --- a/Lib/test/test_code.py +++ b/Lib/test/test_code.py @@ -194,6 +194,18 @@ import ctypes except ImportError: ctypes = None + +try: + import _testcapi +except ImportError: + _testcapi = None + +try: + import _testinternalcapi +except ImportError: + _testinternalcapi = None + +from test import support from test.support import (cpython_only, check_impl_detail, requires_debug_ranges, gc_collect, Py_GIL_DISABLED) @@ -579,6 +591,28 @@ def test_code_equal_with_instrumentation(self): self.assertNotEqual(code1, code2) sys.settrace(None) + @unittest.skipUnless(ctypes, "requires ctypes") + @unittest.skipUnless(_testcapi, "requires _testcapi") + @unittest.skipUnless(_testinternalcapi, "requires _testinternalcapi") + def test_co_framesize_overflow(self): + # See: https://github.com/python/cpython/issues/126119. + + def foo(a, b): + x = a * b + return x + + c = foo.__code__ + + fss = support.get_frame_specials_size() + ps = ctypes.sizeof(ctypes.c_void_p) # sizeof(PyObject *) + co_nlocalsplus = len({*c.co_varnames, *c.co_cellvars, *c.co_freevars}) + # anything below that limit is a valid co_stacksize + evil_stacksize = int(_testcapi.INT_MAX / ps - fss - co_nlocalsplus) + + with self.assertRaisesRegex(OverflowError, "co_stacksize"): + c.__replace__(co_stacksize=evil_stacksize) + c.__replace__(co_stacksize=evil_stacksize - 1) + def isinterned(s): return s is sys.intern(('_' + s + '_')[1:-1]) diff --git a/Lib/test/test_frame.py b/Lib/test/test_frame.py index 11f191700ccef0..d4b3695563a668 100644 --- a/Lib/test/test_frame.py +++ b/Lib/test/test_frame.py @@ -222,6 +222,29 @@ def test_f_lineno_del_segfault(self): with self.assertRaises(AttributeError): del f.f_lineno + @unittest.skipUnless(_testcapi, "requires _testcapi") + def test_sizeof_overflow(self): + # See: https://github.com/python/cpython/issues/126119 + ctypes = import_helper.import_module('ctypes') + + f, _, _ = self.make_frames() + c = f.f_code + co_nlocalsplus = len({*c.co_varnames, *c.co_cellvars, *c.co_freevars}) + + fss = support.get_frame_specials_size() + ps = ctypes.sizeof(ctypes.c_void_p) # sizeof(PyObject *) + evil_stacksize = int(_testcapi.INT_MAX / ps - fss - co_nlocalsplus) + # an evil code with a valid (but very large) stack size + evil_code = f.f_code.replace(co_stacksize=evil_stacksize - 1) + + if sys.maxsize == 2147483647: # 32-bit machine + with self.assertRaises(MemoryError): + frame = _testcapi.frame_new(evil_code, globals(), locals()) + else: + frame = _testcapi.frame_new(evil_code, globals(), locals()) + message = re.escape("size exceeds INT_MAX") + self.assertRaisesRegex(OverflowError, message, frame.__sizeof__) + class ReprTest(unittest.TestCase): """ diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index bf2cb1160723b0..c2f311376fbc49 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -1,6 +1,7 @@ import copy import gc import pickle +import re import sys import doctest import unittest @@ -10,6 +11,7 @@ import types from test import support +from test.support import import_helper try: import _testcapi @@ -268,6 +270,33 @@ def loop(): #This should not raise loop() + @unittest.skipUnless(_testcapi, "requires _testcapi") + def test_gi_frame_f_code_overflow(self): + # See: https://github.com/python/cpython/issues/126119 + ctypes = import_helper.import_module('ctypes') + + def f(): yield + c = f().gi_frame.f_code + co_nlocalsplus = len({*c.co_varnames, *c.co_cellvars, *c.co_freevars}) + + ps = ctypes.sizeof(ctypes.c_void_p) # sizeof(PyObject *) + fss = support.get_frame_specials_size() + # anything below that limit is a valid co_stacksize + evil_stacksize = int(_testcapi.INT_MAX / ps - fss - co_nlocalsplus) + + evil = c.__replace__(co_stacksize=evil_stacksize - 1) + + if support.Py_GIL_DISABLED: + self.skipTest("segmentation fault on free-threaded builds") + elif sys.maxsize == 2147483647: # 32-bit machine + with self.assertRaises(MemoryError): + evil_gi = types.FunctionType(evil, {})() + else: + # the following crashes on free-threaded builds for now + evil_gi = types.FunctionType(evil, {})() + message = re.escape("size exceeds INT_MAX") + self.assertRaisesRegex(OverflowError, message, evil_gi.__sizeof__) + class ModifyUnderlyingIterableTest(unittest.TestCase): iterables = [ diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2024-10-29-11-47-19.gh-issue-126119.xbAvxt.rst b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-29-11-47-19.gh-issue-126119.xbAvxt.rst new file mode 100644 index 00000000000000..c177a81d657005 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2024-10-29-11-47-19.gh-issue-126119.xbAvxt.rst @@ -0,0 +1,4 @@ +Fix a crash in DEBUG builds due to an overflow when the :attr:`co_stacksize +` field of a :ref:`code object ` is +set to an absurdly large integer. +Reported by Valery Fedorenko. Patch by Bénédikt Tran. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 775ea7aca824c4..3c0a942cc909db 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -436,7 +436,21 @@ _PyCode_Validate(struct _PyCodeConstructor *con) PyErr_SetString(PyExc_ValueError, "code: co_varnames is too small"); return -1; } - + /* + * The framesize = stacksize + nlocalsplus + FRAME_SPECIALS_SIZE is used + * as framesize * sizeof(PyObject *) and assumed to be < INT_MAX. Thus, + * we need to dynamically limit the value of stacksize. Note that this + * usually prevents crashes due to assertions but a MemoryError may still + * be triggered later. + * + * See https://github.com/python/cpython/issues/126119 for details. + */ + int ub = (int)(INT_MAX / sizeof(PyObject *)) - FRAME_SPECIALS_SIZE; + Py_ssize_t nlocalsplus = PyTuple_GET_SIZE(con->localsplusnames); + if (nlocalsplus >= (Py_ssize_t)ub || con->stacksize >= (int)ub - nlocalsplus) { + PyErr_SetString(PyExc_OverflowError, "code: co_stacksize is too large"); + return -1; + } return 0; } diff --git a/Objects/frameobject.c b/Objects/frameobject.c index 55394afa523213..9a9097643e5ba6 100644 --- a/Objects/frameobject.c +++ b/Objects/frameobject.c @@ -1802,11 +1802,25 @@ PyDoc_STRVAR(clear__doc__, static PyObject * frame_sizeof(PyFrameObject *f, PyObject *Py_UNUSED(ignored)) { - Py_ssize_t res; - res = offsetof(PyFrameObject, _f_frame_data) + offsetof(_PyInterpreterFrame, localsplus); + Py_ssize_t base = offsetof(PyFrameObject, _f_frame_data) + + offsetof(_PyInterpreterFrame, localsplus); + assert(base <= INT_MAX); PyCodeObject *code = _PyFrame_GetCode(f->f_frame); - res += _PyFrame_NumSlotsForCodeObject(code) * sizeof(PyObject *); - return PyLong_FromSsize_t(res); + int nslots = _PyFrame_NumSlotsForCodeObject(code); + assert(nslots >= 0); + // By construction, 0 <= nslots < code->co_framesize <= INT_MAX. + // It should not be possible to have nslots >= PY_SSIZE_T_MAX + // even if PY_SSIZE_T_MAX < INT_MAX because code->co_framesize + // is checked in _PyCode_Validate(). However, it is possible + // to make base + nslots * sizeof(PyObject *) >= INT_MAX since + // 'base' is not yet known when creating code objects. + // + // See https://github.com/python/cpython/issues/126119 for details. + if (nslots >= (int)((INT_MAX - base) / sizeof(PyObject *))) { + PyErr_SetString(PyExc_OverflowError, "size exceeds INT_MAX"); + return NULL; + } + return PyLong_FromSsize_t(base + nslots * sizeof(PyObject *)); } PyDoc_STRVAR(sizeof__doc__, diff --git a/Objects/genobject.c b/Objects/genobject.c index 19c2c4e3331a89..33eb880cd0dc2a 100644 --- a/Objects/genobject.c +++ b/Objects/genobject.c @@ -817,11 +817,25 @@ static PyMemberDef gen_memberlist[] = { static PyObject * gen_sizeof(PyGenObject *gen, PyObject *Py_UNUSED(ignored)) { - Py_ssize_t res; - res = offsetof(PyGenObject, gi_iframe) + offsetof(_PyInterpreterFrame, localsplus); + Py_ssize_t base = offsetof(PyGenObject, gi_iframe) + + offsetof(_PyInterpreterFrame, localsplus); + assert(base <= INT_MAX); PyCodeObject *code = _PyGen_GetCode(gen); - res += _PyFrame_NumSlotsForCodeObject(code) * sizeof(PyObject *); - return PyLong_FromSsize_t(res); + int nslots = _PyFrame_NumSlotsForCodeObject(code); + assert(nslots >= 0); + // By construction, 0 <= nslots < code->co_framesize <= INT_MAX. + // It should not be possible to have nslots >= PY_SSIZE_T_MAX + // even if PY_SSIZE_T_MAX < INT_MAX because code->co_framesize + // is checked in _PyCode_Validate(). However, it is possible + // to make base + nslots * sizeof(PyObject *) >= INT_MAX since + // 'base' is not yet known when creating code objects. + // + // See https://github.com/python/cpython/issues/126119 for details. + if (nslots >= (int)((INT_MAX - base) / sizeof(PyObject *))) { + PyErr_SetString(PyExc_OverflowError, "size exceeds INT_MAX"); + return NULL; + } + return PyLong_FromSsize_t(base + nslots * sizeof(PyObject *)); } PyDoc_STRVAR(sizeof__doc__,