Skip to content

Backport free-threading memory-safety fixes to 3.14 and 3.15 #153714

Description

@nascheme

Note: this work was assisted by AI agents (Claude Code and Codex).

This is a tracking issue for a batch of free-threading (Py_GIL_DISABLED) fixes that are in main but missing from the 3.14 and/or 3.15 maintenance branches. Most of them are use-after-free bugs, pointer tearing, or uninitialized reads: they can crash or silently corrupt data in a free-threaded build, and 3.14 is the first release where the free-threaded build is officially supported.

Every fix listed below has been backported, built, and tested on a branch already (see Branches). This issue is to agree on the set and to track the resulting PRs; the individual PRs are where the code review belongs.

Candidates

Missing from both 3.15 and 3.14

IssuePRmain commitSummarySeverity
gh-149816#150024d095ceb0f420UAF in pickle.dumps() when a list is mutated concurrentlyMedium
gh-144774#150578f586fd9e304eBaseException.__setstate__() locked the exception, not the state dict, leaving borrowed dict refs exposed to concurrent mutationLow
gh-150858#1508591ec6596828b0UAF reading a type's __qualname__ while another thread replaces itLow
gh-150411#15041312af26d17e43Atomic load for gc.get_count() young countVery low, no known crash
gh-151644#151768cde31ec13590Atomic sys.getdlopenflags() / sys.setdlopenflags()Very low, no known crash

The last two are one- and two-line atomics fixes with no known crash consequence; they are included because they are trivial, not because they are urgent.

Missing from 3.14 only (3.15 already has them)

Concurrent iteration safety in itertools, all under gh-123471. Most wrap the iterator state transition in an object critical section; cycle instead uses atomic index accesses plus PyList_GetItemRef().

IssuePRmain commitIterator(s)
gh-123471#13121226a1cd4e8c0ccycle
gh-123471#132814847d1c2cb401product, combinations
gh-123471#1356890533c1faf27dchain
gh-123471#144402009c8c052f5epermutations, combinations_with_replacement
gh-123471#1444863a2485644700accumulate
gh-123471#1460339214e3f33eeezip_longest

Other memory-safety fixes:

IssuePRmain commitSummarySeverity
gh-129069#142957e46f28c6afcePointer tearing and uninitialized reads in memmove-style list operationsMedium
gh-129069#143019487e91c1203dRelease stores for list reverse, insert, delete, extended-slice assignLow
gh-143100#143127e8e044eda343Pointer tearing from memcpy() while swapping set small tablesLow
gh-132070#14344198e55d70bcb7Atomic object-header init in free-threaded PyObject_Realloc()Medium

Deliberately excluded

Not backported here. Each needs its own decision.

Issue/PRmain commitWhy excluded
#14259908bc03ff2a55Atomic generator frame-state transitions; the commit says it prevents free-threaded segfaults, but it is a large change to generated interpreter files and does not apply cleanly to 3.14. Deserves a dedicated design review.
#144292a01694dacd8eProtects gi_yieldfrom with a frame-state lock; should be reviewed together with #142599.
#144980a56532771a9eCode-extra C APIs vs concurrent buffer growth/replacement. Medium-sized C API/QSBR change.
#143818bcf9cb0217fdOpenSSL races during concurrent SSLContext.load_cert_chain(). Medium-sized SSL locking change; a current 3.14 TSan backport explicitly notes this race is still unfixed in 3.14.
#1517668b1dbb175404gc.get_stats() race, described by its own regression test as benign at the Python level. The fix adds a GC statistics mutex and needs adaptation for 3.14's different stats layout. Not worth it.

Already backported upstream

Found by the audit, but landed in 3.14 in the meantime (the audit snapshot 8e648a9c6b29 predates the current tip). No action needed.

IssuePRNow in 3.14 as
gh-145142#145157d76c56e958c[3.14] gh-145142: Make str.maketrans safe under free-threading (#145320)
gh-148820#148852e5d55416833[3.14] gh-148820: Fix _PyRawMutex use-after-free on spurious semaphore wakeup (#148884)

Backport status

For each candidate, this lists the backport PR or the reason why a backport shouldn't be done.

3.15

IssueOriginal PRDescriptionBackport PR
gh-149816#150024pickle.dumps() UAFGH-153718
gh-144774#150578BaseException.__setstate__()GH-153746
gh-150858#150859type __qualname__GH-153747
gh-150411#150413gc.get_count()GH-153788
gh-151644#151768sys.get/setdlopenflags()GH-153790

3.14

IssueOriginal PRDescriptionBackport PR
gh-149816#150024pickle.dumps() UAF (adapted)GH-153719
gh-144774#150578BaseException.__setstate__()GH-153819
gh-150858#150859type __qualname__Excluded[1]
gh-150411#150413gc.get_count()GH-153787
gh-151644#151768sys.get/setdlopenflags()GH-153789
gh-123471#131212itertools.cycleGH-153791
gh-123471#132814itertools.product, combinationsGH-153791
gh-123471#135689itertools.chainGH-153791
gh-123471#144402itertools.permutations, combinations_with_replacementGH-153791
gh-123471#144486itertools.accumulateGH-153791
gh-123471#146033itertools.zip_longestGH-153791
gh-129069#142957list memmove tearingExcluded[2]
gh-129069#143019list release storesExcluded[2]
gh-143100#143127set memcpy tearingExcluded[2]
gh-132070#143441PyObject_Realloc() header initExcluded[2]
  1. Don't backport, 3.14 has a number of thread-safety issues inside typeobject and fixes are not simple to backport.
  2. Don't backport, non-trivial to do and not serious enough.

How the candidates were found

The audit was done entirely from local git refs, with no GitHub queries, so it is reproducible offline against the exact commits listed under Commit ranges.

  1. Screen two disjoint commit ranges covering everything in main that 3.14 does not have (5,389 commits total, see below).

  2. Filter commit subjects, bodies, NEWS entries, and diffs for:

    • free-threading, free-threaded, Py_GIL_DISABLED, nogil, standalone FT
    • safe, thread-safe, thread safety
    • race, racing, concurrent
    • UAF, use-after-free, segfault, crash, deadlock
    • any commit touching Lib/test/test_free_threading/
    • any diff adding critical sections, atomics, mutexes, or free-threading guards
  3. Rank by severity. The bar for "recommended" is a memory-safety consequence: crash, UAF, refcount/memory corruption, or torn pointer reads. Fixes that are merely correctness or performance improvements were screened out unless they were also small and low-risk.

  4. Check what is already backported, using source commit hashes quoted in maintenance-branch commit messages, PR/issue IDs in subjects and bodies, git cherry patch-equivalence (git cherry origin/3.15 origin/main, likewise for 3.14), and manual inspection where the code had diverged.

  5. Check applicability in a throwaway worktree per candidate:

    git diff COMMIT^ COMMIT | git -C WORKTREE apply --check -
    git diff --binary COMMIT^ COMMIT | git -C WORKTREE apply --3way --check -

    Note that --3way --check passing says nothing about semantic correctness, which is why every
    backport was then built and run against the full test suite.

A first pass only searched commits after the 3.15 fork point, which missed fixes that 3.15 inherited before the fork but that 3.14 never got (e.g. #146033, itertools.zip_longest). The range was widened to catch those. Anyone repeating this exercise should be careful about the same trap: the 3.14-only candidates and the "missing from both" candidates live in different commit ranges.

Commit ranges

Refs as of the audit:

RefCommit
origin/main6d5908368dc7936802d62413344dd4596abc4159
origin/3.15dc448f3d1a2bfc6ea18e3b5a87f5bd9eb1023964
origin/3.148e648a9c6b29c65ac6e290d6453cd69cedb3b1b4
3.15 fork pointf5c75351def83602b5b23c1fba361b7de8ffabc7
3.14/main merge baseb092705907c758d4f9742028652c9802f9f03dd3 (Python 3.14.0b1)

The two ranges screened:

# 969 commits: post-3.15-fork work, i.e. missing from BOTH 3.15 and 3.14
git log f5c75351def83602b5b23c1fba361b7de8ffabc7..origin/main
# 4,420 commits: between 3.14.0b1 and the 3.15 fork, i.e. inherited by 3.15, missing from 3.14
git log b092705907c758d4f9742028652c9802f9f03dd3..f5c75351def83602b5b23c1fba361b7de8ffabc7

To pick up new candidates later, re-run the same filters over <previous origin/main>..origin/main and re-check the 3.14 range for anything newly identified as severe.

The backport branches themselves are cut from the current branch tips, not the audit snapshot: 3.14 base c628cd70506, 3.15 base 9efded46646. Each branch contains exactly one commit.

Clean vs. manual backports

9 of the 20 branches were produced automatically by cherry_picker; 11 needed manual conflict resolution. In every manual case the C source change is byte-identical to the upstream commit, with one deliberate exception (gh-149816 on 3.14, below). The conflicts were in auxiliary files:

  • itertools (6 PRs, 3.14) - 3.14's Lib/test/test_free_threading/test_itertools.py has a TestTeeConcurrent class from a later tee backport, while upstream's copy has an ItertoolsThreading class that grew test by test. Each backport keeps 3.14's existing tee tests and adds only the test(s) its own PR introduced. #131212 also wanted to delete test_itertools_batched.py (upstream folded it into test_itertools.py); that is unrelated to the fix, so the file was kept.
  • gh-129069#142957, gh-143100, gh-132070 - conflicts only in Tools/tsan/suppressions_free_threading.txt, where each commit removes the suppression it retires. 3.14's copy has extra entries and, for the set fix, never had the set_swap_bodies suppression at all. Resolved by removing exactly the suppression each fix retires.
  • gh-129069#143019 - both sides add tests at the same point in test_list.py; kept both.
  • gh-149816 on 3.14 (adapted, the one non-identical case) - 3.14's batch_list_exact() has the single-item fast path before the batch loop; main moved it inside the loop, which changes the emitted pickle byte stream (APPEND vs MARK ... APPENDS for a trailing single item). To avoid changing 3.14's pickle output, 3.14's structure was kept and only the fix applied: the borrowed PyList_GET_ITEM() + Py_INCREF() became PyList_GetItemRef(), and the list changed size during iterationRuntimeError check was added after each batch. The upstream regression test (test_pickle_dumps_with_concurrent_list_mutations) tolerates RuntimeError/IndexError and passes unchanged.

Testing

Every one of the 20 branches builds and passes the full test suite in both a free-threaded and a GIL-enabled --with-pydebug build.

  • Test command: make test (--fast-ci: whole suite, parallel, -u all,-gui), free-threaded first, then GIL, one branch at a time.
  • Baseline on the unpatched 3.14 and 3.15 tips, both configs: 489/492 test files run, 0 failures. So any failure on a backport branch would be attributable to the backport itself.

Branches

All in nascheme/cpython, one commit each, cut from the current 3.14/3.15 tips. "Method" is how the backport was produced, not how risky it is - the manual ones are byte-identical to upstream apart from the auxiliary-file conflicts described above.

3.15

IssuePRBranchMethod
gh-149816#150024ft-backport/gh-149816-3.15cherry_picker
gh-144774#150578ft-backport/gh-144774-3.15cherry_picker
gh-150858#150859ft-backport/gh-150858-3.15cherry_picker
gh-150411#150413ft-backport/gh-150411-3.15cherry_picker
gh-151644#151768ft-backport/gh-151644-3.15cherry_picker

3.14

IssuePRBranchMethod
gh-149816#150024ft-backport/gh-149816-3.14manual (adapted, see above)
gh-144774#150578ft-backport/gh-144774-3.14cherry_picker
gh-150858#150859ft-backport/gh-150858-3.14cherry_picker
gh-150411#150413ft-backport/gh-150411-3.14cherry_picker
gh-151644#151768ft-backport/gh-151644-3.14cherry_picker
gh-123471#131212ft-backport/gh-123471-131212-3.14manual
gh-123471#132814ft-backport/gh-123471-132814-3.14cherry_picker
gh-123471#135689ft-backport/gh-123471-135689-3.14manual
gh-123471#144402ft-backport/gh-123471-144402-3.14manual
gh-123471#144486ft-backport/gh-123471-144486-3.14manual
gh-123471#146033ft-backport/gh-123471-146033-3.14manual
gh-129069#142957ft-backport/gh-129069-142957-3.14manual
gh-129069#143019ft-backport/gh-129069-143019-3.14manual
gh-143100#143127ft-backport/gh-143100-3.14manual
gh-132070#143441ft-backport/gh-132070-3.14manual

Open questions for the release managers:

  • Are the two no-known-crash atomics fixes (#150413, #151768) worth backporting at all, or is the churn not justified? Answer: I backported these two, should be safe.
  • Should the six itertools fixes go in as one coordinated 3.14 batch rather than six PRs? Answer: I created one PR for all.
  • Is the generator frame-state crash (#142599 / #144292) severe enough to justify the dedicated backport work it needs? Answer: This looks too complex to backport to 3.14, the fix exists in the 3.15 branch.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions