Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gh-126119: fix some crashes in code objects if co_stacksize is absurdly large #126122

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
15 changes: 15 additions & 0 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions Lib/test/test_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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])
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
29 changes: 29 additions & 0 deletions Lib/test/test_generators.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import copy
import gc
import pickle
import re
import sys
import doctest
import unittest
Expand All @@ -10,6 +11,7 @@
import types

from test import support
from test.support import import_helper

try:
import _testcapi
Expand Down Expand Up @@ -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 = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a crash in DEBUG builds due to an overflow when the :attr:`co_stacksize
<codeobject.co_stacksize>` field of a :ref:`code object <code-objects>` is
set to an absurdely large integer.
picnixz marked this conversation as resolved.
Show resolved Hide resolved
Reported by Valery Fedorenko. Patch by Bénédikt Tran.
16 changes: 15 additions & 1 deletion Objects/codeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
22 changes: 18 additions & 4 deletions Objects/frameobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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__,
Expand Down
22 changes: 18 additions & 4 deletions Objects/genobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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__,
Expand Down
Loading