Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
22df1e0
Fix bug from bad interaction with incremental GC.
mjp41 Jul 14, 2025
63c79e4
Ownership: Add debugging invariant with `--with-ownership-invariant`
xFrednet Jun 30, 2025
5882ca6
Review comments <3
xFrednet Jul 17, 2025
6b4f7c0
sudo CI=green
xFrednet Jul 17, 2025
0aa1f51
Ownership: Extract common ownership functionality for reuse
xFrednet Jul 18, 2025
6e416a1
Ownership: Extract object graph traversal for reuse
xFrednet Jul 21, 2025
18c011a
Ownership: Add regions
xFrednet Jul 21, 2025
5fc5e7d
Ownership: Add to region
xFrednet Jul 22, 2025
35d4763
Ownership: Plan in TODOs
xFrednet Jul 22, 2025
eedc0bb
Ownership: expand invariant for regions
xFrednet Jul 22, 2025
545fc07
Merge branch 'regions-base' into regions-main
xFrednet Jul 23, 2025
a1837e2
Small Fixes
xFrednet Jul 23, 2025
11e607d
Ownership: New regions module
xFrednet Jul 24, 2025
1fa8dfc
Ownership: The first regions and exceptions...
xFrednet Jul 28, 2025
68c8dc9
Ownership: This is a .. journey
xFrednet Jul 29, 2025
1f7ec83
Ownership: Bugfixes!!
xFrednet Jul 29, 2025
745cd93
Ownership: _PyRegion_AddRefs
xFrednet Jul 30, 2025
ddb4570
Ownership: Start writing Tests
xFrednet Jul 30, 2025
56c481c
More tests
xFrednet Jul 30, 2025
dda5863
Ownership more tests
xFrednet Jul 31, 2025
c8d0b9a
Progress and interesting bugs
xFrednet Jul 31, 2025
0404feb
Skybreaker!!!!
xFrednet Aug 1, 2025
529595a
Small bug fixes, progress and work on the wekend
xFrednet Aug 3, 2025
e224e57
Cleanup and exception'al problems
xFrednet Aug 3, 2025
86685c4
Actual text output o.O
xFrednet Sep 9, 2025
6e2828d
I believe staging region references works now?
xFrednet Sep 25, 2025
83f6574
How is the bug still there
xFrednet Oct 7, 2025
bce3766
Adding more write barriers
xFrednet Oct 8, 2025
8122f98
Try close backend (There is no way this works)
xFrednet Oct 8, 2025
73030fa
Try close testing and debugging one thing at a time
xFrednet Oct 9, 2025
c63b80b
Regions: From asserts to seg faults
xFrednet Oct 10, 2025
af68d36
Regions: Cleaning works
xFrednet Oct 14, 2025
486ddad
Regions: Debugging and dealloc/clear routines
xFrednet Oct 16, 2025
3f47e90
Region: New BridgeObject to access fields in internal regions,c
xFrednet Oct 16, 2025
86ffb76
Region: Move name into bridge object data
xFrednet Oct 17, 2025
13364e7
Regions: Clear region on dealloc to allow region dealloc
xFrednet Oct 17, 2025
7466064
Regions: Planning for next week
xFrednet Oct 17, 2025
c2c9b34
Region: extract objects from GC and track in Region
xFrednet Oct 24, 2025
20010e9
Region: Notes on cowns
xFrednet Oct 25, 2025
423e3f3
Minor fixes to make it compile with GCC
xFrednet Nov 5, 2025
3e5f655
More GCC fixes
xFrednet Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Include/Python.h
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@
#include "cpython/pyfpe.h"
#include "cpython/tracemalloc.h"
#include "immutability.h"
#include "region.h"

// Restore warning filter
#ifdef _MSC_VER
Expand Down
5 changes: 0 additions & 5 deletions Include/internal/pycore_immutability.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@ extern "C" {
#endif

struct _Py_immutability_state {
PyObject *module_locks;
PyObject *blocking_on;
PyObject *freezable_types;
PyObject *destroy_cb;
#ifdef Py_DEBUG
PyObject *traceback_func; // For debugging purposes, can be NULL
#endif
};

#ifdef __cplusplus
Expand Down
2 changes: 2 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ extern "C" {
#include "pycore_immutability.h" // struct _immutability_runtime_state
#include "pycore_llist.h" // struct llist_node
#include "pycore_opcode_utils.h" // NUM_COMMON_CONSTANTS
#include "pycore_ownership.h" // struct _Py_ownership_state
#include "pycore_pymath.h" // _PY_SHORT_FLOAT_REPR
#include "pycore_structs.h" // PyHamtObject
#include "pycore_tstate.h" // _PyThreadStateImpl
Expand Down Expand Up @@ -937,6 +938,7 @@ struct _is {
struct _Py_exc_state exc_state;
struct _Py_immutability_state immutability;
struct _Py_mem_interp_free_queue mem_free_queue;
_Py_ownership_state ownership;

struct ast_state ast;
struct types_state types;
Expand Down
10 changes: 7 additions & 3 deletions Include/internal/pycore_object.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,24 @@ PyAPI_FUNC(int) _PyObject_IsFreed(PyObject *);
.ob_ref_local = _Py_IMMORTAL_REFCNT_LOCAL, \
.ob_flags = _Py_STATICALLY_ALLOCATED_FLAG, \
.ob_gc_bits = _PyGC_BITS_DEFERRED, \
.ob_type = (type) \
.ob_type = (type), \
.ob_region = _Py_LOCAL_REGION \
}
#else
#if SIZEOF_VOID_P > 4
#define _PyObject_HEAD_INIT(type) \
{ \
.ob_refcnt = _Py_IMMORTAL_INITIAL_REFCNT, \
.ob_flags = _Py_STATIC_FLAG_BITS, \
.ob_type = (type) \
.ob_type = (type), \
.ob_region = _Py_LOCAL_REGION \
}
#else
#define _PyObject_HEAD_INIT(type) \
{ \
.ob_refcnt = _Py_STATIC_IMMORTAL_INITIAL_REFCNT, \
.ob_type = (type) \
.ob_type = (type), \
.ob_region = _Py_LOCAL_REGION \
}
#endif
#endif
Expand Down Expand Up @@ -531,6 +534,7 @@ _PyObject_Init(PyObject *op, PyTypeObject *typeobj)
Py_SET_TYPE(op, typeobj);
assert(_PyType_HasFeature(typeobj, Py_TPFLAGS_HEAPTYPE) || _Py_IsImmortal(typeobj));
_Py_INCREF_TYPE(typeobj);
op->ob_region = _Py_LOCAL_REGION;
_Py_NewReference(op);
}

Expand Down
164 changes: 164 additions & 0 deletions Include/internal/pycore_ownership.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#ifndef Py_INTERNAL_OWNERSHIP_H
#define Py_INTERNAL_OWNERSHIP_H
#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_BUILD_CORE
# error "Py_BUILD_CORE must be defined to include this header"
#endif

#include "exports.h"
#include "object.h"

typedef struct _Py_ownership_state {
/* The global ownership tick used to mark open regions as dirty, if their
* invariant might broken. This can happen if untrusted C code is called
* which doesn't have write barriers. This C code might create references
* between objects which could violate the invariant. Marking a region as
* dirty means that it has to be cleaned, before the region can be closed.
*
* The tick has two kinds of values:
* - Even => A region was opened
* - Odd => Untrusted code was called and all currently open regions
* should be marked as dirty.
*
* Transitions by increment:
* - From even to odd => Unknown C code was called
* - From odd to even => A new region was opened
*
* This mechanism allows marking all regions as dirty with a single tick
* change.
*
* Invariant: The tick counter should always be greater or equal to two
* as the values 0 and 1 are reserved values by `_Py_region_data.open_tick`.
* */
Py_ssize_t tick;
// FIXME: xFrednet: Can we remove this special casing in favor of
// unfreezable fields or thread local wrappers.
PyObject *module_locks;
PyObject *blocking_on;
#ifdef Py_OWNERSHIP_INVARIANT
/* Tracks the state of the ownership invariant. Some ownership-related
* operations may temporarily violate the invariant. To handle this safely,
* the invariant must be suspended during such operations and only resumed
* once all of them complete. This is necessary to support re-entrancy.
*
* For example, during freezing, the object graph is traversed and objects
* are marked as immutable — even while they may still reference mutable
* objects. If the invariant were enforced mid-way, it would raise a
* (premature) error, despite the state being corrected as the operation
* completes. To avoid this, the invariant must be paused during the freeze.
*
* States:
* -1 => The invariant is disabled.
* 0 => The invariant is active and enforced.
* N => The invariant is temporarily paused. The value indicates the
* number of suspensions yet to be resumed (this supports nesting).
*/
int invariant_state;
#endif
#ifdef Py_DEBUG
/* Function to create a traceback object in debug builds. This is only used
* for debugging and can be NULL
*/
PyObject *traceback_func;
PyObject *location_key;
#endif
} _Py_ownership_state;

/* This retrives the current ownership tick or 0 if the tick retrival failed.
* See `_Py_ownership_state.tick`
*/
PyAPI_FUNC(Py_ssize_t) _PyOwnership_get_current_tick(void);

/* Returns the tick which should be used for `region.open_tick` or 0 if the
* ownerstate is currently unavialble.
*/
PyAPI_FUNC(Py_ssize_t) _PyOwnership_get_open_region_tick(void);

/* This function should be called when, untrusted code is executed. It will
* mark all currently open regions as dirty.
*
* It can fail, if the ownership state is currently unavailable
*/
PyAPI_FUNC(int) _PyOwnership_notify_untrusted_code(void);


PyAPI_FUNC(int) _PyOwnership_is_c_wrapper(PyObject *obj);

/* Called for every object, to check what should be done with it. This
* can be used to implemented a set visited objects and avoid traversing
* objects multiple times.
*
* The return value indicates success and if the object should be
* traversed. These are the return values:
* -1) Failure
* 0) Ok, but don't traverse the object
* 1) Ok, and traverse the object
*/
typedef int (*ownershipcheckproc)(PyObject* obj, void *state);

/* Like `visitproc` for `_PyOwnership_traverse_object_graph`. The first
* argument is the source of the reference and the second one is the
* referenced object.
*
* The return value indicates success and if the target object should be
* traversed. These are the return values:
* -1) Failure, stop traversal
* 0) Ok, but don't traverse the target object
* 1) Ok, and traverse the target object
*/
typedef int (*ownershipvisitproc)(PyObject* src, PyObject* tgt, void *state);

#define Py_OWNERSHIP_TRAVERSE_ERR -1
#define Py_OWNERSHIP_TRAVERSE_SKIP 0
#define Py_OWNERSHIP_TRAVERSE_VISIT 1

PyAPI_FUNC(int) _PyOwnership_traverse_object_graph(
PyObject *obj,
#ifdef Py_DEBUG
int freeze_location,
#endif
ownershipcheckproc caller_check,
ownershipvisitproc caller_visit,
void *caller_state
);

#ifdef Py_OWNERSHIP_INVARIANT

#include "object.h" // PyObject, visitproc
#include "pytypedefs.h" // PyThreadState

#define Py_OWNERSHIP_INVARIANT_DISABLED -1
#define Py_OWNERSHIP_INVARIANT_ENABLED 0

/* This function validates that the current heap follows the ownership
* rules. This is a slow operation and should only be done for debugging.
*
* 0 indicates a valid heap, -1 will be returned if an error was thrown.
*/
PyAPI_FUNC(int) _PyOwnership_check_invariant(PyThreadState *tstate);

PyAPI_FUNC(int) _PyOwnership_invariant_enable(void);
PyAPI_FUNC(int) _PyOwnership_invariant_pause(void);
PyAPI_FUNC(int) _PyOwnership_invariant_resume(void);
PyAPI_FUNC(int) _PyOwnership_invariant_disable(void);

typedef struct _Py_ownership_invariant_region_data {
Py_region_t next;
Py_ssize_t lrc;
Py_ssize_t osc;
} _Py_ownership_invariant_region_data;

#else
# define _PyOwnership_invariant_enable() 0 /* success */
# define _PyOwnership_invariant_pause() 0 /* success */
# define _PyOwnership_invariant_resume() 0 /* success */
# define _PyOwnership_invariant_disable() 0 /* success */
#endif

#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_OWNERSHIP_H */
146 changes: 146 additions & 0 deletions Include/internal/pycore_region.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#ifndef Py_INTERNAL_REGION_H
#define Py_INTERNAL_REGION_H
#ifdef __cplusplus
extern "C" {
#endif

#ifndef Py_BUILD_CORE
# error "Py_BUILD_CORE must be defined to include this header"
#endif

#include "object.h"
#include "region.h"
#include "pycore_ownership.h"
#include "pycore_gc.h" // PyGC_Head

/* Macros for readability */
#define NULL_REGION 0

/* PyObject_HEAD defines the initial segment of every PyObject used as a region bridge. */
#define PyBridgeObject_HEAD \
PyObject_HEAD; \
/* The region value which will be updated and \
* still filled when the dealloc function of \
* the object is called. \
*/ \
Py_region_t region; \
/** The name of the region or NULL */ \
PyObject *name;

#define PyBridgeObject_HEAD_INIT(op) \
op->region = NULL_REGION; \
op->name = NULL;

/**
* Objects used as bridges need to have an additional region field, which is
* still filled in the dealloc function. This should be the inital segment,
* similar to how `PyObject` is the inital segment for other objects.
*/
typedef struct _PyBridgeObject {
PyBridgeObject_HEAD;
} _PyBridgeObject;

#define _PyBridgeObject_CAST(op) _Py_CAST(_PyBridgeObject*, op)

typedef struct _Py_region_data {
/* The number of references coming in from the local region.
*
* This value should always be >= 0 with the exception of
* the `add_to_region` process. This can create a temporary
* region, which will be merged into the target region. The
* LRC can be negative, if the merge should decrease the LRC
* of the target region.
*/
Py_ssize_t lrc;

/* The number of open subregions. */
Py_ssize_t osc;

/* Snapshot of the ownership tick, when the region was opened. This
* is used to track if the region is open and if the region is clean.
*
* If the region is clean, it means the LRC and OSC can be trusted to
* securely close the region. However, these values might be incorrect,
* if the region is dirty. This can happen, when we call untrusted C
* code. A dirty region first has to be cleaned, before it can be closed.
*
* See `_Py_ownership_state.tick` for an explaination of the tick counter.
*
* This value indicates the following states:
* - (0) => The region is closed
* - (1) => The region is open and dirty
* - (N) if N == state.tick => The region is open and clean, since the
* ownership and open tick are the same
* - (N) if N != state.tick => The region is open but dirty, since an
* ownership tick was triggered.
*
* Invariant: The open tick should always be 1 or an even number.
*/
Py_ssize_t open_tick;

/* The number of references to this object */
Py_ssize_t rc;

/* A tagged pointer to the owner of this region. The tag indicates the
* type of owner and relationship:
*
* These are the possible tags:
* - 0b00 => The pointer points to the parent region (or is null)
* - 0b01 => The pointer points to the cown owing this region
* - 0b10 => The pointer points to the parent in the union-find forest
* - 0b11 => The pointer points to the parent in the union-fing forest, but the
* merge is not confirmed yet. Meaning references should not updated.
*
* Use the macros in `regions.c` to access these
*/
Py_uintptr_t owner;

/* The bridge object belonging to this _Py_region_data. This pointer can be
* NULL, when the bridge was already deallocated but some objects retain
* a reference to the `_Py_region_data` object.
*
* This is a weak reference to the brige, meaning the RC is not updated
* by writes to this field.
*/
_PyBridgeObject* bridge;

/* Objects have to be removed from their local GC cycle, when they're moved
* into a region. Instead they're moved into this list, to allow GC inside
* the region.
*
* Bridges can't form cycles with objects outside their regions (Mudolo cowns).
* It should therefore be safe to take them out of the GC cycle.
*/
PyGC_Head gc_list;

#ifdef Py_OWNERSHIP_INVARIANT
_Py_ownership_invariant_region_data invariant_data;
#endif
} _Py_region_data;

PyAPI_FUNC(int) _PyRegion_New(_PyBridgeObject *bridge);
PyAPI_FUNC(int) _PyRegion_Dissolve(Py_region_t region);
PyAPI_FUNC(void) _PyRegion_DecRc(Py_region_t region);

PyAPI_FUNC(Py_ssize_t) _PyRegion_GetLrc(Py_region_t region);
PyAPI_FUNC(Py_ssize_t) _PyRegion_GetOsc(Py_region_t region);
PyAPI_FUNC(int) _PyRegion_IsOpen(Py_region_t region);
PyAPI_FUNC(int) _PyRegion_IsDirty(Py_region_t region);
PyAPI_FUNC(int) _PyRegion_IsParent(Py_region_t child, Py_region_t parent);
PyAPI_FUNC(int) _PyRegion_ClosesWithLrc(Py_region_t region, Py_ssize_t lrc);
PyAPI_FUNC(Py_region_t) _PyRegion_GetParent(Py_region_t child);
PyAPI_FUNC(int) _PyRegion_Clean(Py_region_t region);

PyAPI_FUNC(int) _PyRegion_IsBridge(PyObject *obj);
PyAPI_FUNC(PyObject*) _PyRegion_GetBridge(Py_region_t region);
PyAPI_FUNC(void) _PyRegion_RemoveBridge(Py_region_t region);

PyAPI_FUNC(int) _PyRegion_SignalImmutable(PyObject *obj);
PyAPI_FUNC(void) _PyRegion_SignalDealloc(PyObject *obj);

PyAPI_FUNC(void) _PyRegion_HackDirtyForPrototype(Py_region_t region);

#ifdef __cplusplus
}
#endif
#endif /* !Py_INTERNAL_REGION_H */
Loading
Loading