Skip to content

Fix free-threaded use-after-free in dict structure comparison - #148

Open
harshitgavita-07 wants to merge 1 commit into
google-deepmind:masterfrom
harshitgavita-07:fix/free-threaded-dict-structure-compare
Open

harshitgavita-07 wants to merge 1 commit into
google-deepmind:masterfrom
harshitgavita-07:fix/free-threaded-dict-structure-compare

Conversation

@harshitgavita-07

Copy link
Copy Markdown

Summary

Fixes #143.

On free-threaded builds (pybind11 marks the module Py_MOD_GIL_NOT_USED), tree.assert_same_structure can dereference freed memory when another thread mutates a compared dict. Two borrowed-reference hazards in tree.cc:

  1. The dict key comparison in AssertSameStructureHelper iterated with PyDict_Next, which yields borrowed keys and is documented as unsafe against concurrent mutation. The PyDict_GetItem(o2, key) lookup can run Python __hash__/__eq__, during which a mutator thread can drop the key's last reference.
  2. DictValueIterator::next() used PyDict_GetItem (borrowed) followed by Py_INCREF - a mutator can free the element in the gap between the lookup and the incref.

Fixes:

  1. Iterate over a snapshot from PyDict_Keys(o1); the list holds strong references for the duration of the comparison. Works identically on all supported Python versions.
  2. Use PyDict_GetItemRef on Python >= 3.13, which returns a strong reference atomically; the legacy PyDict_GetItem + Py_INCREF path is kept for older versions.

Verification

Reproduced the reporter's reproducer (8 comparer threads calling assert_same_structure, 3 mutator threads replacing keys, str subclass keys with Python __hash__) on a stock python3.14.7t build (GIL disabled):

  • Before: SIGSEGV (reproduced immediately, and in 2 of 3 runs after fixing only site 1).
  • After both fixes: 5/5 clean runs, 32,000 comparisons each.
  • Full tree_test.py suite: 70/70 pass on 3.14t (free-threaded) and 70/70 pass on a GIL build (3.10, exercises the legacy #else path).

@google-cla

google-cla Bot commented Sep 30, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@harshitgavita-07

Copy link
Copy Markdown
Author

The Google CLA check is green now. This is ready for maintainer review.

The free-threaded crash reproducer passes 5/5 runs after fixing both borrowed-reference paths, and all 70 tests pass on both Python 3.14t and Python 3.10. Happy to adjust the patch based on review.

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.

Free-threaded (no-GIL) use-after-free: borrowed dict key from PyDict_Next used across __hash__/__eq__ in assert_same_structure

1 participant