Skip to content

[mypyc] Make attribute access memory safe on free-threaded builds#21705

Open
JukkaL wants to merge 14 commits into
masterfrom
mypyc-safe-attr
Open

[mypyc] Make attribute access memory safe on free-threaded builds#21705
JukkaL wants to merge 14 commits into
masterfrom
mypyc-safe-attr

Conversation

@JukkaL

@JukkaL JukkaL commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

This fixes memory unsafety when there is a race condition related to attribute get/set operations on a free-threaded build, for simple reference-based attributes only (PyObject *). This includes attributes with primitive types like list, set and str and dict, and native class types.

The main idea is to use delayed decref for the old attribute value when assigning to an attribute. There are detailed comments explaining the approach in detail.

This causes a ~5% slowdown in self check when using 3.14t. Currently it looks like this is unavoidable if we want to fix memory unsafety.

Remaining work includes fixing attributes with fixed-length tuple types and tagged integer types (in follow-up PRs).

I used coding agent assist heavily.

Work on mypyc/mypyc#1203.

@JukkaL JukkaL marked this pull request as draft July 8, 2026 17:22
@JukkaL

JukkaL commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

cc @msullivan

Comment thread mypyc/lib-rt/pythonsupport.h Outdated
Comment on lines +135 to +137
if (value != NULL) {
_PyObject_SetMaybeWeakref(value);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that this should not be required for correctness anymore. It looks like the original implementation of GetAttrRef had a loop on _Py_TryIncrefCompare, which requires that maybe-weakref be set in order to succeed, but the current version calls _Py_NewRefWithLock, which does not require that, so we should be good. (I think that change was made so that init wouldn't need to call _PyObject_SetMaybeWeakref, but it also means set shouldn't need to either.)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reasoning sounds correct, and removing the call reduced the self check regression by about 1% from ~6% to ~5%, which is nice. Done.

Comment thread mypyc/lib-rt/pythonsupport.c Outdated
if (_Py_TryIncrefCompare(field, v)) {
return v;
}
Py_BEGIN_CRITICAL_SECTION(self);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that the Py_BEGIN_CRITICAL_SECTION can possibly be doing anything useful here, since the writer doesn't take one.

The slow paths for list/dict reads that use Py_BEGIN_CRITICAL_SECTION rely on the write side taking a critical section.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this seems redundant. Removed.

Comment thread mypyc/lib-rt/pythonsupport.c Outdated
Comment on lines +29 to +31
if (_Py_TryIncrefCompare(field, v)) {
return v;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that we need to use _Py_TryIncrefCompare or do a re-load of the field, with the current implementation. I think we should be able to use just _Py_TryIncRefShared.

In particular, I think that the dict/list paths that rely on validating that the values haven't changed are a different code path than this one in which the objects do actually get freed but the allocator makes sure they don't get reused for different kinds of objects, so the refcounts stay refcounts.

We're doing a different approach based on _PyObject_XDecRefDelayed, so I don't think that the reloading-and-checking helps any.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like a good idea, though I'm not sure if the impact can be seen in microbenchmarks. Done.

@JukkaL

JukkaL commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

The simplified implementation still fixes all the memory safety issues I've been able to reproduce on master.

@msullivan msullivan left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good.

The one concern is that doing deferred decrefs is a behavioral change (stuff won't get freed eagerly), and I think it'll happen even on thread-local objects.

Not sure if there's any decent way to fix it without a lock though; I'll ponder about it some though

emitter.emit_line("{")
attr_expr = f"self->{attr_field}"

if IS_FREE_THREADED and is_simple_refcounted_pointer(rtype):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is done in the non is_simple_refcounted_pointer case?

Comment on lines +128 to +136
// Memory safety does NOT depend on SetMaybeWeakref here, so (unlike an earlier
// version) we do not call it. Two things keep this safe:
// - The old value is reclaimed via CPy_DecRefAttrOld, a QSBR-deferred decref, so
// it cannot be freed while an in-flight CPy_GetAttrRef still holds the stale
// pointer.
// - A concurrent cross-thread reader whose fast-path try-incref fails on an
// unflagged 'value' does not spin: CPy_GetAttrRef falls into its
// _Py_NewRefWithLock fallback (CPy_GetAttrRefSlow), which cannot fail and
// sets maybe-weakref lazily on that first read.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Classic claude talking about old code; I'd probably trim this up

@JukkaL JukkaL marked this pull request as ready for review July 10, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants