Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

gh-127266: avoid data races when updating type slots v2 #133177

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
May 28, 2025
Merged
Changes from 1 commit
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
3094372
gh-127266: avoid data races when updating type slots
nascheme Apr 29, 2025
f447ce4
For update_all_slots(), do updates more safely.
nascheme Apr 29, 2025
d511ca6
Avoid "empty structure" compile error.
nascheme Apr 29, 2025
5e38497
Use apply_slot_updates() for type_setattro().
nascheme Apr 30, 2025
e9516c7
Merge 'origin/main' into type-slot-ts-v2
nascheme Apr 30, 2025
8c74a0c
Reduce number of items in test for slot updates.
nascheme Apr 30, 2025
6cd7644
Add TSAN suppression for _Py_slot_tp_getattr_hook.
nascheme Apr 30, 2025
3cb2256
Queue update of tp_flags as well.
nascheme Apr 30, 2025
47e41c9
Performance, skip stop-the-world when possible.
nascheme Apr 30, 2025
cb848f1
Merge 'origin/main' into type-slot-ts-v2
nascheme Apr 30, 2025
9859ebf
Always clear version after __bases__ update.
nascheme May 1, 2025
6c74cac
Merge 'origin/main' into type-slot-ts-v2
nascheme May 1, 2025
583c435
Add test for assigning __bases__.
nascheme May 1, 2025
c01707e
Avoid releasing TYPE_LOCK when stopping the world.
nascheme May 1, 2025
1b9cad5
Merge 'origin/main' into type-slot-ts-v2
nascheme May 5, 2025
a1c6b05
Add issue number for TSAN suppression.
nascheme May 5, 2025
3f6222b
Bug fix for type_lock_prevent_release().
nascheme May 5, 2025
6f218fb
Merge 'origin/main' into type-slot-ts-v2
nascheme May 8, 2025
2bcf7ba
Add additional assert.
nascheme May 27, 2025
ddfdbd5
Merge 'origin/main' into type-slot-ts-v2
nascheme May 27, 2025
63b7ae4
Revert test_opcache item size change.
nascheme May 27, 2025
1a2fee1
Add comment for new unit test.
nascheme May 27, 2025
c1f3ed5
Fix assert for default build.
nascheme May 27, 2025
41e54e1
Improve function name.
nascheme May 27, 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
Prev Previous commit
Next Next commit
For update_all_slots(), do updates more safely.
To avoid deadlocks while the world is stopped, we need to avoid calling APIs
like _PyObject_HashFast() and _PyDict_GetItemRef_KnownHash().  Collect the
slot updates to be done and then apply them all at once.  This reduces the
amount of code running in the stop-the-world condition.
  • Loading branch information
nascheme committed Apr 29, 2025
commit f447ce4adf750b9da15011e30eaf8aee3fd7b478
257 changes: 226 additions & 31 deletions 257 Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -1687,7 +1687,7 @@
static int add_all_subclasses(PyTypeObject *type, PyObject *bases);
static void remove_subclass(PyTypeObject *, PyTypeObject *);
static void remove_all_subclasses(PyTypeObject *type, PyObject *bases);
static void update_all_slots(PyTypeObject *);
static int update_all_slots(PyTypeObject *);

typedef int (*update_callback)(PyTypeObject *, void *);
static int update_subclasses(PyTypeObject *type, PyObject *attr_name,
Expand Down Expand Up @@ -1862,10 +1862,9 @@
add to all new_bases */
remove_all_subclasses(type, old_bases);
res = add_all_subclasses(type, new_bases);
types_stop_world();
update_all_slots(type);
types_start_world();
ASSERT_TYPE_LOCK_HELD();
if (update_all_slots(type) < 0) {
goto bail;
}
}
else {
res = 0;
Expand Down Expand Up @@ -3690,10 +3689,127 @@
}
}

#ifdef Py_GIL_DISABLED

// The structures and functions below are used in the free-threaded build
// to safely make updates to type slots, when __bases__ is re-assigned. Since
// the slots are read without atomic operations and without locking, we can
// only safely update them while the world is stopped. However, with the
// world stopped, we are very limited on which APIs can be safely used. For
// example, calling _PyObject_HashFast() or _PyDict_GetItemRef_KnownHash() are
// not safe and can potentially cause deadlocks. Hashing can be re-entrant
// and _PyDict_GetItemRef_KnownHash can acquire a lock if the dictionary is
// not owned by the current thread, to mark it shared on reading.
//
// We do the slot updates in two steps. First, with TYPE_LOCK held, we lookup
// the descriptor for each slot, for each subclass. We build a queue of
// updates to perform but don't actually update the type structures. After we
// are finished the lookups, we stop-the-world and apply all of the updates.
// The apply_slot_updates() code is simple and easy to confirm that it is
// safe.

typedef struct {
void **slot_ptr;
void *slot_value;
} slot_update_item_t;

// The number of slot updates performed is based on the number of changed
// slots and the number of subclasses. It's possible there are many updates
// required if there are many subclasses (potentially an unbounded amount).
// Usually the number of slot updates is small, most often zero or one. When
// running the unit tests, we don't exceed 20. The chunk size is set to
// handle the common case with a single chunk and to not require too many
// chunk allocations if there are many subclasses.
#define SLOT_UPDATE_CHUNK_SIZE 30

typedef struct _slot_update {
struct _slot_update *prev;
Py_ssize_t n;
slot_update_item_t updates[SLOT_UPDATE_CHUNK_SIZE];
} slot_update_chunk_t;

// a queue of updates to be performed
typedef struct {
slot_update_chunk_t *head;
} slot_update_t;

static slot_update_chunk_t *
slot_update_new_chunk(void)
{
slot_update_chunk_t *chunk = PyMem_Malloc(sizeof(slot_update_chunk_t));
if (chunk == NULL) {
PyErr_NoMemory();
return NULL;
}
chunk->prev = NULL;
chunk->n = 0;
return chunk;
}

static void
slot_update_free_chunks(slot_update_t *updates)
{
slot_update_chunk_t *chunk = updates->head;
while (chunk != NULL) {
slot_update_chunk_t *prev = chunk->prev;
PyMem_Free(chunk);
chunk = prev;
}
}

static int
queue_slot_update(slot_update_t *updates, void **slot_ptr, void *slot_value)
{
if (*slot_ptr == slot_value) {
return 0; // slot pointer not actually changed, don't queue update
}
if (updates->head == NULL || updates->head->n == SLOT_UPDATE_CHUNK_SIZE) {
slot_update_chunk_t *chunk = slot_update_new_chunk();
if (chunk == NULL) {
return -1; // out-of-memory
}
chunk->prev = updates->head;
updates->head = chunk;
}
slot_update_item_t *item = &updates->head->updates[updates->head->n];
item->slot_ptr = slot_ptr;
item->slot_value = slot_value;
updates->head->n++;
assert(updates->head->n <= SLOT_UPDATE_CHUNK_SIZE);
return 0;
}

static void
apply_slot_updates(slot_update_t *updates)
{
assert(types_world_is_stopped());
slot_update_chunk_t *chunk = updates->head;
while (chunk != NULL) {
for (Py_ssize_t i = 0; i < chunk->n; i++) {
slot_update_item_t *item = &chunk->updates[i];
*(item->slot_ptr) = item->slot_value;
}
chunk = chunk->prev;
}
}

#else

// not used, slot updates are applied immediately
typedef struct {} slot_update_t;

Check failure on line 3799 in Objects/typeobject.c

View workflow job for this annotation

GitHub Actions / Windows / Build and test (arm64)

C requires that a struct or union have at least one member [C:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

Check failure on line 3799 in Objects/typeobject.c

View workflow job for this annotation

GitHub Actions / Windows / Build and test (x64)

C requires that a struct or union have at least one member [D:\a\cpython\cpython\PCbuild\_freeze_module.vcxproj]

#endif

/// data passed to update_slots_callback()
typedef struct {
slot_update_t *queued_updates;
pytype_slotdef **defs;
} update_callback_data_t;

static void object_dealloc(PyObject *);
static PyObject *object_new(PyTypeObject *, PyObject *, PyObject *);
static int object_init(PyObject *, PyObject *, PyObject *);
static int update_slot(PyTypeObject *, PyObject *);
static int update_slot(PyTypeObject *, PyObject *, slot_update_t *update);
static void fixup_slot_dispatchers(PyTypeObject *);
static int type_new_set_names(PyTypeObject *);
static int type_new_init_subclass(PyTypeObject *, PyObject *);
Expand Down Expand Up @@ -6274,7 +6390,7 @@
if (is_dunder_name(name) && has_slotdef(name)) {
// The name corresponds to a type slot.
types_stop_world();
res = update_slot(type, name);
res = update_slot(type, name, NULL);
types_start_world();
ASSERT_TYPE_LOCK_HELD();
}
Expand Down Expand Up @@ -11254,13 +11370,22 @@
* There are some further special cases for specific slots, like supporting
* __hash__ = None for tp_hash and special code for tp_new.
*
* When done, return a pointer to the next slotdef with a different offset,
* because that's convenient for fixup_slot_dispatchers(). This function never
* sets an exception: if an internal error happens (unlikely), it's ignored. */
static pytype_slotdef *
update_one_slot(PyTypeObject *type, pytype_slotdef *p)
* When done, next_p is set to the next slotdef with a different offset,
* because that's convenient for fixup_slot_dispatchers().
*
* If the queued_updates pointer is provided, the actual updates to the slot
* pointers are queued, rather than being immediately performed. That argument
* is only used for the free-threaded build since those updates need to be
* done while the world is stopped.
*
* This function will only return an error if the queued_updates argument is
* provided and allocating memory for the queue fails. Other exceptions that
* occur internally are ignored, such as when looking up descriptors. */
static int
update_one_slot(PyTypeObject *type, pytype_slotdef *p, pytype_slotdef **next_p,
slot_update_t *queued_updates)
{
ASSERT_WORLD_STOPPED_OR_NEW_TYPE(type);
ASSERT_NEW_TYPE_OR_LOCKED(type);

PyObject *descr;
PyWrapperDescrObject *d;
Expand All @@ -11283,7 +11408,10 @@
do {
++p;
} while (p->offset == offset);
return p;
if (next_p != NULL) {
*next_p = p;
}
return 0;
}
/* We may end up clearing live exceptions below, so make sure it's ours. */
assert(!PyErr_Occurred());
Expand Down Expand Up @@ -11371,37 +11499,63 @@
}
Py_DECREF(descr);
} while ((++p)->offset == offset);
if (specific && !use_generic)
*ptr = specific;
else
*ptr = generic;
return p;

void *slot_value;
if (specific && !use_generic) {
slot_value = specific;
} else {
slot_value = generic;
}

#ifdef Py_GIL_DISABLED
if (queued_updates != NULL) {
// queue the update to perform later, while world is stopped
if (queue_slot_update(queued_updates, ptr, slot_value) < 0) {
return -1;
}
} else {
// do the update to the type structure now
*ptr = slot_value;
}
#else
// always do the update immediately
assert(queued_updates == NULL);
*ptr = slot_value;
#endif

if (next_p != NULL) {
*next_p = p;
}
return 0;
}

/* In the type, update the slots whose slotdefs are gathered in the pp array.
This is a callback for update_subclasses(). */
static int
update_slots_callback(PyTypeObject *type, void *data)
{
ASSERT_WORLD_STOPPED_OR_NEW_TYPE(type);
ASSERT_NEW_TYPE_OR_LOCKED(type);

pytype_slotdef **pp = (pytype_slotdef **)data;
update_callback_data_t *update_data = (update_callback_data_t *)data;
pytype_slotdef **pp = update_data->defs;
for (; *pp; pp++) {
update_one_slot(type, *pp);
if (update_one_slot(type, *pp, NULL, update_data->queued_updates) < 0) {
return -1;
}
}
return 0;
}

/* Update the slots after assignment to a class (type) attribute. */
static int
update_slot(PyTypeObject *type, PyObject *name)
update_slot(PyTypeObject *type, PyObject *name, slot_update_t *queued_updates)
{
pytype_slotdef *ptrs[MAX_EQUIV];
pytype_slotdef *p;
pytype_slotdef **pp;
int offset;

assert(types_world_is_stopped());
ASSERT_TYPE_LOCK_HELD();
assert(PyUnicode_CheckExact(name));
assert(PyUnicode_CHECK_INTERNED(name));

Expand All @@ -11425,8 +11579,12 @@
}
if (ptrs[0] == NULL)
return 0; /* Not an attribute that affects any slots */

update_callback_data_t callback_data;
callback_data.defs = ptrs;
callback_data.queued_updates = queued_updates;
return update_subclasses(type, name,
update_slots_callback, (void *)ptrs);
update_slots_callback, (void *)&callback_data);
}

/* Store the proper functions in the slot dispatches at class (type)
Expand All @@ -11437,27 +11595,64 @@
{
assert(!PyErr_Occurred());
for (pytype_slotdef *p = slotdefs; p->name; ) {
p = update_one_slot(type, p);
update_one_slot(type, p, &p, NULL);
}
}

#ifdef Py_GIL_DISABLED

// Called when __bases__ is re-assigned.
static void
static int
update_all_slots(PyTypeObject* type)
{
pytype_slotdef *p;
// Note that update_slot() can fail due to out-of-memory when allocating
// the queue chunks to hold the updates. That's unlikely since the number
// of updates is normally small but we handle that case. update_slot()
// can fail internally for other reasons (a lookup fails) but those
// errors are suppressed.
slot_update_t queued_updates = {0};
for (pytype_slotdef *p = slotdefs; p->name; p++) {
if (update_slot(type, p->name_strobj, &queued_updates) < 0) {
if (queued_updates.head) {
slot_update_free_chunks(&queued_updates);
}
return -1;
}
}
if (queued_updates.head != NULL) {
types_stop_world();
apply_slot_updates(&queued_updates);
types_start_world();
ASSERT_TYPE_LOCK_HELD();

assert(types_world_is_stopped());
slot_update_free_chunks(&queued_updates);

/* Clear the VALID_VERSION flag of 'type' and all its subclasses. */
type_modified_unlocked(type);
}
return 0;
}

#else

// Called when __bases__ is re-assigned.
static int
update_all_slots(PyTypeObject* type)
{
pytype_slotdef *p;

for (p = slotdefs; p->name; p++) {
/* update_slot returns int but can't actually fail */
update_slot(type, p->name_strobj);
/* update_slot returns int but can't actually fail in this case*/
update_slot(type, p->name_strobj, NULL);
}

/* Clear the VALID_VERSION flag of 'type' and all its subclasses. */
type_modified_unlocked(type);
return 0;
}

#endif


PyObject *
_PyType_GetSlotWrapperNames(void)
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.