From 126d0b443efcc075b70c4b3ec5f92d774db53313 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 17 Jul 2025 05:50:07 -0400 Subject: [PATCH 1/8] Add myself as an author. --- .github/CODEOWNERS | 1 + peps/pep-0797.rst | 661 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 peps/pep-0797.rst diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 31cb4e89dde..e4588d8549d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -673,6 +673,7 @@ peps/pep-0791.rst @vstinner peps/pep-0792.rst @dstufft peps/pep-0793.rst @encukou peps/pep-0794.rst @brettcannon +peps/pep-0797.rst @ZeroIntensity # ... peps/pep-0801.rst @warsaw # ... diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst new file mode 100644 index 00000000000..54e21c452bb --- /dev/null +++ b/peps/pep-0797.rst @@ -0,0 +1,661 @@ +PEP: 797 +Title: Arbitrary Object Immortalization +Author: Peter Bierma +Discussions-To: Pending +Status: Draft +Type: Standards Track +Requires: 683 +Created: 16-Jul-2025 +Python-Version: 3.15 +Post-History: `01-Jul-2025 `__ + +Abstract +======== + +:pep:`683` introduced :term:`immortal` objects into CPython, using an "immortal +reference count" where the reference count field is never modified on the +object. Currently, all APIs for enabling immortality on a given object are +private, and even those APIs aren't fully safe for immortalizing *any* object. + +In particular, the private API has ``_Py_SetImmortal``, which simply untracks a +passed object from the garbage collector, and then sets its reference count to +the magic immortal number. But again, this isn't totally safe; for example, due +to the current string interning implementation, immortalizing a string can cause +crashes. To make matters worse, there's no mechanism in place to deallocate an +object once it has become immortal; objects on the heap are leaked, which is bad +for per-interpreter state. + +This PEP aims to introduce a public way to immortalize arbitrary objects, while +making sure the object is deallocated later (specifically, during interpreter +finalization). This is done by introducing two new APIs: :func:`sys._immortalize` +on the Python side, and :c:func:`PyUnstable_Immortalize` on the C side. + +Both of these functions make the given object immortal, and then deallocate it when +the interpreter is shutting down using a simulated garbage collection. + +For example: + +.. code-block:: C + + static PyObject * + my_method(PyObject *self, PyObject *object) // METH_O + { + if (PyUnstable_Immortalize(object) < 0) { + return NULL; + } + + /* The object is now immortal, we can do whatever we want with it's + reference count without worrying about thread safety or leaks. */ + + // This is totally safe for an immortal object + for (int i = 0; i < 10; ++i) { + Py_DECREF(object); + } + + Py_RETURN_NONE; + } + +Or, in Python: + +.. code-block:: python + + import sys + + my_own_singleton = object() + sys._immortalize(my_own_singleton) + assert sys._is_immortal(my_own_singleton) + +What Do We Mean By "Immortal"? +****************************** + +By immortal, we mean an object with an immortal reference count, per :pep:`683`. +However, the main difference in this PEP is that the underlying behavior differs +from the documentation, which states that the object is *never* deallocated: + + Some objects are immortal and have reference counts that are never + modified, and therefore the objects are never deallocated. + +In this case, immortal objects are eventually deallocated, it's just that the user +never really sees when that happens because they're freed during interpreter +finalization. So, the term "immortal" throughout this PEP really means +"never deallocated for the lifetime of the interpreter," rather than +"never deallocated." + +What Problem Does Immortality Solve? +************************************ + +Currently, the main way to share objects between subinterpreters is via +serialization. This works OK, but can be limited for many objects. +Immortality is a good precursor to directly sharing objects across multiple +interpreters, as it provides a way to make reference counting thread-safe +without atomic operations on the reference count field (which has proven to +drastically hurt performance). + +Subinterpreters aside, immortality is just generally a powerful tool to wield, +especially for CPython maintainers. The authors of this PEP do not expect that +immortality will become a common tool for Python code, but the complexity of +the approach on its own warrants a PEP. + +Speculatively, arbitrary object immortalization could prove to be useful in +other areas of CPython, or in third-party libraries: + +- Some (especially user-defined) objects on the :term:`free threaded ` + build are still subject to reference count contention. It is possible to help + mitigate this problem through deferred reference counting in the C API + (:c:func:`PyUnstable_Object_EnableDeferredRefcount`), but that has the downside + of damaging single-threaded performance. +- Immortality can help memory usage by avoiding copy-on-write operations in + child processes. Instagram has been `doing this in Cinder `_ + for years, and it would be nice to let CPython also take advantage of it. + +.. _pep-797-cinder: https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/ + +Motivation +========== + +Immortal Objects Must Remain in CPython +*************************************** + +In the past, the main pushback to exposing APIs for immortality is that +it's exposing an implementation detail, and would force CPython to keep +immortality forever. Unfortunately, we've already reached that point: too +many users, and `CPython itself <_pep-797-omitting-immortals>`_, omit +reference counting for known immortal objects, such as :c:data:`Py_None`. +Since there's no good way to deprecate that, CPython will always need some +form of immortality to retain compatibility. That said, this proposal still +keeps all new APIs as unstable. + +.. _pep-797-omitting-immortals: https://github.com/python/cpython/issues/103906 + +Objects Cannot be Directly Shared Between Interpreters +****************************************************** + +The main benefit of immortality is that it makes it somewhat +possible to share objects between subinterpreters. + +Currently, objects cannot be shared for two main reasons, +which will be discussed further in a moment. As of writing, the rule +is that you cannot modify the reference count of an object created +in a different interpreter, and since you can't modify the reference +count, you can't do anything with the object. Since immortality prevents +reference counting operations, this PEP hopes to solve that issue, allowing +for an object proxy that can safely be reference counted in all interpreters. + +Reference Count Modifications Must be Per-interpreter +----------------------------------------------------- + +On GIL-enabled builds of CPython, reference counting operations are protected +by the interpreter's GIL, meaning that trying to modify the reference count of +an object that belongs to another interpreter could cause data races. + +This isn't a problem on the free-threaded build as reference count operations +are atomic, but subinterpreters are supported on the with-GIL build. An +alternative option could be to make reference counting also atomic on the +GIL-enabled build, but that has been shown to damage single-threaded performance +or compromise compatibility with the stable ABI. + +Immortality is a clever way of fixing this problem, because by avoiding +reference counting, there is no risk of data races. In fact, both +subinterpreters and free-threading already use immortality for thread-safety +on some common objects, such as :const:`None`, :const:`True`, or :const:`False`. + +Lastly, immortality is also faster than deferred reference counting +(:c:func:`PyUnstable_Object_EnableDeferredRefcount`), which is used for +mitigating reference count contention on some objects on the free-threaded build. +It would be nice to allow users (likely large C API wrappers) to take advantage +of that speedup. + +Deallocators Rely On the Interpreter State +------------------------------------------ + +Another, less intuitive, danger of reference counting is that when the +object's reference count reaches zero, it immediately tries to get freed. +In doing so, an object will release its memory back to CPython's object allocator +(typically via :c:func:`PyObject_GC_Del` or :c:func:`PyObject_Free`). This +allocator is per-interpreter, so it is unsafe to try to release an object's +memory in an interpreter different from the one that created it. + +Rationale +========= + + +Specification +============= + +Changes to the Internal API +*************************** + +Internally, there are two functions for making an object immortal: +``_Py_SetImmortal`` and ``_Py_SetImmortalUntracked``. +Because the interpreter might want to do its own things with immortal +objects, either now or in the future, the interpreter state should yield +control of a user-defined immortal object to the internal API. So, +the existing internal functions will gain an extra case to remove an object +from the user-defined immortals, so the interpreter won't try and manually +free it later. A new internal function, ``_Py_SetImmortalKnown``, will be +used instead for places where it's certain that objects could not have +been made immortal by a user. + +Changes to String Interning +*************************** + +Currently, the string interning implementation is reliant +on the idea that it controls when a string will become immortal. +Since a user can now immortalize a string on their own, string +interning has to be made "self-aware," meaning that it has to be +able to dynamically figure out if its immortal or not. + +The only place where this needs to change is in +:c:func:`PyUnicode_CHECK_INTERNED`. If the current state is +``SSTATE_INTERNED_MORTAL``, but the object is immortal, then it should +return ``SSTATE_INTERNED_IMMORTAL`` instead. + +Changes to Object Finalization +****************************** + +The C API function, :c:func:`PyObject_CallFinalizer`, must be able to +de-duplicate calls to immortal objects, even if they're non-GC types. + +Immortalization APIs +******************** + +This PEP introduces two new semi-public APIs for making an object immortal: +:c:func:`PyUnstable_Immortalize` and :func:`sys._immortalize`. + +.. c:function:: int PyUnstable_Immortalize(PyObject *obj) + + Make *obj* :term:`immortal`. An :term:`immortal` object can have higher performance for + :term:`reference count` modifications with the cost of being less memory efficient. + + This function returns ``1`` if *obj* was successfully made immortal, or + ``0`` if the object is already immortal. On error, this function returns + ``-1`` with an exception set. The caller must hold an + :term:`attached thread state`. + + In order to be made immortal, an object must follow the "immortalization + contract". This contract is based on best practices in Python's C API, and + it's very unlikely that a type doesn't follow it: + + * The object must be allocated under either the "object" or "memory" allocator + domains. See :ref:`Allocator Domains ` for more information. + Default values of :c:member:`~PyTypeObject.tp_alloc` always use the object + allocator. + * All :term:`strong references ` released in the object's + :c:member:`~PyTypeObject.tp_dealloc` must also be traversed by the + object's :c:member:`~PyTypeObject.tp_traverse` slot, if the type can + contain circular references. This is a general requirement for any garbage + collected type. + * All finalization for an object must be done in + :c:member:`~PyTypeObject.tp_finalize` by + :c:function:`PyObject_CallFinalizerFromDealloc`. + + .. warning:: + Abuse of this function can result in high memory usage for a program. + +.. function:: _immortalize(obj) + + Make *obj* an :term:`immortal` object. This means that it will not be deallocated for the + lifetime of the Python interpreter. + + For objects that have costly modifications to their :term:`reference count`, + making them :term:`immortal` can help improve performance. For example, on + the :term:`free-threaded ` build, making an object + :term:`immortal` can help mitigate bottlenecks that come from concurrent + modification of an object's :term:`reference count`. + + .. warning:: + This function should be considered a low-level routine that most users should avoid. Making too many + objects immortal can result in drastically higher memory usage for a program. + + .. impl-detail:: This function is specific to CPython. + +Immortalization Contract +************************ + +The hardest part about immortality is the deallocation, which has to rely +on some assumptions about the object model in order to work properly. We'll +refer to this as the "immortalization contract." These assumptions aren't all +that disruptive, as they're already documented requires and most (if not all) +extensions in the wild already follow these rules. The only difference is that +these are a hard requirement for immortalization, whereas they're sometimes only +"best practices" in the current API. + +Better yet, the immortalization contract only applies to the C API, because objects +defined in Python already follow these rules. Note that even for objects that +don't properly uphold the immortalization contract will not immediately break +if this PEP is accepted; they'll only break if a user decides to immortalize them. +See `Backwards Compatibility`_ for more information. + +Reference Counts Cannot be Relied On +------------------------------------ + +This is already true today, but becomes moreso with this proposal. +Any object that relies on the reference count (typically, in assertions +or in tests) will now break if they are made immortal, because the reference +count of an object will unexpectedly not change. + +In one of the 3.14 alphas, this rule about reference counting was +`brought up `_ for clarification by a user of the +limited C API. From that post, it's clear that reference count numbers are +an implementation detail: + + But honestly, expecting specific refcounts is not going to work in the + future anyway, due to changes in GC implementation and free threading. + +In addition, the :term:`documentation ` explicitly mentions +that reference counts are unstable: + + In CPython, reference counts are not considered to be stable or + well-defined values; the number of references to an object, and how that + number is affected by Python code, may be different between versions. + +.. _pep-797-limited-refcount: https://discuss.python.org/t/72006 + +Destructors Must Use ``tp_finalize`` +------------------------------------ + +Once again, this should already be true for all extensions. +Since Python 3.4 (:pep:`442`), :c:member:`~PyTypeObject.tp_finalize` alongside +:c:func:`PyObject_CallFinalizerFromDealloc` has been the correct way to run +cleanup code for an extension type. Immortal object deallocation requires that +:c:member:`~PyTypeObject.tp_dealloc` will not try and run Python code, and instead +defer it to :c:member:`~PyTypeObject.tp_finalize`, for both GC and non-GC types. + +As long as :c:func:`PyObject_CallFinalizerFromDealloc` is used in the destructor, +it is completely safe to run cleanup code, because it will just end up no-oping +due to the de-duplication from :c:func:`PyObject_CallFinalizer` (the actual +finalizer call will have been called earlier during immortal object cleanup; +it's just that destruction is one of the last steps in immortal object +deallocation). + +Using the Object or Memory Domain is a Must +------------------------------------------- + +For an object immortal on the heap, the object domain (:c:func:`PyObject_Malloc` +or :c:func:`PyObject_GC_New`) or the memory domain (:c:func:`PyMem_Malloc`) +must be used. Otherwise, the interpreter will not be able to hijack the +object's allocator and will result in use-after-free violations during +deallocation. Note that in 3.13, using the object domain is already a +:ref:`hard requirement ` for the free threading +garbage collector. + + The free-threaded build requires that only Python objects are allocated + using the “object” domain and that all Python objects are allocated using + that domain. This differs from the prior Python versions, where this was + only a best practice and not a hard requirement. + +Reference Cycles Must be Clearable and Traversable +-------------------------------------------------- + +In order for immortal object deallocation to be safe, an object that can have +reference cycles must be able to clear those reference cycles via +their :c:member:`~PyTypeObject.tp_clear` slot. + +Currently, most extension types that hold a :term:`strong reference` to another +Python object will be able to properly clear references through the garbage +collector. In rare cases, it's possible that a buggy extension type could miss +traversing a strong reference. For immortal object deallocation to work +correctly, Python must be able to identify reference cycles on immortal objects. +Prior to this proposal, reference cycles that aren't traversed by the garbage +collector will just leak, but now if they're immortalized, they +will crash. There's not all that much that can be done about this problem; it's +a bug anyway to have these hidden reference cycles, but it just becomes more of +an issue with this PEP. + +Immortal Object Deallocation +**************************** + +The hardest part of immortalizing arbitrary objects is dealing with +deallocating them. The "intuitive" way of doing it would be to go +through the list of immortal objects, set their reference count to zero, +and then run their deallocator. Unfortunately, immortal objects can contain +circular references, and if that's the case, simply deallocating them in this +way would result in a use-after-free violation. Instead, the immortal object +deallocation process intends to simulate what would happen if the objects were +cleaned up by a garbage collection. In this PEP, this process is divided into +three iterations over the full list of user-defined immortal objects, or "phases". + +Phase One: Finalization +----------------------- + +The initial problem to solv ewith deallocating immortal objects is that they +can run finalizers, which might reference other immortal objects. Imagine a +case where there are two immortal objects: A, and B. Object B has a finalizer +that references object A, and object A has a finalizer that references object +B. The solution is to simply run their finalizer seperately from their +deallocator. + +Temporary Mortalization for Finalizers +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is documented (and expected with assertions) that finalizers are run with +a reference count of 1 [#tp_dealloc_refcount]_, so each immortal object needs +to be temporarily mortalized; this can be done with the private ``_Py_SetMortal`` +API that currently exists. After the finalizer is done, it must be +immortalized again so other finalizers don't accidentally deallocate it in +their own finalizers by releasing a reference. + +In addition, an object might expect that its finalizer only be called by the +garbage collector if the type doesn't call +:c:func:`PyObject_CallFinalizerFromDealloc` in its destructor, so objects that +were GC-tracked prior to immortalized immortal should get re-tracked for the +finalizer. + +The psuedo-code looks like this: + +.. code-block:: python + + def finalize_immortals(interp: PyInterpreterState) -> None: + # iter_immortals() is implementation-dependent + for immortal in iter_immortals(interp.runtime_immortals.values): + _Py_SetMortal(immortal.object, 1) + if immortal.gc_tracked: + PyObject_GC_Track(immortal.object) + + PyObject_CallFinalizer(immortal.object) + + if immortal.gc_tracked: + PyObject_GC_UnTrack(immortal.object) + _Py_SetImmortalKnown(immortal.object) + +Identifying Immortal Referents +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +In some cases, a + +To find the mortal objects that might want to reference an immortal, we have +to make a reasonable assumption about how garbage collection works: in order +for an object to :c:func:`Py_DECREF` another object in its destructor, it +must hold a :term:`strong reference` to that object, and therefore traverse +it in its :c:member:`~PyTypeObject.tp_traverse` slot. With that in mind, Python +supports customizing the ``visitproc`` passed to the ``tp_traverse`` function, so +it's possible to find all the mortal objects referenced by an immortal and +correctly finalize them. + +While unlikely, it's possible that some types rely on being garbage collector +tracked in their ``tp_traverse`` slot, because it's typically only possible to +be called during scanning. To ensure that things don't accidentally break, immortal +objects are once again re-tracked to traverse the object. + +Since finalizers might mutate the references of other objects, the interpreter must +continuously execute finalizers until it traverses (and calls +:c:func:`PyObject_CallFinalizer`) over all the referents without running a +finalizer. + +Possible psuedo-code: + +.. code-block:: python + + def visit_finalize(op: object, traversing: MutableSet[object]) -> int: + if id(op) in traversing: + return 0 + + traversing.add(id(op)) + PyObject_CallFinalizer(op) + was_called: int = finalizer_was_called(op) + + if _PyObject_IS_GC(op) and Py_TYPE(op).tp_traverse is not None: + if Py_TYPE(op).tp_traverse(op, visit_finalize, traversing) == 1: + return 1 + + return was_called + + + def finalize_immortal_referents() -> None: + traversing = set[int]() + ran_finalizer: bool = True + + while ran_finalizer: + ran_finalizer = False + + for immortal in iter_immortals(): + op: object = immortal.object + if immortal.gc_tracked and Py_TYPE(op).traverse is not None: + PyObject_GC_Track(op) + + if Py_TYPE(op).traverse(op, visit_finalize, traversing) and did_run_finalizer(): + ran_finalizer = True + + PyObject_GC_UnTrack(op) + + +Phase Two: Clearing and Destruction +----------------------------------- + +At this point, the second phase assumes that executing the +:c:member:`~PyTypeObject.tp_clear` or :c:member:`PyTypeObject.tp_dealloc` +of an immortal object doesn't run Python code, because all possible +:term:`strong references ` that be released should have +already been traversed and finalized by phase one. The interpreter may need +to finalize immortal referents again before running phase two to make sure +that this is true. + +Phase two should typically happen after all other objects in the interpreter +have been cleaned up. It is divided into two iterations: one to run the +``tp_clear`` slot if the immortal was originally GC-tracked, and the +``tp_dealloc`` in the second iteration. + +After phase two, the interpreter should not attempt to use an object +(other than for reference counting) that had been exposed to the user +in some way, because it could have been made immortal and is now mostly +deallocated (the pure object structures are still alive in memory, but all +remaining memory on an object has been freed). + +Deferred Memory Deletion +^^^^^^^^^^^^^^^^^^^^^^^^ + +Objects in Python are supposed to be under the "object" domain. Again, this is +already required on free-threaded builds. It's generally safe enough to assume +that all objects are using the object allocator, but this PEP also permits +use of the "memory" domain. See `Using the Object or Memory Domain is a Must`_. + +Python's allocators can be customized via :c:func:`PyMem_SetAllocator`, which +makes it possible for the interpreter to change how objects free their memory. +For immortal object deallocation, the interpreter needs to temporarily suspend +``free()`` operations on objects so reference cycles won't break things +This algorithm will be known as "deferred memory deletion". + +When deferred memory deletion is enabled, calling :c:func:`PyMem_Free`, +:c:func:`PyObject_Free`, or :c:func:`PyObject_GC_Del` will not free the +passed pointer, but instead store the pointer in a deferred memory bank +to be deleted later. The pointer will be kept as completely valid memory +until the deferred memory bank is cleared. + +Some objects also use internal freelist APIs instead of usual deallocators, +so those have to be disabled during deferred memory deallocation. Python's +internal freelists are cleared and stored in the deferred memory bank to be +restored later. + +After deferred memory deletion has been disabled and the pointers stored in +the deferred memory bank have been deleted, the interpreter must not try +to modify the reference count of any object visible to a user, as it could +have been made immortal and is now deleted. As such, the deferred memory bank +should be cleared at the very end of an interpreter's life. + +Preventing Reference Cycle UAFs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Before any destruction of immortal objects starts to happen, +`deferred memory deletion `_ needs to be enabled. +This is because the garbage collector doesn't manually call +:c:member:`~PyTypeObject.tp_dealloc`, but instead it relies on +:c:member:`~PyTypeObject.tp_clear` to get reference counts to zero. + +Unfortunately, the interpreter does not know the "real" number of references to +immortal objects (because there is no real reference count for an immortal +object), so deleting an immortal object arbitrarily could cause use-after-free +violations if immortal objects reference one another. +Deferred memory deletion will prevent this, because an immortal object's +reference count field will remain in valid memory and thus can still +be reference counted (but *not* used for any other purpose; that's why it was +finalized beforehand). + +For destruction, each immortal object is mortalized (to a reference count of +zero), and then their ``tp_dealloc`` slot is triggered. Once again, deferred +memory deletion will prevent the object structure from getting freed, so it's +safe to reference count an immortal object postmortem. + +After an immortal object's destructor is run, it will be immortalized once more +in case other destructors try to release a reference to it. +The immortal objects will remain with an immortal reference count for the rest +of the interpreter, and will only be finally deleted in the third and final +phase of immortal object deallocation. + +Psuedo-code for phase two: + +.. code-block:: python + + def destruct_immortals() -> None: + enable_deferred_memory_deletion() + for immortal in iter_immortals(): + op: object = immortal.object + + if immortal.gc_tracked: + PyObject_GC_Track(op) + + if immortal.gc_tracked and Py_TYPE(op).clear is not None: + Py_TYPE(op).clear(op) + + for immortal in iter_immortals(interp.runtime_immortals.values): + op: object = immortal.object + + _Py_SetMortal(op, 1) + Py_DECREF(op) + _Py_SetImmortalKnown(op) + + disable_deferred_memory_deletion() + +Phase Three: Deletion +--------------------- + +Deletion is the simplest phase of immortal object deallocation. +The deferred memory is deleted and the immortal objects have been finally +released. Once again, after this phase, it is unsafe for the interpreter to +reference count any object that could have been exposed to the user, +because if it was immortal, it will now be freed. + +The psuedo-code is simple: + +.. code-block:: python + + def delete_immortals() -> None: + delete_deferred_memory() + +Backwards Compatibility +======================= + +The `Immortalization Contract`_ is very slightly backwards-incompatible. +The contract is based on best practices in the C API; it doesn't have +anything that's really new, it's just based around what objects currently +do. That being said, there was nothing enforcing before that objects follow +these assumptions, but the authors of this PEP have never seen code in the +wild that breaks the immortalization contract. + +In addition, this PEP only exposes an opt-in feature; types that don't support +immortalization will continue to work as long as they are not immortalized. + +What if CPython Removes Immortality? +------------------------------------ + +The APIs in this PEP are exposed as unstable, allowing CPython to +remove them later on if it needs to. + +Security Implications +===================== + +The main issue with immortality is that, well, the objects are immortal. +They don't get deallocated, at least not until the interpreter is shutting +down. That means for applications that make a lot of things immortal will +have higher memory usage. Even worse, applications that don't ever shut down +(such as web servers) will essentially leak the objects, so making something +immortal can possibly be an attack vector depending on where it's used. + +How to Teach This +================= + +:c:func:`PyUnstable_Immortalize` will be documented in the C API documentation +and :func:`sys._immortalize` in the :mod:`sys` documentation. They will both +contain warnings about the increased memory usage that results from immortalizing +too many objects. + +Reference Implementation +======================== + +A reference implementation of this PEP can be found +`here `_. + +Rejected Ideas +============== + +This PEP currently has no rejected ideas. + +Open Issues +=========== + +This PEP currently has no open issues. + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From 97a569e3ce64974220b4b18da450723e66c04b7f Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 17 Jul 2025 05:55:37 -0400 Subject: [PATCH 2/8] Fix Sphinx warnings. --- peps/pep-0797.rst | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index 54e21c452bb..a8688958676 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -157,7 +157,7 @@ or compromise compatibility with the stable ABI. Immortality is a clever way of fixing this problem, because by avoiding reference counting, there is no risk of data races. In fact, both subinterpreters and free-threading already use immortality for thread-safety -on some common objects, such as :const:`None`, :const:`True`, or :const:`False`. +on some common objects, such as ``None``, ``True``, or ``False``. Lastly, immortality is also faster than deferred reference counting (:c:func:`PyUnstable_Object_EnableDeferredRefcount`), which is used for @@ -247,12 +247,12 @@ This PEP introduces two new semi-public APIs for making an object immortal: collected type. * All finalization for an object must be done in :c:member:`~PyTypeObject.tp_finalize` by - :c:function:`PyObject_CallFinalizerFromDealloc`. + :c:func:`PyObject_CallFinalizerFromDealloc`. .. warning:: Abuse of this function can result in high memory usage for a program. -.. function:: _immortalize(obj) +.. function:: sys._immortalize(obj) Make *obj* an :term:`immortal` object. This means that it will not be deallocated for the lifetime of the Python interpreter. @@ -267,8 +267,6 @@ This PEP introduces two new semi-public APIs for making an object immortal: This function should be considered a low-level routine that most users should avoid. Making too many objects immortal can result in drastically higher memory usage for a program. - .. impl-detail:: This function is specific to CPython. - Immortalization Contract ************************ @@ -332,7 +330,7 @@ Using the Object or Memory Domain is a Must ------------------------------------------- For an object immortal on the heap, the object domain (:c:func:`PyObject_Malloc` -or :c:func:`PyObject_GC_New`) or the memory domain (:c:func:`PyMem_Malloc`) +or :c:macro:`PyObject_New`) or the memory domain (:c:func:`PyMem_Malloc`) must be used. Otherwise, the interpreter will not be able to hijack the object's allocator and will result in use-after-free violations during deallocation. Note that in 3.13, using the object domain is already a @@ -389,11 +387,11 @@ Temporary Mortalization for Finalizers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ It is documented (and expected with assertions) that finalizers are run with -a reference count of 1 [#tp_dealloc_refcount]_, so each immortal object needs -to be temporarily mortalized; this can be done with the private ``_Py_SetMortal`` -API that currently exists. After the finalizer is done, it must be -immortalized again so other finalizers don't accidentally deallocate it in -their own finalizers by releasing a reference. +a reference count of 1, so each immortal object needs to be temporarily +mortalized; this can be done with the private ``_Py_SetMortal`` API +that currently exists. After the finalizer is done, it must be immortalized +again so other finalizers don't accidentally deallocate it in their own +finalizers by releasing a reference. In addition, an object might expect that its finalizer only be called by the garbage collector if the type doesn't call @@ -615,7 +613,7 @@ In addition, this PEP only exposes an opt-in feature; types that don't support immortalization will continue to work as long as they are not immortalized. What if CPython Removes Immortality? ------------------------------------- +************************************ The APIs in this PEP are exposed as unstable, allowing CPython to remove them later on if it needs to. From 79a7977cc71b13b172ef3b49ff738bc178362ecc Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 17 Jul 2025 06:00:12 -0400 Subject: [PATCH 3/8] Some typo fixes. --- peps/pep-0797.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index a8688958676..a6a23bf8281 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -175,10 +175,6 @@ In doing so, an object will release its memory back to CPython's object allocato allocator is per-interpreter, so it is unsafe to try to release an object's memory in an interpreter different from the one that created it. -Rationale -========= - - Specification ============= @@ -419,8 +415,6 @@ The psuedo-code looks like this: Identifying Immortal Referents ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In some cases, a - To find the mortal objects that might want to reference an immortal, we have to make a reasonable assumption about how garbage collection works: in order for an object to :c:func:`Py_DECREF` another object in its destructor, it From ccf9056f69a35afc2ac09914e376441548491f68 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 17 Jul 2025 06:01:15 -0400 Subject: [PATCH 4/8] Another typo. --- peps/pep-0797.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index a6a23bf8281..429f95f3fe7 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -372,7 +372,7 @@ three iterations over the full list of user-defined immortal objects, or "phases Phase One: Finalization ----------------------- -The initial problem to solv ewith deallocating immortal objects is that they +The initial problem to solve with deallocating immortal objects is that they can run finalizers, which might reference other immortal objects. Imagine a case where there are two immortal objects: A, and B. Object B has a finalizer that references object A, and object A has a finalizer that references object From 09d9880f07c91d1a69b1ea9504ef887f53c59ed7 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Thu, 17 Jul 2025 06:23:22 -0400 Subject: [PATCH 5/8] Add a new section and fix links. --- peps/pep-0797.rst | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index 429f95f3fe7..c4a48c59105 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -105,10 +105,10 @@ other areas of CPython, or in third-party libraries: (:c:func:`PyUnstable_Object_EnableDeferredRefcount`), but that has the downside of damaging single-threaded performance. - Immortality can help memory usage by avoiding copy-on-write operations in - child processes. Instagram has been `doing this in Cinder `_ - for years, and it would be nice to let CPython also take advantage of it. + child processes. Instagram has been `doing this in Cinder`_ for years, + and it would be nice to let CPython also take advantage of it. -.. _pep-797-cinder: https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/ +.. _doing this in Cinder: https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/ Motivation ========== @@ -119,13 +119,12 @@ Immortal Objects Must Remain in CPython In the past, the main pushback to exposing APIs for immortality is that it's exposing an implementation detail, and would force CPython to keep immortality forever. Unfortunately, we've already reached that point: too -many users, and `CPython itself <_pep-797-omitting-immortals>`_, omit -reference counting for known immortal objects, such as :c:data:`Py_None`. -Since there's no good way to deprecate that, CPython will always need some -form of immortality to retain compatibility. That said, this proposal still -keeps all new APIs as unstable. +many users, and `CPython itself`_, omit reference counting for known immortal +objects, such as :c:data:`Py_None`. Since there's no good way to deprecate +that, CPython will always need some form of immortality to retain +compatibility. That said, this proposal still keeps all new APIs unstable. -.. _pep-797-omitting-immortals: https://github.com/python/cpython/issues/103906 +.. _CPython itself: https://github.com/python/cpython/issues/103906 Objects Cannot be Directly Shared Between Interpreters ****************************************************** @@ -175,6 +174,17 @@ In doing so, an object will release its memory back to CPython's object allocato allocator is per-interpreter, so it is unsafe to try to release an object's memory in an interpreter different from the one that created it. + +Immortality is a Powerful Tool to Wield +*************************************** + +In general, immortal objects seem to have a lot of applications that aren't +immediately clear. For example, `Nanobind used immortal objects`_ +in its original :term:`free threading` implementation. It seems like it would +have interesting use-cases for many places outside of CPython. + +.. _Nanobind used immortal objects: https://github.com/wjakob/nanobind/pull/695 + Specification ============= @@ -233,7 +243,7 @@ This PEP introduces two new semi-public APIs for making an object immortal: it's very unlikely that a type doesn't follow it: * The object must be allocated under either the "object" or "memory" allocator - domains. See :ref:`Allocator Domains ` for more information. + domains. See :ref:`Allocator Domains ` for more information. Default values of :c:member:`~PyTypeObject.tp_alloc` always use the object allocator. * All :term:`strong references ` released in the object's @@ -289,7 +299,7 @@ or in tests) will now break if they are made immortal, because the reference count of an object will unexpectedly not change. In one of the 3.14 alphas, this rule about reference counting was -`brought up `_ for clarification by a user of the +`brought up`_ for clarification by a user of the limited C API. From that post, it's clear that reference count numbers are an implementation detail: @@ -303,7 +313,7 @@ that reference counts are unstable: well-defined values; the number of references to an object, and how that number is affected by Python code, may be different between versions. -.. _pep-797-limited-refcount: https://discuss.python.org/t/72006 +.. _brought up: https://discuss.python.org/t/72006 Destructors Must Use ``tp_finalize`` ------------------------------------ @@ -528,7 +538,7 @@ Preventing Reference Cycle UAFs ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Before any destruction of immortal objects starts to happen, -`deferred memory deletion `_ needs to be enabled. +deferred memory deletion needs to be enabled. This is because the garbage collector doesn't manually call :c:member:`~PyTypeObject.tp_dealloc`, but instead it relies on :c:member:`~PyTypeObject.tp_clear` to get reference counts to zero. From 6e1f1ade513c302a5850bf6efddc8e655596f7d9 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 18 Jul 2025 11:47:27 -0400 Subject: [PATCH 6/8] Improve the abstract. --- peps/pep-0797.rst | 46 ++++++++++++++-------------------------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index c4a48c59105..d1bb74a9cd7 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -38,21 +38,18 @@ For example: .. code-block:: C static PyObject * - my_method(PyObject *self, PyObject *object) // METH_O + my_singleton_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) { - if (PyUnstable_Immortalize(object) < 0) { + PyObject *self = type->tp_alloc(type, 0); + if (self == NULL) { return NULL; } - /* The object is now immortal, we can do whatever we want with it's - reference count without worrying about thread safety or leaks. */ - - // This is totally safe for an immortal object - for (int i = 0; i < 10; ++i) { - Py_DECREF(object); + if (PyUnstable_Immortalize(self) < 0) { + return NULL; } - Py_RETURN_NONE; + return self; } Or, in Python: @@ -61,25 +58,12 @@ Or, in Python: import sys - my_own_singleton = object() - sys._immortalize(my_own_singleton) - assert sys._is_immortal(my_own_singleton) - -What Do We Mean By "Immortal"? -****************************** - -By immortal, we mean an object with an immortal reference count, per :pep:`683`. -However, the main difference in this PEP is that the underlying behavior differs -from the documentation, which states that the object is *never* deallocated: + my_singleton = object() + sys._immortalize(my_singleton) + assert sys._is_immortal(my_singleton) - Some objects are immortal and have reference counts that are never - modified, and therefore the objects are never deallocated. - -In this case, immortal objects are eventually deallocated, it's just that the user -never really sees when that happens because they're freed during interpreter -finalization. So, the term "immortal" throughout this PEP really means -"never deallocated for the lifetime of the interpreter," rather than -"never deallocated." +Motivation +========== What Problem Does Immortality Solve? ************************************ @@ -110,8 +94,6 @@ other areas of CPython, or in third-party libraries: .. _doing this in Cinder: https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/ -Motivation -========== Immortal Objects Must Remain in CPython *************************************** @@ -179,9 +161,9 @@ Immortality is a Powerful Tool to Wield *************************************** In general, immortal objects seem to have a lot of applications that aren't -immediately clear. For example, `Nanobind used immortal objects`_ -in its original :term:`free threading` implementation. It seems like it would -have interesting use-cases for many places outside of CPython. +immediately clear to CPython maintainers. For example, `Nanobind used immortal objects`_ +in its original :term:`free threading` implementation. So, it seems like immortality +has interesting use-cases in third-party libraries. .. _Nanobind used immortal objects: https://github.com/wjakob/nanobind/pull/695 From e8cdb33fbb5265c66a4cd56fbf43925b64c3074d Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 18 Jul 2025 11:58:40 -0400 Subject: [PATCH 7/8] Further improve the abstract. --- peps/pep-0797.rst | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index d1bb74a9cd7..c25cb8f4e80 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -12,23 +12,12 @@ Post-History: `01-Jul-2025 `__ Abstract ======== -:pep:`683` introduced :term:`immortal` objects into CPython, using an "immortal -reference count" where the reference count field is never modified on the -object. Currently, all APIs for enabling immortality on a given object are -private, and even those APIs aren't fully safe for immortalizing *any* object. - -In particular, the private API has ``_Py_SetImmortal``, which simply untracks a -passed object from the garbage collector, and then sets its reference count to -the magic immortal number. But again, this isn't totally safe; for example, due -to the current string interning implementation, immortalizing a string can cause -crashes. To make matters worse, there's no mechanism in place to deallocate an -object once it has become immortal; objects on the heap are leaked, which is bad -for per-interpreter state. - This PEP aims to introduce a public way to immortalize arbitrary objects, while -making sure the object is deallocated later (specifically, during interpreter +making sure the object is deallocated later (during interpreter finalization). This is done by introducing two new APIs: :func:`sys._immortalize` -on the Python side, and :c:func:`PyUnstable_Immortalize` on the C side. +on the Python side, and :c:func:`PyUnstable_Immortalize` on the C side. These +are generally considered to be low-level APIs that won't be needed by users; the +main beneficiary will be CPython. Both of these functions make the given object immortal, and then deallocate it when the interpreter is shutting down using a simulated garbage collection. @@ -62,9 +51,33 @@ Or, in Python: sys._immortalize(my_singleton) assert sys._is_immortal(my_singleton) +Running either of these examples on a Python debug build with :option:`-X showrefcount <-X>` +show that the (heap-allocated) immortal object is not leaked: + +.. code-block:: bash + + $ ./python -Xshowrefcount -c "import sys; sys._immortalize(object())" + [0 refs, 0 blocks] + Motivation ========== +It's Currently Impossible to Immortalize General Objects +******************************************************** + +:pep:`683` introduced :term:`immortal` objects into CPython, using an "immortal +reference count" where the reference count field is never modified on the +object. Currently, all APIs for enabling immortality on a given object are +private, and even those APIs aren't fully safe for immortalizing *any* object. + +In particular, the private API has ``_Py_SetImmortal``, which simply untracks a +passed object from the garbage collector, and then sets its reference count to +the magic immortal number. But again, this isn't totally safe; for example, due +to the current string interning implementation, immortalizing a string can cause +crashes. To make matters worse, there's no mechanism in place to deallocate an +object once it has become immortal; this is limiting for potential optimizations +using immortality. + What Problem Does Immortality Solve? ************************************ From 25d23d260ab1d0003b3b8054103f1292669a5f42 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Fri, 18 Jul 2025 12:16:14 -0400 Subject: [PATCH 8/8] Some minor edits. --- peps/pep-0797.rst | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/peps/pep-0797.rst b/peps/pep-0797.rst index c25cb8f4e80..ee70a585d97 100644 --- a/peps/pep-0797.rst +++ b/peps/pep-0797.rst @@ -76,19 +76,20 @@ the magic immortal number. But again, this isn't totally safe; for example, due to the current string interning implementation, immortalizing a string can cause crashes. To make matters worse, there's no mechanism in place to deallocate an object once it has become immortal; this is limiting for potential optimizations -using immortality. +using immortality, because immortal objects stored on the heap (and the memory +that they own) are leaked. What Problem Does Immortality Solve? ************************************ Currently, the main way to share objects between subinterpreters is via serialization. This works OK, but can be limited for many objects. -Immortality is a good precursor to directly sharing objects across multiple +Immortality is a necessary precursor to directly sharing objects across multiple interpreters, as it provides a way to make reference counting thread-safe without atomic operations on the reference count field (which has proven to drastically hurt performance). -Subinterpreters aside, immortality is just generally a powerful tool to wield, +Subinterpreters aside, immortality is just generally a powerful tool, especially for CPython maintainers. The authors of this PEP do not expect that immortality will become a common tool for Python code, but the complexity of the approach on its own warrants a PEP. @@ -100,10 +101,12 @@ other areas of CPython, or in third-party libraries: build are still subject to reference count contention. It is possible to help mitigate this problem through deferred reference counting in the C API (:c:func:`PyUnstable_Object_EnableDeferredRefcount`), but that has the downside - of damaging single-threaded performance. + of damaging single-threaded performance. Immortality is able to avoid reference + count contention while also keeping good single-threaded performance. - Immortality can help memory usage by avoiding copy-on-write operations in child processes. Instagram has been `doing this in Cinder`_ for years, - and it would be nice to let CPython also take advantage of it. + and it would be nice to let CPython also take advantage of it. This is + mainly an incidental goal of this PEP. .. _doing this in Cinder: https://engineering.fb.com/2023/08/15/developer-tools/immortal-objects-for-python-instagram-meta/ @@ -113,10 +116,10 @@ Immortal Objects Must Remain in CPython In the past, the main pushback to exposing APIs for immortality is that it's exposing an implementation detail, and would force CPython to keep -immortality forever. Unfortunately, we've already reached that point: too -many users, and `CPython itself`_, omit reference counting for known immortal -objects, such as :c:data:`Py_None`. Since there's no good way to deprecate -that, CPython will always need some form of immortality to retain +immortality forever. Unfortunately, it's likely that point has already been +reached: too many users, and `CPython itself`_, omit reference counting for +known immortal objects, such as :c:data:`Py_None`. Since there's no good way +to deprecate that, CPython will always need some form of immortality to retain compatibility. That said, this proposal still keeps all new APIs unstable. .. _CPython itself: https://github.com/python/cpython/issues/103906 @@ -132,8 +135,9 @@ which will be discussed further in a moment. As of writing, the rule is that you cannot modify the reference count of an object created in a different interpreter, and since you can't modify the reference count, you can't do anything with the object. Since immortality prevents -reference counting operations, this PEP hopes to solve that issue, allowing -for an object proxy that can safely be reference counted in all interpreters. +reference counting operations, this PEP will solve that issue, allowing +for an object proxy that can safely be used with reference counting APIs +in all interpreters. Reference Count Modifications Must be Per-interpreter ----------------------------------------------------- @@ -143,7 +147,7 @@ by the interpreter's GIL, meaning that trying to modify the reference count of an object that belongs to another interpreter could cause data races. This isn't a problem on the free-threaded build as reference count operations -are atomic, but subinterpreters are supported on the with-GIL build. An +are atomic, but subinterpreters are supported on the GIL-enabled build. An alternative option could be to make reference counting also atomic on the GIL-enabled build, but that has been shown to damage single-threaded performance or compromise compatibility with the stable ABI. @@ -169,6 +173,16 @@ In doing so, an object will release its memory back to CPython's object allocato allocator is per-interpreter, so it is unsafe to try to release an object's memory in an interpreter different from the one that created it. +Immortality Has No Impact on Reference Counting Performance +----------------------------------------------------------- + +If the primary issue with cross-interpreter reference counting is thread-safety, +why not make reference counting atomic? Unfortunately, atomic operations are +slower than their non-atomic counterparts, so this would hurt overalll +performance. :pep:`703` managed to solve this issue using biased reference +counting, splitting the reference count field into two, but at the cost of +breaking the stable C API. Atomic reference counting on its own also doesn't +solve the per-interpreter deallocator issue as mentioned previously. Immortality is a Powerful Tool to Wield *************************************** @@ -604,7 +618,7 @@ Backwards Compatibility The `Immortalization Contract`_ is very slightly backwards-incompatible. The contract is based on best practices in the C API; it doesn't have anything that's really new, it's just based around what objects currently -do. That being said, there was nothing enforcing before that objects follow +do. That being said, there was nothing previously enforcing that objects follow these assumptions, but the authors of this PEP have never seen code in the wild that breaks the immortalization contract.