From 95036ea25d47f0081bda2ba96ea327f3375cb6a4 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 22 Nov 2018 17:15:37 +0100 Subject: [PATCH] [3.7] bpo-9263: _PyObject_Dump() detects freed memory (GH-10061) (GH-10662) * bpo-9263: _PyObject_Dump() detects freed memory (GH-10061) _PyObject_Dump() now uses an heuristic to check if the object memory has been freed: log "" in that case. The heuristic rely on the debug hooks on Python memory allocators which fills the memory with DEADBYTE (0xDB) when memory is deallocated. Use PYTHONMALLOC=debug to always enable these debug hooks. (cherry picked from commit 82af0b63b07aa8d92b50098e382b458143cfc677) * bpo-9263: Fix _PyObject_Dump() for freed object (#10661) If _PyObject_Dump() detects that the object is freed, don't try to dump it (exit immediately). Enhance also _PyObject_IsFreed(): it now detects if the pointer itself looks like freed memory. (cherry picked from commit 2cf5d32fd9e61488e8b0be55a2e92a752ba8b06b) --- Include/object.h | 1 + Include/pymem.h | 4 ++- Objects/object.c | 83 +++++++++++++++++++++++++++++++++------------- Objects/obmalloc.c | 16 +++++++++ 4 files changed, 80 insertions(+), 24 deletions(-) diff --git a/Include/object.h b/Include/object.h index c772deaf57d..bcf78afe6bb 100644 --- a/Include/object.h +++ b/Include/object.h @@ -521,6 +521,7 @@ struct _Py_Identifier; PyAPI_FUNC(int) PyObject_Print(PyObject *, FILE *, int); PyAPI_FUNC(void) _Py_BreakPoint(void); PyAPI_FUNC(void) _PyObject_Dump(PyObject *); +PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *); #endif PyAPI_FUNC(PyObject *) PyObject_Repr(PyObject *); PyAPI_FUNC(PyObject *) PyObject_Str(PyObject *); diff --git a/Include/pymem.h b/Include/pymem.h index 8ee0efddca7..ef6e0bb5e25 100644 --- a/Include/pymem.h +++ b/Include/pymem.h @@ -55,7 +55,9 @@ PyAPI_FUNC(int) PyTraceMalloc_Untrack( PyAPI_FUNC(PyObject*) _PyTraceMalloc_GetTraceback( unsigned int domain, uintptr_t ptr); -#endif /* !Py_LIMITED_API */ + +PyAPI_FUNC(int) _PyMem_IsFreed(void *ptr, size_t size); +#endif /* !defined(Py_LIMITED_API) */ /* BEWARE: diff --git a/Objects/object.c b/Objects/object.c index 5535b7ee7ec..138df448802 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -411,34 +411,71 @@ _Py_BreakPoint(void) } +/* Heuristic checking if the object memory has been deallocated. + Rely on the debug hooks on Python memory allocators which fills the memory + with DEADBYTE (0xDB) when memory is deallocated. + + The function can be used to prevent segmentation fault on dereferencing + pointers like 0xdbdbdbdbdbdbdbdb. Such pointer is very unlikely to be mapped + in memory. */ +int +_PyObject_IsFreed(PyObject *op) +{ + uintptr_t ptr = (uintptr_t)op; + if (_PyMem_IsFreed(&ptr, sizeof(ptr))) { + return 1; + } + int freed = _PyMem_IsFreed(&op->ob_type, sizeof(op->ob_type)); + /* ignore op->ob_ref: the value can have be modified + by Py_INCREF() and Py_DECREF(). */ +#ifdef Py_TRACE_REFS + freed &= _PyMem_IsFreed(&op->_ob_next, sizeof(op->_ob_next)); + freed &= _PyMem_IsFreed(&op->_ob_prev, sizeof(op->_ob_prev)); +#endif + return freed; +} + + /* For debugging convenience. See Misc/gdbinit for some useful gdb hooks */ void _PyObject_Dump(PyObject* op) { - if (op == NULL) - fprintf(stderr, "NULL\n"); - else { - PyGILState_STATE gil; - PyObject *error_type, *error_value, *error_traceback; - - fprintf(stderr, "object : "); - gil = PyGILState_Ensure(); - - PyErr_Fetch(&error_type, &error_value, &error_traceback); - (void)PyObject_Print(op, stderr, 0); - PyErr_Restore(error_type, error_value, error_traceback); - - PyGILState_Release(gil); - /* XXX(twouters) cast refcount to long until %zd is - universally available */ - fprintf(stderr, "\n" - "type : %s\n" - "refcount: %ld\n" - "address : %p\n", - Py_TYPE(op)==NULL ? "NULL" : Py_TYPE(op)->tp_name, - (long)op->ob_refcnt, - op); + if (op == NULL) { + fprintf(stderr, "\n"); + fflush(stderr); + return; } + + if (_PyObject_IsFreed(op)) { + /* It seems like the object memory has been freed: + don't access it to prevent a segmentation fault. */ + fprintf(stderr, "\n"); + return; + } + + PyGILState_STATE gil; + PyObject *error_type, *error_value, *error_traceback; + + fprintf(stderr, "object : "); + fflush(stderr); + gil = PyGILState_Ensure(); + + PyErr_Fetch(&error_type, &error_value, &error_traceback); + (void)PyObject_Print(op, stderr, 0); + fflush(stderr); + PyErr_Restore(error_type, error_value, error_traceback); + + PyGILState_Release(gil); + /* XXX(twouters) cast refcount to long until %zd is + universally available */ + fprintf(stderr, "\n" + "type : %s\n" + "refcount: %ld\n" + "address : %p\n", + Py_TYPE(op)==NULL ? "NULL" : Py_TYPE(op)->tp_name, + (long)op->ob_refcnt, + op); + fflush(stderr); } PyObject * diff --git a/Objects/obmalloc.c b/Objects/obmalloc.c index 1b8b5eefe21..3b0c35bcc94 100644 --- a/Objects/obmalloc.c +++ b/Objects/obmalloc.c @@ -2091,6 +2091,22 @@ _PyMem_DebugRawCalloc(void *ctx, size_t nelem, size_t elsize) } +/* Heuristic checking if the memory has been freed. Rely on the debug hooks on + Python memory allocators which fills the memory with DEADBYTE (0xDB) when + memory is deallocated. */ +int +_PyMem_IsFreed(void *ptr, size_t size) +{ + unsigned char *bytes = ptr; + for (size_t i=0; i < size; i++) { + if (bytes[i] != DEADBYTE) { + return 0; + } + } + return 1; +} + + /* The debug free first checks the 2*SST bytes on each end for sanity (in particular, that the FORBIDDENBYTEs with the api ID are still intact). Then fills the original bytes with DEADBYTE.