Skip to content

Commit 48b2e54

Browse files
committed
gh-155742: Check singletons consistencty in the garbage collector
If Python is built in debug mode or with assertions, the garbage collector now checks singletons consistencty to detect data corruption in C extension. Add _Py_CheckSingletons() function. Call this function on a GC collection. Add tests checking that corrupting a singleton is properly detected.
1 parent b8e23da commit 48b2e54

9 files changed

Lines changed: 245 additions & 0 deletions

File tree

Include/internal/pycore_object.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,6 +1037,10 @@ static inline Py_ALWAYS_INLINE void _Py_INCREF_MORTAL(PyObject *op)
10371037
* references. */
10381038
PyAPI_FUNC(int) _PyObject_VisitType(PyObject *op, visitproc visit, void *arg);
10391039

1040+
#ifndef NDEBUG
1041+
extern void _Py_CheckSingletons(void);
1042+
#endif
1043+
10401044
#ifdef __cplusplus
10411045
}
10421046
#endif

Lib/test/test_capi/test_misc.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3094,5 +3094,58 @@ def test_ceval_decref(self):
30943094
self.assertEqual(lines.count("DESTROY list"), 2)
30953095

30963096

3097+
class TestCheckSingleton(unittest.TestCase):
3098+
# Test _Py_CheckSingletons() which is called by gc.collect()
3099+
#
3100+
# Corrupt some singleton objects and make sure that the data corruption
3101+
# is detected.
3102+
3103+
def check(self, func):
3104+
code = f"""if 1:
3105+
import _testcapi
3106+
from test import support
3107+
support.SuppressCrashReport().__enter__()
3108+
_testcapi.{func}()
3109+
"""
3110+
proc = assert_python_failure("-c", code)
3111+
return proc.err
3112+
3113+
def test_corrupt_bytes_singleton(self):
3114+
stderr = self.check("corrupt_bytes_singleton")
3115+
3116+
# In fact, it's the character b'a' which is corrupted
3117+
self.assertIn(b"object repr : b'A'", stderr)
3118+
self.assertIn((b'check_singleton_bytes: '
3119+
b'Assertion "str[0] == ch" failed'),
3120+
stderr)
3121+
3122+
def test_corrupt_unicode_singleton(self):
3123+
stderr = self.check("corrupt_unicode_singleton")
3124+
3125+
# In fact, it's the character b'a' which is corrupted
3126+
self.assertIn(b"object repr : 'A'", stderr)
3127+
self.assertIn((b'check_singleton_unicode: Assertion '
3128+
b'"PyUnicode_READ_CHAR(((PyObject*)((obj))), (0))'
3129+
b' == ch" failed'),
3130+
stderr)
3131+
3132+
def test_corrupt_bool_singleton(self):
3133+
stderr = self.check("corrupt_bool_singleton")
3134+
3135+
self.assertIn(b"object repr : True", stderr)
3136+
self.assertIn((b'check_singleton_long: '
3137+
b'Assertion "compact == value" failed'),
3138+
stderr)
3139+
3140+
def test_corrupt_long_singleton(self):
3141+
stderr = self.check("corrupt_long_singleton")
3142+
3143+
# In fact, it's the number 5 which is corrupted
3144+
self.assertIn(b"object repr : 42", stderr)
3145+
self.assertIn((b'check_singleton_long: '
3146+
b'Assertion "compact == value" failed'),
3147+
stderr)
3148+
3149+
30973150
if __name__ == "__main__":
30983151
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
If Python is built in debug mode or with assertions, the garbage collector
2+
now checks singletons consistencty to detect data corruption in C extension.
3+
Patch by Victor Stinner.

Modules/_testcapi/bytes.c

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,12 +351,25 @@ byteswriter_highlevel(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
351351
}
352352

353353

354+
static PyObject *
355+
corrupt_bytes_singleton(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
356+
{
357+
PyObject *obj = PyBytes_FromStringAndSize("a", 1);
358+
assert(obj != NULL);
359+
PyBytes_AS_STRING(obj)[0] = 'A';
360+
361+
PyGC_Collect();
362+
Py_RETURN_NONE;
363+
}
364+
365+
354366
static PyMethodDef test_methods[] = {
355367
{"bytes_resize", bytes_resize, METH_VARARGS},
356368
{"bytes_join", bytes_join, METH_VARARGS},
357369
{"byteswriter_abc", byteswriter_abc, METH_NOARGS},
358370
{"byteswriter_resize", byteswriter_resize, METH_NOARGS},
359371
{"byteswriter_highlevel", byteswriter_highlevel, METH_NOARGS},
372+
{"corrupt_bytes_singleton", corrupt_bytes_singleton, METH_NOARGS},
360373
{NULL},
361374
};
362375

Modules/_testcapi/long.c

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,29 @@ get_pylong_layout(PyObject *module, PyObject *Py_UNUSED(args))
281281
}
282282

283283

284+
static PyObject *
285+
corrupt_bool_singleton(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
286+
{
287+
PyObject *obj = Py_True;
288+
((PyLongObject*)obj)->long_value.ob_digit[0] = 0;
289+
290+
PyGC_Collect();
291+
Py_RETURN_NONE;
292+
}
293+
294+
295+
static PyObject *
296+
corrupt_long_singleton(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
297+
{
298+
PyObject *obj = PyLong_FromLong(5);
299+
assert(obj != NULL);
300+
((PyLongObject*)obj)->long_value.ob_digit[0] = 42;
301+
302+
PyGC_Collect();
303+
Py_RETURN_NONE;
304+
}
305+
306+
284307
static PyMethodDef test_methods[] = {
285308
_TESTCAPI_CALL_LONG_COMPACT_API_METHODDEF
286309
{"pylong_fromunicodeobject", pylong_fromunicodeobject, METH_VARARGS},
@@ -295,6 +318,8 @@ static PyMethodDef test_methods[] = {
295318
{"pylong_ispositive", pylong_ispositive, METH_O},
296319
{"pylong_isnegative", pylong_isnegative, METH_O},
297320
{"pylong_iszero", pylong_iszero, METH_O},
321+
{"corrupt_bool_singleton", corrupt_bool_singleton, METH_NOARGS},
322+
{"corrupt_long_singleton", corrupt_long_singleton, METH_NOARGS},
298323
{NULL},
299324
};
300325

Modules/_testcapi/unicode.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,19 @@ static PyType_Spec Writer_spec = {
563563
};
564564

565565

566+
static PyObject *
567+
corrupt_unicode_singleton(PyObject *Py_UNUSED(module), PyObject *Py_UNUSED(args))
568+
{
569+
PyObject *obj = PyUnicode_FromOrdinal('a');
570+
assert(obj != NULL);
571+
assert(PyUnicode_KIND(obj) == PyUnicode_1BYTE_KIND);
572+
PyUnicode_1BYTE_DATA(obj)[0] = 'A';
573+
574+
PyGC_Collect();
575+
Py_RETURN_NONE;
576+
}
577+
578+
566579
static PyMethodDef TestMethods[] = {
567580
{"unicode_new", unicode_new, METH_VARARGS},
568581
{"unicode_fill", unicode_fill, METH_VARARGS},
@@ -572,6 +585,7 @@ static PyMethodDef TestMethods[] = {
572585
{"unicode_asutf8", unicode_asutf8, METH_VARARGS},
573586
{"unicode_copycharacters", unicode_copycharacters, METH_VARARGS},
574587
{"unicode_GET_CACHED_HASH", unicode_GET_CACHED_HASH, METH_O},
588+
{"corrupt_unicode_singleton", corrupt_unicode_singleton, METH_NOARGS},
575589
{NULL},
576590
};
577591

Objects/object.c

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3526,3 +3526,124 @@ Py_ssize_t Py_REFCNT(PyObject *ob) { return _Py_REFCNT(ob); }
35263526
Py_ssize_t Py_SIZE(PyObject *o) { return _Py_SIZE_impl(o); }
35273527
int Py_IS_TYPE(PyObject *o, PyTypeObject *t) { return _Py_IS_TYPE_impl(o, t); }
35283528
void Py_SET_SIZE(PyVarObject *o, Py_ssize_t s) { _Py_SET_SIZE_impl(o, s); }
3529+
3530+
3531+
#ifndef NDEBUG
3532+
static void
3533+
check_singleton(PyObject *obj, PyTypeObject *type)
3534+
{
3535+
// Check PyObject.ob_refcnt
3536+
_PyObject_ASSERT(obj, _Py_IsImmortal(obj));
3537+
3538+
// Check PyObject.ob_type
3539+
_PyObject_ASSERT(obj, Py_TYPE(obj) == type);
3540+
}
3541+
3542+
3543+
static void
3544+
check_singleton_long(PyObject *obj, long value, int is_bool)
3545+
{
3546+
PyTypeObject *type = is_bool ? &PyBool_Type : &PyLong_Type;
3547+
check_singleton(obj, type);
3548+
3549+
// Check _PyLong_CompactValue()
3550+
Py_ssize_t compact = _PyLong_CompactValue((const PyLongObject *)obj);
3551+
_PyObject_ASSERT(obj, compact == value);
3552+
3553+
// Check tv_tag and ob_digit[0]
3554+
_PyLongValue *long_value = &((PyLongObject*)obj)->long_value;
3555+
int sign = (value == 0) ? 0 : ((value < 0) ? -1 : 1);
3556+
uintptr_t lv_tag = TAG_FROM_SIGN_AND_SIZE(sign, (value == 0) ? 0 : 1);
3557+
if (!is_bool) {
3558+
lv_tag |= IMMORTALITY_BIT_MASK;
3559+
}
3560+
_PyObject_ASSERT(obj, long_value->lv_tag == lv_tag);
3561+
_PyObject_ASSERT(obj, long_value->ob_digit[0] == Py_ABS(value));
3562+
}
3563+
3564+
3565+
static void
3566+
check_singleton_bytes(PyObject *obj, Py_ssize_t size, unsigned char ch)
3567+
{
3568+
check_singleton(obj, &PyBytes_Type);
3569+
_PyObject_ASSERT(obj, PyBytes_GET_SIZE(obj) == size);
3570+
const unsigned char *str = (const unsigned char *)PyBytes_AS_STRING(obj);
3571+
_PyObject_ASSERT(obj, str[0] == ch);
3572+
if (size > 0) {
3573+
_PyObject_ASSERT(obj, str[1] == 0);
3574+
}
3575+
}
3576+
3577+
3578+
static void
3579+
check_singleton_unicode(PyObject *obj, Py_ssize_t length, Py_UCS4 ch)
3580+
{
3581+
check_singleton(obj, &PyUnicode_Type);
3582+
_PyObject_ASSERT(obj, _PyUnicode_CheckConsistency(obj, 1));
3583+
3584+
_PyObject_ASSERT(obj, PyUnicode_GET_LENGTH(obj) == length);
3585+
3586+
_PyObject_ASSERT(obj, PyUnicode_READ_CHAR(obj, 0) == ch);
3587+
if (length > 0) {
3588+
_PyObject_ASSERT(obj, PyUnicode_READ_CHAR(obj, 1) == 0);
3589+
}
3590+
}
3591+
3592+
3593+
// Check singletons consistency: try to detect if a C extension modified a
3594+
// singleton by mistake.
3595+
//
3596+
// Since the hash is computed lazily, don't check the hash, except for empty
3597+
// tuple.
3598+
void
3599+
_Py_CheckSingletons(void)
3600+
{
3601+
assert(!PyErr_Occurred());
3602+
PyObject *obj;
3603+
long ival;
3604+
3605+
// None
3606+
obj = Py_None;
3607+
check_singleton(obj, &_PyNone_Type);
3608+
3609+
// Ellipsis (...)
3610+
obj = Py_Ellipsis;
3611+
check_singleton(obj, &PyEllipsis_Type);
3612+
3613+
// False, True
3614+
check_singleton_long(Py_False, 0, 1);
3615+
check_singleton_long(Py_True, 1, 1);
3616+
3617+
// Small integers
3618+
for (ival=-_PY_NSMALLNEGINTS; ival < _PY_NSMALLPOSINTS; ival++) {
3619+
obj = (PyObject *)&_PyLong_SMALL_INTS[_PY_NSMALLNEGINTS + ival];
3620+
check_singleton_long(obj, ival, 0);
3621+
}
3622+
3623+
// Empty bytes string (b'')
3624+
obj = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
3625+
check_singleton_bytes(obj, 0, '\0');
3626+
3627+
for (ival=0; ival <= 255; ival++) {
3628+
obj = (PyObject*)&_Py_SINGLETON(bytes_characters)[ival];
3629+
check_singleton_bytes(obj, 1, ival);
3630+
}
3631+
3632+
// Empty Unicode string ('')
3633+
obj = Py_GetConstant(Py_CONSTANT_EMPTY_STR);
3634+
check_singleton_unicode(obj, 0, 0);
3635+
3636+
for (ival=0; ival <= 255; ival++) {
3637+
obj = _Py_LATIN1_CHR(ival);
3638+
check_singleton_unicode(obj, 1, ival);
3639+
}
3640+
3641+
// Empty tuple (())
3642+
obj = Py_GetConstant(Py_CONSTANT_EMPTY_TUPLE);
3643+
check_singleton(obj, &PyTuple_Type);
3644+
_PyObject_ASSERT(obj, PyTuple_GET_SIZE(obj) == 0);
3645+
_PyObject_ASSERT(obj, ((PyTupleObject*)obj)->ob_hash == _PyTuple_HASH_EMPTY);
3646+
3647+
assert(!PyErr_Occurred());
3648+
}
3649+
#endif

Python/gc.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,12 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
16431643
invoke_gc_callback(tstate, "stop", generation, &stats);
16441644
}
16451645

1646+
#ifndef NDEBUG
1647+
// Checking singletons consistency is unrelated to a garbage collection.
1648+
// Using a garbage collection to trigger this function is just convenient.
1649+
_Py_CheckSingletons();
1650+
#endif
1651+
16461652
assert(!_PyErr_Occurred(tstate));
16471653
gcstate->frame = NULL;
16481654
_Py_atomic_store_int(&gcstate->collecting, 0);

Python/gc_free_threading.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2313,6 +2313,12 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason)
23132313
invoke_gc_callback(tstate, "stop", generation, m, n, state.candidates, duration);
23142314
}
23152315

2316+
#ifndef NDEBUG
2317+
// Checking singletons consistency is unrelated to a garbage collection.
2318+
// Using a garbage collection to trigger this function is just convenient.
2319+
_Py_CheckSingletons();
2320+
#endif
2321+
23162322
assert(!_PyErr_Occurred(tstate));
23172323
gcstate->frame = NULL;
23182324
_Py_atomic_store_int(&gcstate->collecting, 0);

0 commit comments

Comments
 (0)