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.

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:

Ref Commit
origin/main 6d5908368dc7936802d62413344dd4596abc4159
origin/3.15 dc448f3d1a2bfc6ea18e3b5a87f5bd9eb1023964
origin/3.14 8e648a9c6b29c65ac6e290d6453cd69cedb3b1b4
3.15 fork point f5c75351def83602b5b23c1fba361b7de8ffabc7
3.14/main merge base b092705907c758d4f9742028652c9802f9f03dd3 (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.

Candidates

Missing from both 3.15 and 3.14

Issue PR main commit Summary Severity
gh-149816 #150024 d095ceb0f420 UAF in pickle.dumps() when a list is mutated concurrently Medium
gh-144774 #150578 f586fd9e304e BaseException.__setstate__() locked the exception, not the state dict, leaving borrowed dict refs exposed to concurrent mutation Low
gh-150858 #150859 1ec6596828b0 UAF reading a type's __qualname__ while another thread replaces it Low
gh-150411 #150413 12af26d17e43 Atomic load for gc.get_count() young count Very low, no known crash
gh-151644 #151768 cde31ec13590 Atomic 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().

Issue PR main commit Iterator(s)
gh-123471 #131212 26a1cd4e8c0c cycle
gh-123471 #132814 847d1c2cb401 product, combinations
gh-123471 #135689 0533c1faf27d chain
gh-123471 #144402 009c8c052f5e permutations, combinations_with_replacement
gh-123471 #144486 3a2485644700 accumulate
gh-123471 #146033 9214e3f33eee zip_longest

Other memory-safety fixes:

Issue PR main commit Summary Severity
gh-129069 #142957 e46f28c6afce Pointer tearing and uninitialized reads in memmove-style list operations Medium
gh-129069 #143019 487e91c1203d Release stores for list reverse, insert, delete, extended-slice assign Low
gh-143100 #143127 e8e044eda343 Pointer tearing from memcpy() while swapping set small tables Low
gh-132070 #143441 98e55d70bcb7 Atomic object-header init in free-threaded PyObject_Realloc() Medium

Deliberately excluded

Not backported here. Each needs its own decision.

Issue/PR main commit Why excluded
#142599 08bc03ff2a55 Atomic 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.
#144292 a01694dacd8e Protects gi_yieldfrom with a frame-state lock; should be reviewed together with #142599.
#144980 a56532771a9e Code-extra C APIs vs concurrent buffer growth/replacement. Medium-sized C API/QSBR change.
#143818 bcf9cb0217fd OpenSSL 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.
#151766 8b1dbb175404 gc.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.

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

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 iteration RuntimeError 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

Issue PR Branch Method
gh-149816 #150024 ft-backport/gh-149816-3.15 cherry_picker
gh-144774 #150578 ft-backport/gh-144774-3.15 cherry_picker
gh-150858 #150859 ft-backport/gh-150858-3.15 cherry_picker
gh-150411 #150413 ft-backport/gh-150411-3.15 cherry_picker
gh-151644 #151768 ft-backport/gh-151644-3.15 cherry_picker

3.14

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

Checklist

Backport PRs to open, once the set above is agreed:

3.15:

3.14:

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?
  • Should the six itertools fixes go in as one coordinated 3.14 batch rather than six PRs?
  • Is the generator frame-state crash (#142599 / #144292) severe enough to justify the dedicated backport work it needs?

Metadata

Metadata

Assignees

No one assigned

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions