Skip to content

gh-159558: Avoid re-entering an already held critical section in ctypes setters - #159562

Open
johng wants to merge 4 commits into
python:mainfrom
johng:ft-ctypes-reentry
Open

johng wants to merge 4 commits into
python:mainfrom
johng:ft-ctypes-reentry

Conversation

@johng

@johng johng commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Fixing re-entering an already held lock in ctypes. This involves creating lock free version of implementation and also providing wrappers which also lock for callers that do not already hold a critical section.

Micro benchmark estimates.

Operation main this PR
c_int().value = 1 35 ns 22 ns
c_char_p().value = b"x" 39 ns 22 ns
pointer.contents = c_int(2) 86 ns 58 ns
Structure.i = 1 38 ns 24 ns
Structure.p = b"x" 69 ns 54 ns

johng added 4 commits October 16, 2026 18:23
…ree-threaded build

KeepRef() takes the critical section on its target so that it is safe
from any caller, and for a type that keeps a reference to the stored
object it then calls PyCData_GetContainer(), which takes the critical
section on the root object.  The Simple.value setter already holds the
lock on self, taken by its Argument Clinic wrapper, and for a top-level
object the root is self as well, so `c_char_p().value = b"x"` entered the
same critical section three times and `c_int().value = 1` twice (the
"nothing to keep" early return sits inside the locked region).

Add PyCData_GetContainer_lock_held() for a caller that holds the root's
lock, use it from KeepRef_lock_held() when the target is its own root,
and call KeepRef_lock_held() from the setter.

Measured on the free-threaded build: c_int().value = 1 35 -> 22 ns,
c_char_p().value = b"x" 39 -> 22 ns.

(cherry picked from commit 5ca398f)
…nts setter

The setter runs under a two-object critical section on the pointer and
the new target, then calls KeepRef() twice on the pointer and
GetKeepedObjects() on the target; both helpers lock their argument
again.  Call the lock-held variants instead, adding
GetKeepedObjects_lock_held() for the target.

Measured on the free-threaded build: p.contents = c_int(2) 86 -> 58 ns.

(cherry picked from commit 576731e)
PyCData_set() already requires its caller to hold the critical section
on the destination object and asserts it, but finished by calling
KeepRef(), which takes that lock again.  Every store of a Python object
into a Structure or Union field, an Array item or a Pointer item went
through it.  Call KeepRef_lock_held() instead.

Measured on the free-threaded build: Structure int field assignment
38 -> 24 ns, c_char_p field assignment 69 -> 54 ns.

(cherry picked from commit ead0976)
@bedevere-app bedevere-app Bot added the type-feature A feature request or enhancement label Oct 16, 2026
@picnixz
picnixz requested a review from ZeroIntensity October 16, 2026 17:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting review type-feature A feature request or enhancement

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant