Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions Lib/test/test_structseq_mutation.py
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
"""Regression tests for gh-155322: mutating a heap PyStructSequence type's
writable size attributes (n_fields / n_sequence_fields / n_unnamed_fields)
must not crash the interpreter when a new instance is constructed.
"""
import os
import time
import unittest

try:
import resource
except ImportError:
resource = None


class StructSeqMutationTests(unittest.TestCase):

def _check_overflow_raises(self, structseq_type, sample_args):
structseq_type(sample_args)

original = structseq_type.n_fields
try:
structseq_type.n_fields = 100000
with self.assertRaises(TypeError):
structseq_type(sample_args)
finally:
structseq_type.n_fields = original

structseq_type(sample_args)

def test_terminal_size_n_fields_overflow(self):
self._check_overflow_raises(os.terminal_size, (80, 24))

def test_stat_result_n_fields_overflow(self):
st = os.stat(".")
self._check_overflow_raises(os.stat_result, tuple(st))

def test_struct_time_n_fields_overflow(self):
t = time.localtime()
self._check_overflow_raises(time.struct_time, tuple(t))

@unittest.skipIf(resource is None, "resource module not available")
def test_struct_rusage_n_fields_overflow(self):
ru = resource.getrusage(resource.RUSAGE_SELF)
self._check_overflow_raises(resource.struct_rusage, tuple(ru))

def test_terminal_size_n_sequence_fields_overflow(self):
original = os.terminal_size.n_sequence_fields
try:
os.terminal_size.n_sequence_fields = 100000
with self.assertRaises(TypeError):
os.terminal_size((80, 24))
finally:
os.terminal_size.n_sequence_fields = original
os.terminal_size((80, 24))

def test_terminal_size_negative_n_fields(self):
original = os.terminal_size.n_fields
try:
os.terminal_size.n_fields = -1
with self.assertRaises(TypeError):
os.terminal_size((80, 24))
finally:
os.terminal_size.n_fields = original
os.terminal_size((80, 24))

def test_n_sequence_fields_exceeds_n_fields(self):
orig_seq = time.struct_time.n_sequence_fields
orig_fields = time.struct_time.n_fields
try:
time.struct_time.n_fields = 2
time.struct_time.n_sequence_fields = 3
with self.assertRaises(TypeError):
time.struct_time(tuple(time.localtime()))
finally:
time.struct_time.n_fields = orig_fields
time.struct_time.n_sequence_fields = orig_seq
time.struct_time(tuple(time.localtime()))

def test_n_unnamed_fields_exceeds_n_sequence_fields(self):
orig_seq = time.struct_time.n_sequence_fields
orig_unnamed = time.struct_time.n_unnamed_fields
try:
time.struct_time.n_sequence_fields = 2
time.struct_time.n_unnamed_fields = 5
with self.assertRaises(TypeError):
time.struct_time(tuple(time.localtime()))
finally:
time.struct_time.n_sequence_fields = orig_seq
time.struct_time.n_unnamed_fields = orig_unnamed
time.struct_time(tuple(time.localtime()))


if __name__ == "__main__":
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
Fix a crash in heap-allocated PyStructSequence types when the writable
n_fields attribute is modified before constructing a new instance.
79 changes: 76 additions & 3 deletions Objects/structseq.c
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,7 +38,18 @@ get_type_attr_as_size(PyTypeObject *tp, PyObject *name)
name, tp->tp_name);
return -1;
}
return PyLong_AsSsize_t(v);
Py_ssize_t result = PyLong_AsSsize_t(v);
if (result < 0 && !PyErr_Occurred()) {
// gh-155322: a legitimate (non-overflow) negative value, e.g. the
// user set n_fields = -1. Callers treat "< 0" as "an exception is
// already set" and bail without formatting their own message, so
// we must set one here rather than silently returning -1.
PyErr_Format(PyExc_TypeError,
"%.500s: attribute '%U' must be a non-negative integer",
tp->tp_name, name);
return -1;
}
return result;
}

#define VISIBLE_SIZE(op) Py_SIZE(op)
Expand All@@ -52,13 +63,65 @@ get_type_attr_as_size(PyTypeObject *tp, PyObject *name)
get_type_attr_as_size(tp, &_Py_ID(n_unnamed_fields))
#define UNNAMED_FIELDS(op) UNNAMED_FIELDS_TP(Py_TYPE(op))

// gh-155322: Hidden slots stored in tp_basicsize beyond tp_members.
static Py_ssize_t
structseq_hidden_size(PyTypeObject *type)
{
return (type->tp_basicsize - offsetof(PyStructSequence, ob_item))
/ sizeof(PyObject *);
}

static Py_ssize_t
get_real_size(PyObject *op)
{
// Compute the real size from the visible size (i.e., Py_SIZE()) and the
// number of non-sequence fields accounted for in tp_basicsize.
Py_ssize_t hidden = Py_TYPE(op)->tp_basicsize - offsetof(PyStructSequence, ob_item);
return Py_SIZE(op) + hidden / sizeof(PyObject *);
return Py_SIZE(op) + structseq_hidden_size(Py_TYPE(op));
}

// gh-155322: Upper bound on visible fields derived from tp_members.
static Py_ssize_t
structseq_named_member_count(PyTypeObject *type)
{
Py_ssize_t count = 0;
if (type->tp_members != NULL) {
while (type->tp_members[count].name != NULL) {
count++;
}
}
return count;
}

// gh-155322: Validate user-modifiable size attributes against the immutable
// structseq layout.
static int
structseq_validate_sizes(PyTypeObject *type, Py_ssize_t n_fields,
Py_ssize_t n_sequence_fields,
Py_ssize_t n_unnamed_fields)
{
Py_ssize_t hidden = structseq_hidden_size(type);
Py_ssize_t named_max = structseq_named_member_count(type);

if (n_unnamed_fields < 0 || n_unnamed_fields > n_sequence_fields) {
PyErr_Format(PyExc_TypeError,
"%.500s: n_unnamed_fields (%zd) is invalid",
type->tp_name, n_unnamed_fields);
return -1;
}
if (n_sequence_fields < 0 || n_sequence_fields > named_max) {
PyErr_Format(PyExc_TypeError,
"%.500s: n_sequence_fields (%zd) is invalid",
type->tp_name, n_sequence_fields);
return -1;
}
if (n_fields != n_sequence_fields + hidden) {
PyErr_Format(PyExc_TypeError,
"%.500s: n_fields (%zd) is inconsistent with "
"n_sequence_fields (%zd)",
type->tp_name, n_fields, n_sequence_fields);
return -1;
}
return 0;
}

PyObject *
Expand All@@ -74,6 +137,11 @@ PyStructSequence_New(PyTypeObject *type)
return NULL;
}

// gh-155322: Validate the type's declared layout before allocating.
if (structseq_validate_sizes(type, size, vsize, 0) < 0) {
return NULL;
}

obj = PyObject_GC_NewVar(PyStructSequence, type, size);
if (obj == NULL)
return NULL;
Expand DownExpand Up@@ -183,6 +251,11 @@ structseq_new_impl(PyTypeObject *type, PyObject *arg, PyObject *dict)
return NULL;
}

// gh-155322: Validate the declared layout before indexing tp_members.
if (structseq_validate_sizes(type, max_len, min_len, n_unnamed_fields) < 0) {
return NULL;
}

arg = PySequence_Fast(arg, "constructor requires a sequence");

if (!arg) {
Expand Down
Loading