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.
-
Screen two disjoint commit ranges covering everything in main that 3.14 does not have (5,389 commits total, see below).
-
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
-
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.
-
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.
-
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().
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
3.14
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?
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 inmainbut 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.
Screen two disjoint commit ranges covering everything in
mainthat 3.14 does not have (5,389 commits total, see below).Filter commit subjects, bodies, NEWS entries, and diffs for:
free-threading,free-threaded,Py_GIL_DISABLED,nogil, standaloneFTsafe,thread-safe,thread safetyrace,racing,concurrentUAF,use-after-free,segfault,crash,deadlockLib/test/test_free_threading/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.
Check what is already backported, using source commit hashes quoted in maintenance-branch commit messages, PR/issue IDs in subjects and bodies,
git cherrypatch-equivalence (git cherry origin/3.15 origin/main, likewise for 3.14), and manual inspection where the code had diverged.Check applicability in a throwaway worktree per candidate:
Note that
--3way --checkpassing says nothing about semantic correctness, which is why everybackport 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:
origin/main6d5908368dc7936802d62413344dd4596abc4159origin/3.15dc448f3d1a2bfc6ea18e3b5a87f5bd9eb1023964origin/3.148e648a9c6b29c65ac6e290d6453cd69cedb3b1b4f5c75351def83602b5b23c1fba361b7de8ffabc7mainmerge baseb092705907c758d4f9742028652c9802f9f03dd3(Python 3.14.0b1)The two ranges screened:
To pick up new candidates later, re-run the same filters over
<previous origin/main>..origin/mainand 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 base9efded46646. Each branch contains exactly one commit.Candidates
Missing from both 3.15 and 3.14
maincommitd095ceb0f420pickle.dumps()when a list is mutated concurrentlyf586fd9e304eBaseException.__setstate__()locked the exception, not the state dict, leaving borrowed dict refs exposed to concurrent mutation1ec6596828b0__qualname__while another thread replaces it12af26d17e43gc.get_count()young countcde31ec13590sys.getdlopenflags()/sys.setdlopenflags()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;cycleinstead uses atomic index accesses plusPyList_GetItemRef().maincommit26a1cd4e8c0ccycle847d1c2cb401product,combinations0533c1faf27dchain009c8c052f5epermutations,combinations_with_replacement3a2485644700accumulate9214e3f33eeezip_longestOther memory-safety fixes:
maincommite46f28c6afce487e91c1203de8e044eda343memcpy()while swapping set small tables98e55d70bcb7PyObject_Realloc()Deliberately excluded
Not backported here. Each needs its own decision.
maincommit08bc03ff2a55a01694dacd8egi_yieldfromwith a frame-state lock; should be reviewed together with #142599.a56532771a9ebcf9cb0217fdSSLContext.load_cert_chain(). Medium-sized SSL locking change; a current 3.14 TSan backport explicitly notes this race is still unfixed in 3.14.8b1dbb175404gc.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
8e648a9c6b29predates the current tip). No action needed.d76c56e958c[3.14] gh-145142: Make str.maketrans safe under free-threading (#145320)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:Lib/test/test_free_threading/test_itertools.pyhas aTestTeeConcurrentclass from a laterteebackport, while upstream's copy has anItertoolsThreadingclass 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 deletetest_itertools_batched.py(upstream folded it intotest_itertools.py); that is unrelated to the fix, so the file was kept.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 theset_swap_bodiessuppression at all. Resolved by removing exactly the suppression each fix retires.test_list.py; kept both.batch_list_exact()has the single-item fast path before the batch loop;mainmoved it inside the loop, which changes the emitted pickle byte stream (APPENDvsMARK ... APPENDSfor a trailing single item). To avoid changing 3.14's pickle output, 3.14's structure was kept and only the fix applied: the borrowedPyList_GET_ITEM()+Py_INCREF()becamePyList_GetItemRef(), and thelist changed size during iterationRuntimeErrorcheck was added after each batch. The upstream regression test (test_pickle_dumps_with_concurrent_list_mutations) toleratesRuntimeError/IndexErrorand 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-pydebugbuild.make test(--fast-ci: whole suite, parallel,-u all,-gui), free-threaded first, then GIL, one branch at a time.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
ft-backport/gh-149816-3.15ft-backport/gh-144774-3.15ft-backport/gh-150858-3.15ft-backport/gh-150411-3.15ft-backport/gh-151644-3.153.14
ft-backport/gh-149816-3.14ft-backport/gh-144774-3.14ft-backport/gh-150858-3.14ft-backport/gh-150411-3.14ft-backport/gh-151644-3.14ft-backport/gh-123471-131212-3.14ft-backport/gh-123471-132814-3.14ft-backport/gh-123471-135689-3.14ft-backport/gh-123471-144402-3.14ft-backport/gh-123471-144486-3.14ft-backport/gh-123471-146033-3.14ft-backport/gh-129069-142957-3.14ft-backport/gh-129069-143019-3.14ft-backport/gh-143100-3.14ft-backport/gh-132070-3.14Checklist
Backport PRs to open, once the set above is agreed:
3.15:
pickle.dumps()UAFBaseException.__setstate__()__qualname__gc.get_count()sys.get/setdlopenflags()3.14:
pickle.dumps()UAF (adapted)BaseException.__setstate__()__qualname__gc.get_count()sys.get/setdlopenflags()itertools.cycleitertools.product,combinationsitertools.chainitertools.permutations,combinations_with_replacementitertools.accumulateitertools.zip_longestmemcpytearingPyObject_Realloc()header initOpen questions for the release managers: