Skip to content

Commit 8bcbcf8

Browse files
vstinnerencukou
andauthored
gh-155742: Check singletons consistency at Python exit (#157572)
In debug mode at Python exit, check if immutable singleton objects have been modified by mistake to detect bugs in C extensions. Add tests corrupting bytes, str, bool and int singleton objects. Add pycore_global_objects_fini.h internal header file. pycore_global_objects_fini_generated.h is now fully generated by the script. Co-authored-by: Petr Viktorin <encukou@gmail.com>
1 parent e8c06d6 commit 8bcbcf8

12 files changed

Lines changed: 1231 additions & 2475 deletions

File tree

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#ifndef Py_INTERNAL_GLOBAL_OBJECTS_FINI_H
2+
#define Py_INTERNAL_GLOBAL_OBJECTS_FINI_H
3+
#ifdef __cplusplus
4+
extern "C" {
5+
#endif
6+
7+
#ifndef Py_BUILD_CORE
8+
# error "this header requires Py_BUILD_CORE define"
9+
#endif
10+
11+
#ifdef Py_DEBUG
12+
13+
#include "pycore_bytesobject.h" // _PyBytes_CheckOverflow()
14+
#include "pycore_long.h" // TAG_FROM_SIGN_AND_SIZE()
15+
16+
static inline void
17+
_PyStaticObject_CheckSingleton(PyObject *obj, PyTypeObject *type)
18+
{
19+
// Check PyObject.ob_refcnt
20+
_PyObject_ASSERT(obj, _Py_IsImmortal(obj));
21+
22+
// Check PyObject.ob_type
23+
_PyObject_ASSERT(obj, Py_TYPE(obj) == type);
24+
}
25+
26+
27+
static void
28+
_PyStaticObject_CheckLongSingleton(PyObject *obj, long value, int is_bool)
29+
{
30+
PyTypeObject *type = is_bool ? &PyBool_Type : &PyLong_Type;
31+
_PyStaticObject_CheckSingleton(obj, type);
32+
33+
// Check _PyLong_CompactValue()
34+
Py_ssize_t compact = _PyLong_CompactValue((const PyLongObject *)obj);
35+
_PyObject_ASSERT(obj, compact == value);
36+
37+
// Check tv_tag and ob_digit[0]
38+
_PyLongValue *long_value = &((PyLongObject*)obj)->long_value;
39+
int sign = (value == 0) ? 0 : ((value < 0) ? -1 : 1);
40+
uintptr_t lv_tag = TAG_FROM_SIGN_AND_SIZE(sign, (value == 0) ? 0 : 1);
41+
if (!is_bool) {
42+
lv_tag |= IMMORTALITY_BIT_MASK;
43+
}
44+
_PyObject_ASSERT(obj, long_value->lv_tag == lv_tag);
45+
_PyObject_ASSERT(obj, long_value->ob_digit[0] == Py_ABS(value));
46+
}
47+
48+
49+
static inline void
50+
_PyStaticObject_CheckBytesSingleton(PyObject *obj,
51+
Py_ssize_t size, unsigned char ch)
52+
{
53+
_PyStaticObject_CheckSingleton(obj, &PyBytes_Type);
54+
_PyObject_ASSERT(obj, PyBytes_GET_SIZE(obj) == size);
55+
const unsigned char *str = (const unsigned char *)PyBytes_AS_STRING(obj);
56+
_PyObject_ASSERT(obj, str[0] == ch);
57+
_PyBytes_CheckOverflow(obj, obj, "bytes singleton");
58+
}
59+
60+
static void
61+
_PyStaticObject_CheckUnicode(PyObject *obj, const char *str, Py_ssize_t length)
62+
{
63+
_PyStaticObject_CheckSingleton(obj, &PyUnicode_Type);
64+
_PyObject_ASSERT(obj, _PyUnicode_CheckConsistency(obj, 1));
65+
_PyObject_ASSERT(obj, PyUnicode_GET_LENGTH(obj) == length);
66+
_PyObject_ASSERT(obj, PyUnicode_KIND(obj) == PyUnicode_1BYTE_KIND);
67+
const Py_UCS1 *data = PyUnicode_1BYTE_DATA(obj);
68+
_PyObject_ASSERT(obj, memcmp(data, str, length) == 0);
69+
_PyObject_ASSERT(obj, data[length] == 0);
70+
}
71+
72+
73+
static void
74+
_PyStaticObject_CheckUnicodeCharSingleton(PyObject *obj, unsigned char ch)
75+
{
76+
_PyStaticObject_CheckUnicode(obj, (char *)&ch, 1);
77+
_PyObject_ASSERT(obj, PyUnicode_IS_ASCII(obj) == (ch <= 127));
78+
}
79+
80+
81+
static void
82+
_PyStaticObject_CheckUnicodeSingleton(PyObject *obj,
83+
const char *str, Py_ssize_t length)
84+
{
85+
_PyStaticObject_CheckUnicode(obj, str, length);
86+
_PyObject_ASSERT(obj, PyUnicode_IS_ASCII(obj));
87+
}
88+
89+
#endif // Py_DEBUG
90+
91+
#ifdef __cplusplus
92+
}
93+
#endif
94+
#endif /* !Py_INTERNAL_GLOBAL_OBJECTS_FINI_H */

Include/internal/pycore_global_objects_fini_generated.h

Lines changed: 912 additions & 2438 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_capi/test_misc.py

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

30963096

3097+
@unittest.skipUnless(support.Py_DEBUG, 'need Py_DEBUG')
3098+
class TestCheckSingleton(unittest.TestCase):
3099+
# Test that _PyStaticObjects_CheckAll() detects memory corruptions in
3100+
# singleton objects at Python exit.
3101+
3102+
def check(self, code):
3103+
code = f"""if 1:
3104+
import _testcapi
3105+
from test import support
3106+
support.SuppressCrashReport().__enter__()
3107+
{code}
3108+
"""
3109+
proc = assert_python_failure("-c", code)
3110+
return proc.err
3111+
3112+
def test_corrupt_bytes(self):
3113+
stderr = self.check("_testcapi.corrupt_bytes(b'a', b'#')")
3114+
3115+
self.assertIn((b'_PyStaticObject_CheckBytesSingleton: '
3116+
b'Assertion "str[0] == ch" failed'), stderr)
3117+
self.assertIn(b"object repr : b'#'", stderr)
3118+
3119+
def test_corrupt_unicode(self):
3120+
stderr = self.check("_testcapi.corrupt_unicode('a', '#')")
3121+
3122+
self.assertIn((b'_PyStaticObject_CheckUnicode: '
3123+
b'Assertion "memcmp(data, str, length) == 0" failed'), stderr)
3124+
self.assertIn(b"object repr : '#'", stderr)
3125+
3126+
def test_corrupt_bool(self):
3127+
stderr = self.check("_testcapi.corrupt_long(True, 0)")
3128+
3129+
self.assertIn((b'_PyStaticObject_CheckLongSingleton: '
3130+
b'Assertion "compact == value" failed'),
3131+
stderr)
3132+
self.assertIn(b"object repr : True", stderr)
3133+
3134+
def test_corrupt_long(self):
3135+
stderr = self.check("_testcapi.corrupt_long(5, 42)")
3136+
3137+
self.assertIn((b'_PyStaticObject_CheckLongSingleton: '
3138+
b'Assertion "compact == value" failed'),
3139+
stderr)
3140+
self.assertIn(b"object repr : 42", stderr)
3141+
3142+
30973143
if __name__ == "__main__":
30983144
unittest.main()

Makefile.pre.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1345,6 +1345,7 @@ PYTHON_HEADERS= \
13451345
$(srcdir)/Include/internal/pycore_getopt.h \
13461346
$(srcdir)/Include/internal/pycore_gil.h \
13471347
$(srcdir)/Include/internal/pycore_global_objects.h \
1348+
$(srcdir)/Include/internal/pycore_global_objects_fini.h \
13481349
$(srcdir)/Include/internal/pycore_global_objects_fini_generated.h \
13491350
$(srcdir)/Include/internal/pycore_global_strings.h \
13501351
$(srcdir)/Include/internal/pycore_hamt.h \
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
In debug mode at Python exit, check if immutable singleton objects have been
2+
modified by mistake to detect bugs in C extensions. Patch by Victor Stinner.

Modules/_testcapi/bytes.c

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,21 @@ bytearray_overflow(PyObject *Py_UNUSED(module), PyObject *args)
567567
}
568568

569569

570+
// Write into an immutable bytes object to test _PyStaticObjects_CheckAll()
571+
static PyObject *
572+
corrupt_bytes(PyObject *Py_UNUSED(module), PyObject *args)
573+
{
574+
char *bytes, *override;
575+
Py_ssize_t size;
576+
if (!PyArg_ParseTuple(args, "yy#", &bytes, &override, &size)) {
577+
return NULL;
578+
}
579+
580+
memcpy(bytes, override, size);
581+
Py_RETURN_NONE;
582+
}
583+
584+
570585
static PyMethodDef test_methods[] = {
571586
{"bytes_resize", bytes_resize, METH_VARARGS},
572587
{"bytes_join", bytes_join, METH_VARARGS},
@@ -576,6 +591,7 @@ static PyMethodDef test_methods[] = {
576591
{"test_byteswriter_ptr", test_byteswriter_ptr, METH_NOARGS},
577592
{"bytes_overflow", bytes_overflow, METH_VARARGS},
578593
{"bytearray_overflow", bytearray_overflow, METH_VARARGS},
594+
{"corrupt_bytes", corrupt_bytes, METH_VARARGS},
579595
{NULL},
580596
};
581597

Modules/_testcapi/long.c

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

283283

284+
// Write into an immutable int object to test _PyStaticObjects_CheckAll()
285+
static PyObject *
286+
corrupt_long(PyObject *Py_UNUSED(module), PyObject *args)
287+
{
288+
PyObject *obj;
289+
int value;
290+
if (!PyArg_ParseTuple(args, "Oi", &obj, &value)) {
291+
return NULL;
292+
}
293+
294+
((PyLongObject*)obj)->long_value.ob_digit[0] = value;
295+
Py_RETURN_NONE;
296+
}
297+
298+
284299
static PyMethodDef test_methods[] = {
285300
_TESTCAPI_CALL_LONG_COMPACT_API_METHODDEF
286301
{"pylong_fromunicodeobject", pylong_fromunicodeobject, METH_VARARGS},
@@ -295,6 +310,7 @@ static PyMethodDef test_methods[] = {
295310
{"pylong_ispositive", pylong_ispositive, METH_O},
296311
{"pylong_isnegative", pylong_isnegative, METH_O},
297312
{"pylong_iszero", pylong_iszero, METH_O},
313+
{"corrupt_long", corrupt_long, METH_VARARGS},
298314
{NULL},
299315
};
300316

Modules/_testcapi/unicode.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,25 @@ _Py_COMP_DIAG_POP
276276
}
277277

278278

279+
// Write into an immutable str object to test _PyStaticObjects_CheckAll()
280+
static PyObject *
281+
corrupt_unicode(PyObject *Py_UNUSED(module), PyObject *args)
282+
{
283+
PyObject *obj, *override;
284+
if (!PyArg_ParseTuple(args, "OO", &obj, &override)) {
285+
return NULL;
286+
}
287+
assert(PyUnicode_KIND(obj) == PyUnicode_1BYTE_KIND);
288+
assert(PyUnicode_KIND(override) == PyUnicode_1BYTE_KIND);
289+
290+
Py_UCS1 *dst = PyUnicode_1BYTE_DATA(obj);
291+
Py_UCS1 *src = PyUnicode_1BYTE_DATA(override);
292+
Py_ssize_t size = PyUnicode_GET_LENGTH(override);
293+
memcpy(dst, src, size);
294+
Py_RETURN_NONE;
295+
}
296+
297+
279298
// --- PyUnicodeWriter type -------------------------------------------------
280299

281300
typedef struct {
@@ -622,6 +641,7 @@ static PyMethodDef TestMethods[] = {
622641
{"unicode_copycharacters", unicode_copycharacters, METH_VARARGS},
623642
{"unicode_GET_CACHED_HASH", unicode_GET_CACHED_HASH, METH_O},
624643
{"test_py_identifier", test_py_identifier, METH_NOARGS},
644+
{"corrupt_unicode", corrupt_unicode, METH_VARARGS},
625645
{NULL},
626646
};
627647

PCbuild/pythoncore.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@
261261
<ClInclude Include="..\Include\internal\pycore_getopt.h" />
262262
<ClInclude Include="..\Include\internal\pycore_gil.h" />
263263
<ClInclude Include="..\Include\internal\pycore_global_objects.h" />
264+
<ClInclude Include="..\Include\internal\pycore_global_objects_fini.h" />
264265
<ClInclude Include="..\Include\internal\pycore_global_objects_fini_generated.h" />
265266
<ClInclude Include="..\Include\internal\pycore_hamt.h" />
266267
<ClInclude Include="..\Include\internal\pycore_hashtable.h" />

PCbuild/pythoncore.vcxproj.filters

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,9 @@
702702
<ClInclude Include="..\Include\internal\pycore_global_objects.h">
703703
<Filter>Include\internal</Filter>
704704
</ClInclude>
705+
<ClInclude Include="..\Include\internal\pycore_global_objects_fini.h">
706+
<Filter>Include\internal</Filter>
707+
</ClInclude>
705708
<ClInclude Include="..\Include\internal\pycore_global_objects_fini_generated.h">
706709
<Filter>Include\internal</Filter>
707710
</ClInclude>

0 commit comments

Comments
 (0)