Skip to content

Commit 2b00db6

Browse files
authored
bpo-36389: _PyObject_IsFreed() now also detects uninitialized memory (GH-12770)
Replace _PyMem_IsFreed() function with _PyMem_IsPtrFreed() inline function. The function is now way more efficient, it became a simple comparison on integers, rather than a short loop. It detects also uninitialized bytes and "forbidden bytes" filled by debug hooks on memory allocators. Add unit tests on _PyObject_IsFreed().
1 parent 57b1a28 commit 2b00db6

File tree

6 files changed

+113
-27
lines changed

6 files changed

+113
-27
lines changed

Include/internal/pycore_pymem.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,31 @@ PyAPI_FUNC(int) _PyMem_SetDefaultAllocator(
155155
PyMemAllocatorDomain domain,
156156
PyMemAllocatorEx *old_alloc);
157157

158+
/* Heuristic checking if a pointer value is newly allocated
159+
(uninitialized) or newly freed. The pointer is not dereferenced, only the
160+
pointer value is checked.
161+
162+
The heuristic relies on the debug hooks on Python memory allocators which
163+
fills newly allocated memory with CLEANBYTE (0xCB) and newly freed memory
164+
with DEADBYTE (0xDB). Detect also "untouchable bytes" marked
165+
with FORBIDDENBYTE (0xFB). */
166+
static inline int _PyMem_IsPtrFreed(void *ptr)
167+
{
168+
uintptr_t value = (uintptr_t)ptr;
169+
#if SIZEOF_VOID_P == 8
170+
return (value == (uintptr_t)0xCBCBCBCBCBCBCBCB
171+
|| value == (uintptr_t)0xDBDBDBDBDBDBDBDB
172+
|| value == (uintptr_t)0xFBFBFBFBFBFBFBFB
173+
);
174+
#elif SIZEOF_VOID_P == 4
175+
return (value == (uintptr_t)0xCBCBCBCB
176+
|| value == (uintptr_t)0xDBDBDBDB
177+
|| value == (uintptr_t)0xFBFBFBFB);
178+
#else
179+
# error "unknown pointer size"
180+
#endif
181+
}
182+
158183
#ifdef __cplusplus
159184
}
160185
#endif

Include/pymem.h

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ PyAPI_FUNC(int) _PyMem_SetupAllocators(const char *opt);
2323

2424
/* Try to get the allocators name set by _PyMem_SetupAllocators(). */
2525
PyAPI_FUNC(const char*) _PyMem_GetAllocatorsName(void);
26-
27-
PyAPI_FUNC(int) _PyMem_IsFreed(void *ptr, size_t size);
2826
#endif /* !defined(Py_LIMITED_API) */
2927

3028

Lib/test/test_capi.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,29 @@ def test_pyobject_malloc_without_gil(self):
526526
code = 'import _testcapi; _testcapi.pyobject_malloc_without_gil()'
527527
self.check_malloc_without_gil(code)
528528

529+
def check_pyobject_is_freed(self, func):
530+
code = textwrap.dedent('''
531+
import gc, os, sys, _testcapi
532+
# Disable the GC to avoid crash on GC collection
533+
gc.disable()
534+
obj = _testcapi.{func}()
535+
error = (_testcapi.pyobject_is_freed(obj) == False)
536+
# Exit immediately to avoid a crash while deallocating
537+
# the invalid object
538+
os._exit(int(error))
539+
''')
540+
code = code.format(func=func)
541+
assert_python_ok('-c', code, PYTHONMALLOC=self.PYTHONMALLOC)
542+
543+
def test_pyobject_is_freed_uninitialized(self):
544+
self.check_pyobject_is_freed('pyobject_uninitialized')
545+
546+
def test_pyobject_is_freed_forbidden_bytes(self):
547+
self.check_pyobject_is_freed('pyobject_forbidden_bytes')
548+
549+
def test_pyobject_is_freed_free(self):
550+
self.check_pyobject_is_freed('pyobject_freed')
551+
529552

530553
class PyMemMallocDebugTests(PyMemDebugTests):
531554
PYTHONMALLOC = 'malloc_debug'

Modules/_testcapimodule.c

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4236,6 +4236,59 @@ test_pymem_getallocatorsname(PyObject *self, PyObject *args)
42364236
}
42374237

42384238

4239+
static PyObject*
4240+
pyobject_is_freed(PyObject *self, PyObject *op)
4241+
{
4242+
int res = _PyObject_IsFreed(op);
4243+
return PyBool_FromLong(res);
4244+
}
4245+
4246+
4247+
static PyObject*
4248+
pyobject_uninitialized(PyObject *self, PyObject *args)
4249+
{
4250+
PyObject *op = (PyObject *)PyObject_Malloc(sizeof(PyObject));
4251+
if (op == NULL) {
4252+
return NULL;
4253+
}
4254+
/* Initialize reference count to avoid early crash in ceval or GC */
4255+
Py_REFCNT(op) = 1;
4256+
/* object fields like ob_type are uninitialized! */
4257+
return op;
4258+
}
4259+
4260+
4261+
static PyObject*
4262+
pyobject_forbidden_bytes(PyObject *self, PyObject *args)
4263+
{
4264+
/* Allocate an incomplete PyObject structure: truncate 'ob_type' field */
4265+
PyObject *op = (PyObject *)PyObject_Malloc(offsetof(PyObject, ob_type));
4266+
if (op == NULL) {
4267+
return NULL;
4268+
}
4269+
/* Initialize reference count to avoid early crash in ceval or GC */
4270+
Py_REFCNT(op) = 1;
4271+
/* ob_type field is after the memory block: part of "forbidden bytes"
4272+
when using debug hooks on memory allocatrs! */
4273+
return op;
4274+
}
4275+
4276+
4277+
static PyObject*
4278+
pyobject_freed(PyObject *self, PyObject *args)
4279+
{
4280+
PyObject *op = _PyObject_CallNoArg((PyObject *)&PyBaseObject_Type);
4281+
if (op == NULL) {
4282+
return NULL;
4283+
}
4284+
Py_TYPE(op)->tp_dealloc(op);
4285+
/* Reset reference count to avoid early crash in ceval or GC */
4286+
Py_REFCNT(op) = 1;
4287+
/* object memory is freed! */
4288+
return op;
4289+
}
4290+
4291+
42394292
static PyObject*
42404293
pyobject_malloc_without_gil(PyObject *self, PyObject *args)
42414294
{
@@ -4907,6 +4960,10 @@ static PyMethodDef TestMethods[] = {
49074960
{"pymem_api_misuse", pymem_api_misuse, METH_NOARGS},
49084961
{"pymem_malloc_without_gil", pymem_malloc_without_gil, METH_NOARGS},
49094962
{"pymem_getallocatorsname", test_pymem_getallocatorsname, METH_NOARGS},
4963+
{"pyobject_is_freed", (PyCFunction)(void(*)(void))pyobject_is_freed, METH_O},
4964+
{"pyobject_uninitialized", pyobject_uninitialized, METH_NOARGS},
4965+
{"pyobject_forbidden_bytes", pyobject_forbidden_bytes, METH_NOARGS},
4966+
{"pyobject_freed", pyobject_freed, METH_NOARGS},
49104967
{"pyobject_malloc_without_gil", pyobject_malloc_without_gil, METH_NOARGS},
49114968
{"tracemalloc_track", tracemalloc_track, METH_VARARGS},
49124969
{"tracemalloc_untrack", tracemalloc_untrack, METH_VARARGS},

Objects/object.c

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -425,18 +425,17 @@ _Py_BreakPoint(void)
425425
int
426426
_PyObject_IsFreed(PyObject *op)
427427
{
428-
uintptr_t ptr = (uintptr_t)op;
429-
if (_PyMem_IsFreed(&ptr, sizeof(ptr))) {
428+
if (_PyMem_IsPtrFreed(op) || _PyMem_IsPtrFreed(op->ob_type)) {
430429
return 1;
431430
}
432-
int freed = _PyMem_IsFreed(&op->ob_type, sizeof(op->ob_type));
433-
/* ignore op->ob_ref: the value can have be modified
431+
/* ignore op->ob_ref: its value can have be modified
434432
by Py_INCREF() and Py_DECREF(). */
435433
#ifdef Py_TRACE_REFS
436-
freed &= _PyMem_IsFreed(&op->_ob_next, sizeof(op->_ob_next));
437-
freed &= _PyMem_IsFreed(&op->_ob_prev, sizeof(op->_ob_prev));
434+
if (_PyMem_IsPtrFreed(op->_ob_next) || _PyMem_IsPtrFreed(op->_ob_prev)) {
435+
return 1;
436+
}
438437
#endif
439-
return freed;
438+
return 0;
440439
}
441440

442441

@@ -453,7 +452,7 @@ _PyObject_Dump(PyObject* op)
453452
if (_PyObject_IsFreed(op)) {
454453
/* It seems like the object memory has been freed:
455454
don't access it to prevent a segmentation fault. */
456-
fprintf(stderr, "<freed object>\n");
455+
fprintf(stderr, "<Freed object>\n");
457456
return;
458457
}
459458

Objects/obmalloc.c

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1914,7 +1914,7 @@ _Py_GetAllocatedBlocks(void)
19141914

19151915
/* Special bytes broadcast into debug memory blocks at appropriate times.
19161916
* Strings of these are unlikely to be valid addresses, floats, ints or
1917-
* 7-bit ASCII.
1917+
* 7-bit ASCII. If modified, _PyMem_IsPtrFreed() should be updated as well.
19181918
*/
19191919
#undef CLEANBYTE
19201920
#undef DEADBYTE
@@ -2059,22 +2059,6 @@ _PyMem_DebugRawCalloc(void *ctx, size_t nelem, size_t elsize)
20592059
}
20602060

20612061

2062-
/* Heuristic checking if the memory has been freed. Rely on the debug hooks on
2063-
Python memory allocators which fills the memory with DEADBYTE (0xDB) when
2064-
memory is deallocated. */
2065-
int
2066-
_PyMem_IsFreed(void *ptr, size_t size)
2067-
{
2068-
unsigned char *bytes = ptr;
2069-
for (size_t i=0; i < size; i++) {
2070-
if (bytes[i] != DEADBYTE) {
2071-
return 0;
2072-
}
2073-
}
2074-
return 1;
2075-
}
2076-
2077-
20782062
/* The debug free first checks the 2*SST bytes on each end for sanity (in
20792063
particular, that the FORBIDDENBYTEs with the api ID are still intact).
20802064
Then fills the original bytes with DEADBYTE.

0 commit comments

Comments
 (0)