Skip to content

Commit dcf5df0

Browse files
authored
Merge branch '3.15' into backport-ac2e14b-3.15
2 parents 087ab15 + 7342e01 commit dcf5df0

7 files changed

Lines changed: 78 additions & 20 deletions

File tree

.github/workflows/build.yml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,6 @@ jobs:
601601
- Thread
602602
free-threading:
603603
- false
604-
- true
605604
sanitizer:
606605
- TSan
607606
include:
@@ -613,6 +612,17 @@ jobs:
613612
sanitizer: ${{ matrix.sanitizer }}
614613
free-threading: ${{ matrix.free-threading }}
615614

615+
# XXX: Temporarily allow this job to fail to not block PRs.
616+
build-san-free-threading:
617+
# ${{ '' } is a hack to nest jobs under the same sidebar category.
618+
name: Sanitizers${{ '' }} # zizmor: ignore[obfuscation]
619+
needs: build-context
620+
if: needs.build-context.outputs.run-ubuntu == 'true'
621+
uses: ./.github/workflows/reusable-san.yml
622+
with:
623+
sanitizer: TSan
624+
free-threading: true
625+
616626
cross-build-linux:
617627
name: Cross build Linux
618628
runs-on: ubuntu-latest
@@ -716,6 +726,7 @@ jobs:
716726
- test-hypothesis
717727
- build-asan
718728
- build-san
729+
- build-san-free-threading
719730
- cross-build-linux
720731
- cifuzz
721732
if: always()
@@ -727,6 +738,7 @@ jobs:
727738
allowed-failures: >-
728739
build-android,
729740
build-emscripten,
741+
build-san-free-threading,
730742
build-windows-msi,
731743
build-ubuntu-ssltests,
732744
test-hypothesis,

Lib/test/test_ssl.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,19 +1606,24 @@ def dummycallback(sock, servername, ctx, cycle=ctx):
16061606
gc.collect()
16071607
self.assertIs(wr(), None)
16081608

1609-
@unittest.skipUnless(support.Py_GIL_DISABLED,
1610-
"test is only useful if the GIL is disabled")
16111609
@threading_helper.requires_working_threading()
16121610
def test_sni_callback_race(self):
1613-
# Replacing sni_callback while handshakes are in-flight must not
1611+
# Replacing sni_callback while a handshake is in-flight must not
16141612
# crash (use-after-free on the callback in free-threaded builds).
1613+
#
1614+
# Use a single handshake thread: OpenSSL has internal data races
1615+
# on shared SSL_CTX state when multiple handshakes run
1616+
# concurrently against the same context (gh-150191). Concurrency
1617+
# on the *setter* is what exercises the fix from gh-149816, so
1618+
# multiple toggler threads race against each other and against
1619+
# the one handshake worker.
16151620
client_ctx, server_ctx, hostname = testing_context()
16161621

16171622
server_ctx.sni_callback = lambda *a: None
1618-
done = threading.Event()
1623+
deadline = time.monotonic() + 0.1
16191624

16201625
def do_handshakes():
1621-
while not done.is_set():
1626+
while time.monotonic() < deadline:
16221627
c_in = ssl.MemoryBIO()
16231628
c_out = ssl.MemoryBIO()
16241629
s_in = ssl.MemoryBIO()
@@ -1645,19 +1650,11 @@ def do_handshakes():
16451650
c_in.write(s_out.read())
16461651

16471652
def toggle_callback():
1648-
while not done.is_set():
1653+
while time.monotonic() < deadline:
16491654
server_ctx.sni_callback = lambda *a: None
16501655
server_ctx.sni_callback = None
16511656

1652-
workers = max(4, (os.cpu_count() or 4) * 2)
1653-
threads = [threading.Thread(target=do_handshakes)
1654-
for _ in range(workers)]
1655-
threads.append(threading.Thread(target=toggle_callback))
1656-
1657-
with threading_helper.catch_threading_exception() as cm:
1658-
with threading_helper.start_threads(threads):
1659-
done.set()
1660-
self.assertIsNone(cm.exc_value)
1657+
threading_helper.run_concurrently([do_handshakes] + 4 * [toggle_callback])
16611658

16621659
def test_cert_store_stats(self):
16631660
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)

Lib/test/test_tkinter/test_misc.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
import functools
33
import platform
44
import sys
5+
import textwrap
56
import unittest
67
import weakref
78
import tkinter
89
from tkinter import TclError
910
import enum
1011
from test import support
1112
from test.support import os_helper
13+
from test.support.script_helper import assert_python_ok
1214
from test.test_tkinter.support import setUpModule # noqa: F401
1315
from test.test_tkinter.support import (AbstractTkTest, AbstractDefaultRootTest,
1416
requires_tk, get_tk_patchlevel,
@@ -53,6 +55,33 @@ class Button2(tkinter.Button):
5355
b4 = Button2(f2)
5456
self.assertEqual(len({str(b), str(b2), str(b3), str(b4)}), 4)
5557

58+
def test_dealloc_in_wrong_thread(self):
59+
# gh-83274: deallocating the interpreter in the wrong thread must not
60+
# crash.
61+
script = textwrap.dedent("""
62+
import threading
63+
import tkinter
64+
root = tkinter.Tk()
65+
root.destroy()
66+
# Let another thread drop the last reference.
67+
ready = threading.Event()
68+
t = threading.Thread(target=lambda obj: ready.wait(), args=(root,))
69+
t.start()
70+
del root
71+
ready.set()
72+
t.join()
73+
print('ok')
74+
""")
75+
rc, out, err = assert_python_ok('-c', script)
76+
self.assertEqual(out.strip(), b'ok')
77+
if not support.Py_GIL_DISABLED:
78+
# On the free-threaded build the interpreter may instead be
79+
# deallocated in its own thread (deferred reference counting), so
80+
# the warning is not necessarily emitted. The crucial guarantee --
81+
# no crash -- is already checked by assert_python_ok() above.
82+
self.assertIn(b'RuntimeWarning', err)
83+
self.assertIn(b'gh-83274', err)
84+
5685
@requires_tk(8, 6, 6)
5786
def test_tk_busy(self):
5887
root = self.root
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a potential :exc:`SystemError` during vector calls when memory allocation fails.
2+
A :exc:`MemoryError` is now raised instead.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Deallocating a :mod:`tkinter` application from a thread other than the one it
2+
was created in no longer crashes the interpreter. The underlying Tcl
3+
interpreter is leaked instead, and a :exc:`RuntimeWarning` is reported.

Modules/_tkinter.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3132,10 +3132,24 @@ Tkapp_Dealloc(PyObject *op)
31323132
{
31333133
TkappObject *self = TkappObject_CAST(op);
31343134
PyTypeObject *tp = Py_TYPE(self);
3135-
/*CHECK_TCL_APPARTMENT;*/
3136-
ENTER_TCL
3137-
Tcl_DeleteInterp(Tkapp_Interp(self));
3138-
LEAVE_TCL
3135+
if (self->threaded && self->thread_id != Tcl_GetCurrentThread()) {
3136+
/* Deleting the interpreter from another thread aborts the process
3137+
("Tcl_AsyncDelete: async handler deleted by the wrong thread").
3138+
Leak it instead (gh-83274). */
3139+
if (PyErr_WarnEx(PyExc_RuntimeWarning,
3140+
"the Tcl interpreter is leaked because it was "
3141+
"deallocated in a thread other than the one it was "
3142+
"created in (see gh-83274)", 1) < 0)
3143+
{
3144+
PyErr_FormatUnraisable("Exception ignored while finalizing "
3145+
"a Tcl interpreter");
3146+
}
3147+
}
3148+
else {
3149+
ENTER_TCL
3150+
Tcl_DeleteInterp(Tkapp_Interp(self));
3151+
LEAVE_TCL
3152+
}
31393153
Py_XDECREF(self->trace);
31403154
PyObject_Free(self);
31413155
Py_DECREF(tp);

Python/ceval.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,7 @@ _PyObjectArray_FromStackRefArray(_PyStackRef *input, Py_ssize_t nargs, PyObject
10571057
// +1 in case PY_VECTORCALL_ARGUMENTS_OFFSET is set.
10581058
result = PyMem_Malloc((nargs + 1) * sizeof(PyObject *));
10591059
if (result == NULL) {
1060+
PyErr_NoMemory();
10601061
return NULL;
10611062
}
10621063
}

0 commit comments

Comments
 (0)