From f2f9eccc505274e292c0f5fbf1ef92b0188cf2dd Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 18 Jul 2026 15:31:47 +0300 Subject: [PATCH 1/6] gh-153935: Fix data race on socket timeout in the free-threaded build --- Lib/test/test_free_threading/test_socket.py | 84 +++++++++++++++++++ ...-07-18-15-30-56.gh-issue-153935.xpkjEc.rst | 3 + Modules/socketmodule.c | 35 ++++---- 3 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 Lib/test/test_free_threading/test_socket.py create mode 100644 Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst diff --git a/Lib/test/test_free_threading/test_socket.py b/Lib/test/test_free_threading/test_socket.py new file mode 100644 index 000000000000000..bad7087328570ec --- /dev/null +++ b/Lib/test/test_free_threading/test_socket.py @@ -0,0 +1,84 @@ +import socket +import unittest + +from test import support +from test.support import threading_helper + +N = 200 if support.check_sanitizer(thread=True) else 1000 + + +@threading_helper.requires_working_threading() +class TestSocketTimeoutRaces(unittest.TestCase): + # gh-153935: the socket timeout was read/written non-atomically, so reading + # it (gettimeout()/getblocking() or during a socket operation) raced with + # settimeout()/setblocking() on the same socket. + + def new_socket(self): + s = socket.socket() + self.addCleanup(s.close) + return s + + def check_race(self, read, write): + def reader(): + for _ in range(N): + read() + + def writer(): + for _ in range(N): + write() + + threading_helper.run_concurrently([reader, writer]) + + def test_gettimeout_vs_setblocking(self): + s = self.new_socket() + self.check_race(s.gettimeout, lambda: s.setblocking(False)) + + def test_gettimeout_vs_settimeout(self): + s = self.new_socket() + self.check_race(s.gettimeout, lambda: s.settimeout(1.0)) + + def test_getblocking_vs_settimeout(self): + s = self.new_socket() + self.check_race(s.getblocking, lambda: s.settimeout(0)) + + def test_operation_vs_settimeout(self): + a, b = socket.socketpair() + self.addCleanup(a.close) + self.addCleanup(b.close) + a.setblocking(False) + + def recv(): + try: + a.recv(64) + except OSError: + pass + + self.check_race(recv, lambda: a.settimeout(0)) + + def test_sendall_vs_settimeout(self): + a, b = socket.socketpair() + self.addCleanup(a.close) + self.addCleanup(b.close) + a.setblocking(False) + + def sendall(): + try: + a.sendall(b"x" * 64) + except OSError: + pass + + self.check_race(sendall, lambda: a.settimeout(0)) + + def test_connect_vs_settimeout(self): + lsock = socket.socket() + lsock.bind(("127.0.0.1", 0)) + lsock.listen() + self.addCleanup(lsock.close) + addr = lsock.getsockname() + s = self.new_socket() + + self.check_race(lambda: s.connect_ex(addr), lambda: s.settimeout(0)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst b/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst new file mode 100644 index 000000000000000..a831148044a438c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst @@ -0,0 +1,3 @@ +Fix a data race on a socket's timeout in the :term:`free-threaded ` build: the timeout is now read and written atomically. Detected +by ThreadSanitizer. diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 084c2dbcff066e5..c95adc7f45e396e 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -681,7 +681,7 @@ class _socket.socket "PySocketSockObject *" "clinic_state()->sock_type" #else /* If there's no timeout left, we don't have to call select, so it's a safe, * little white lie. */ -#define IS_SELECTABLE(s) (_PyIsSelectable_fd((s)->sock_fd) || (s)->sock_timeout <= 0) +#define IS_SELECTABLE(s) (_PyIsSelectable_fd((s)->sock_fd) || _Py_atomic_load_int64_relaxed(&(s)->sock_timeout) <= 0) #endif // SCM_RIGHTS, sendmsg(), recvmsg() and sethostname() don't work properly on @@ -1068,7 +1068,7 @@ sock_call_ex(PySocketSockObject *s, /* retry sock_func() */ } - if (s->sock_timeout > 0 + if (_Py_atomic_load_int64_relaxed(&s->sock_timeout) > 0 && (CHECK_ERRNO(EWOULDBLOCK) || CHECK_ERRNO(EAGAIN))) { /* False positive: sock_func() failed with EWOULDBLOCK or EAGAIN. @@ -1094,7 +1094,8 @@ sock_call(PySocketSockObject *s, int (*func) (PySocketSockObject *s, void *data), void *data) { - return sock_call_ex(s, writing, func, data, 0, NULL, s->sock_timeout); + return sock_call_ex(s, writing, func, data, 0, NULL, + _Py_atomic_load_int64_relaxed(&s->sock_timeout)); } @@ -3169,7 +3170,8 @@ sock_setblocking(PyObject *self, PyObject *arg) return NULL; PySocketSockObject *s = _PySocketSockObject_CAST(self); - s->sock_timeout = _PyTime_FromSeconds(block ? -1 : 0); + _Py_atomic_store_int64_relaxed(&s->sock_timeout, + _PyTime_FromSeconds(block ? -1 : 0)); if (internal_setblocking(s, block) == -1) { return NULL; } @@ -3191,7 +3193,7 @@ static PyObject * sock_getblocking(PyObject *self, PyObject *Py_UNUSED(ignored)) { PySocketSockObject *s = _PySocketSockObject_CAST(self); - if (s->sock_timeout) { + if (_Py_atomic_load_int64_relaxed(&s->sock_timeout)) { Py_RETURN_TRUE; } else { @@ -3261,7 +3263,7 @@ sock_settimeout(PyObject *self, PyObject *arg) return NULL; PySocketSockObject *s = _PySocketSockObject_CAST(self); - s->sock_timeout = timeout; + _Py_atomic_store_int64_relaxed(&s->sock_timeout, timeout); int block = timeout < 0; /* Blocking mode for a Python socket object means that operations @@ -3305,11 +3307,12 @@ static PyObject * sock_gettimeout_impl(PyObject *self, void *Py_UNUSED(ignored)) { PySocketSockObject *s = _PySocketSockObject_CAST(self); - if (s->sock_timeout < 0) { + PyTime_t sock_timeout = _Py_atomic_load_int64_relaxed(&s->sock_timeout); + if (sock_timeout < 0) { Py_RETURN_NONE; } else { - double seconds = PyTime_AsSecondsDouble(s->sock_timeout); + double seconds = PyTime_AsSecondsDouble(sock_timeout); return PyFloat_FromDouble(seconds); } } @@ -3706,10 +3709,12 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, If the socket is non-blocking, raise InterruptedError. The caller is responsible to wait until the connection completes, fails or timed out (it's the case in asyncio for example). */ - wait_connect = (s->sock_timeout != 0 && IS_SELECTABLE(s)); + wait_connect = (_Py_atomic_load_int64_relaxed(&s->sock_timeout) != 0 + && IS_SELECTABLE(s)); } else { - wait_connect = (s->sock_timeout > 0 && err == SOCK_INPROGRESS_ERR + wait_connect = (_Py_atomic_load_int64_relaxed(&s->sock_timeout) > 0 + && err == SOCK_INPROGRESS_ERR && IS_SELECTABLE(s)); } @@ -3727,13 +3732,15 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, if (raise) { /* socket.connect() raises an exception on error */ if (sock_call_ex(s, 1, sock_connect_impl, NULL, - 1, NULL, s->sock_timeout) < 0) + 1, NULL, + _Py_atomic_load_int64_relaxed(&s->sock_timeout)) < 0) return -1; } else { /* socket.connect_ex() returns the error code on error */ if (sock_call_ex(s, 1, sock_connect_impl, NULL, - 1, &err, s->sock_timeout) < 0) + 1, &err, + _Py_atomic_load_int64_relaxed(&s->sock_timeout)) < 0) return err; } return 0; @@ -4688,8 +4695,8 @@ _socket_socket_sendall_impl(PySocketSockObject *s, Py_buffer *pbuf, char *buf; Py_ssize_t len, n; struct sock_send ctx; - int has_timeout = (s->sock_timeout > 0); - PyTime_t timeout = s->sock_timeout; + PyTime_t timeout = _Py_atomic_load_int64_relaxed(&s->sock_timeout); + int has_timeout = (timeout > 0); PyTime_t deadline = 0; int deadline_initialized = 0; PyObject *res = NULL; From 643d9a9bfbd0f6ccee43b7e05bcbf6975f4d358b Mon Sep 17 00:00:00 2001 From: Timofei <128279579+deadlovelll@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:42:17 +0300 Subject: [PATCH 2/6] Update Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst Co-authored-by: Peter Bierma --- .../Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst b/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst index a831148044a438c..8fd42b20ce97d62 100644 --- a/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst +++ b/Misc/NEWS.d/next/Library/2026-07-18-15-30-56.gh-issue-153935.xpkjEc.rst @@ -1,3 +1 @@ -Fix a data race on a socket's timeout in the :term:`free-threaded ` build: the timeout is now read and written atomically. Detected -by ThreadSanitizer. +Fix a data race on a socket's timeout in the :term:`free-threaded build`. From e5e97d586793e9c623c3df963a27019270a50618 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Sat, 18 Jul 2026 15:59:04 +0300 Subject: [PATCH 3/6] Use FT_ATOMIC wrappers for sock_timeout --- .../internal/pycore_pyatomic_ft_wrappers.h | 6 +++++ Modules/socketmodule.c | 25 ++++++++++--------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index d8ec306a0dae3fc..c706b0322fcf82b 100644 --- a/Include/internal/pycore_pyatomic_ft_wrappers.h +++ b/Include/internal/pycore_pyatomic_ft_wrappers.h @@ -53,6 +53,10 @@ extern "C" { _Py_atomic_load_uint64_acquire(&value) #define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \ _Py_atomic_load_uint64_relaxed(&value) +#define FT_ATOMIC_LOAD_INT64_RELAXED(value) \ + _Py_atomic_load_int64_relaxed(&value) +#define FT_ATOMIC_STORE_INT64_RELAXED(value, new_value) \ + _Py_atomic_store_int64_relaxed(&value, new_value) #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \ _Py_atomic_load_ulong_relaxed(&value) #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \ @@ -159,6 +163,8 @@ extern "C" { #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT64_ACQUIRE(value) value #define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value +#define FT_ATOMIC_LOAD_INT64_RELAXED(value) value +#define FT_ATOMIC_STORE_INT64_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index c95adc7f45e396e..a178e4301c290be 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -110,6 +110,7 @@ Local naming conventions: #include "pycore_fileutils.h" // _Py_set_inheritable() #include "pycore_moduleobject.h" // _PyModule_GetState #include "pycore_object.h" // _PyObject_VisitType() +#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT64_RELAXED() #include "pycore_time.h" // _PyTime_AsMilliseconds() #include "pycore_tuple.h" // _PyTuple_FromPairSteal #include "pycore_pystate.h" // _Py_AssertHoldsTstate() @@ -681,7 +682,7 @@ class _socket.socket "PySocketSockObject *" "clinic_state()->sock_type" #else /* If there's no timeout left, we don't have to call select, so it's a safe, * little white lie. */ -#define IS_SELECTABLE(s) (_PyIsSelectable_fd((s)->sock_fd) || _Py_atomic_load_int64_relaxed(&(s)->sock_timeout) <= 0) +#define IS_SELECTABLE(s) (_PyIsSelectable_fd((s)->sock_fd) || FT_ATOMIC_LOAD_INT64_RELAXED((s)->sock_timeout) <= 0) #endif // SCM_RIGHTS, sendmsg(), recvmsg() and sethostname() don't work properly on @@ -1068,7 +1069,7 @@ sock_call_ex(PySocketSockObject *s, /* retry sock_func() */ } - if (_Py_atomic_load_int64_relaxed(&s->sock_timeout) > 0 + if (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout) > 0 && (CHECK_ERRNO(EWOULDBLOCK) || CHECK_ERRNO(EAGAIN))) { /* False positive: sock_func() failed with EWOULDBLOCK or EAGAIN. @@ -1095,7 +1096,7 @@ sock_call(PySocketSockObject *s, void *data) { return sock_call_ex(s, writing, func, data, 0, NULL, - _Py_atomic_load_int64_relaxed(&s->sock_timeout)); + FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)); } @@ -3170,7 +3171,7 @@ sock_setblocking(PyObject *self, PyObject *arg) return NULL; PySocketSockObject *s = _PySocketSockObject_CAST(self); - _Py_atomic_store_int64_relaxed(&s->sock_timeout, + FT_ATOMIC_STORE_INT64_RELAXED(s->sock_timeout, _PyTime_FromSeconds(block ? -1 : 0)); if (internal_setblocking(s, block) == -1) { return NULL; @@ -3193,7 +3194,7 @@ static PyObject * sock_getblocking(PyObject *self, PyObject *Py_UNUSED(ignored)) { PySocketSockObject *s = _PySocketSockObject_CAST(self); - if (_Py_atomic_load_int64_relaxed(&s->sock_timeout)) { + if (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)) { Py_RETURN_TRUE; } else { @@ -3263,7 +3264,7 @@ sock_settimeout(PyObject *self, PyObject *arg) return NULL; PySocketSockObject *s = _PySocketSockObject_CAST(self); - _Py_atomic_store_int64_relaxed(&s->sock_timeout, timeout); + FT_ATOMIC_STORE_INT64_RELAXED(s->sock_timeout, timeout); int block = timeout < 0; /* Blocking mode for a Python socket object means that operations @@ -3307,7 +3308,7 @@ static PyObject * sock_gettimeout_impl(PyObject *self, void *Py_UNUSED(ignored)) { PySocketSockObject *s = _PySocketSockObject_CAST(self); - PyTime_t sock_timeout = _Py_atomic_load_int64_relaxed(&s->sock_timeout); + PyTime_t sock_timeout = FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout); if (sock_timeout < 0) { Py_RETURN_NONE; } @@ -3709,11 +3710,11 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, If the socket is non-blocking, raise InterruptedError. The caller is responsible to wait until the connection completes, fails or timed out (it's the case in asyncio for example). */ - wait_connect = (_Py_atomic_load_int64_relaxed(&s->sock_timeout) != 0 + wait_connect = (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout) != 0 && IS_SELECTABLE(s)); } else { - wait_connect = (_Py_atomic_load_int64_relaxed(&s->sock_timeout) > 0 + wait_connect = (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout) > 0 && err == SOCK_INPROGRESS_ERR && IS_SELECTABLE(s)); } @@ -3733,14 +3734,14 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, /* socket.connect() raises an exception on error */ if (sock_call_ex(s, 1, sock_connect_impl, NULL, 1, NULL, - _Py_atomic_load_int64_relaxed(&s->sock_timeout)) < 0) + FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)) < 0) return -1; } else { /* socket.connect_ex() returns the error code on error */ if (sock_call_ex(s, 1, sock_connect_impl, NULL, 1, &err, - _Py_atomic_load_int64_relaxed(&s->sock_timeout)) < 0) + FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)) < 0) return err; } return 0; @@ -4695,7 +4696,7 @@ _socket_socket_sendall_impl(PySocketSockObject *s, Py_buffer *pbuf, char *buf; Py_ssize_t len, n; struct sock_send ctx; - PyTime_t timeout = _Py_atomic_load_int64_relaxed(&s->sock_timeout); + PyTime_t timeout = FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout); int has_timeout = (timeout > 0); PyTime_t deadline = 0; int deadline_initialized = 0; From b0d61d2935b275ef2484961f839e529571b65660 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 21 Jul 2026 15:09:28 -0700 Subject: [PATCH 4/6] Hold mutex when setting timeout/blocking. --- .../internal/pycore_pyatomic_ft_wrappers.h | 12 ++-- Modules/socketmodule.c | 62 ++++++++++++------- Modules/socketmodule.h | 3 + 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/Include/internal/pycore_pyatomic_ft_wrappers.h b/Include/internal/pycore_pyatomic_ft_wrappers.h index c706b0322fcf82b..7c622a6eaeeea1b 100644 --- a/Include/internal/pycore_pyatomic_ft_wrappers.h +++ b/Include/internal/pycore_pyatomic_ft_wrappers.h @@ -53,10 +53,10 @@ extern "C" { _Py_atomic_load_uint64_acquire(&value) #define FT_ATOMIC_LOAD_UINT64_RELAXED(value) \ _Py_atomic_load_uint64_relaxed(&value) -#define FT_ATOMIC_LOAD_INT64_RELAXED(value) \ - _Py_atomic_load_int64_relaxed(&value) -#define FT_ATOMIC_STORE_INT64_RELAXED(value, new_value) \ - _Py_atomic_store_int64_relaxed(&value, new_value) +#define FT_ATOMIC_LOAD_INT64_ACQUIRE(value) \ + ((int64_t)_Py_atomic_load_uint64_acquire((const uint64_t *)&value)) +#define FT_ATOMIC_STORE_INT64_RELEASE(value, new_value) \ + _Py_atomic_store_uint64_release((uint64_t *)&value, (uint64_t)new_value) #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) \ _Py_atomic_load_ulong_relaxed(&value) #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) \ @@ -163,8 +163,8 @@ extern "C" { #define FT_ATOMIC_LOAD_UINT32_RELAXED(value) value #define FT_ATOMIC_LOAD_UINT64_ACQUIRE(value) value #define FT_ATOMIC_LOAD_UINT64_RELAXED(value) value -#define FT_ATOMIC_LOAD_INT64_RELAXED(value) value -#define FT_ATOMIC_STORE_INT64_RELAXED(value, new_value) value = new_value +#define FT_ATOMIC_LOAD_INT64_ACQUIRE(value) value +#define FT_ATOMIC_STORE_INT64_RELEASE(value, new_value) value = new_value #define FT_ATOMIC_LOAD_ULONG_RELAXED(value) value #define FT_ATOMIC_STORE_PTR_RELAXED(value, new_value) value = new_value #define FT_ATOMIC_STORE_PTR_RELEASE(value, new_value) value = new_value diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index a178e4301c290be..bff20828daf7503 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -110,7 +110,7 @@ Local naming conventions: #include "pycore_fileutils.h" // _Py_set_inheritable() #include "pycore_moduleobject.h" // _PyModule_GetState #include "pycore_object.h" // _PyObject_VisitType() -#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT64_RELAXED() +#include "pycore_pyatomic_ft_wrappers.h" // FT_ATOMIC_LOAD_INT64_ACQUIRE() #include "pycore_time.h" // _PyTime_AsMilliseconds() #include "pycore_tuple.h" // _PyTuple_FromPairSteal #include "pycore_pystate.h" // _Py_AssertHoldsTstate() @@ -592,6 +592,12 @@ get_sock_fd(PySocketSockObject *s) #endif } +static inline PyTime_t +get_sock_timeout(PySocketSockObject *s) +{ + return FT_ATOMIC_LOAD_INT64_ACQUIRE(s->sock_timeout); +} + #define _PySocketSockObject_CAST(op) ((PySocketSockObject *)(op)) static inline socket_state * @@ -682,7 +688,7 @@ class _socket.socket "PySocketSockObject *" "clinic_state()->sock_type" #else /* If there's no timeout left, we don't have to call select, so it's a safe, * little white lie. */ -#define IS_SELECTABLE(s) (_PyIsSelectable_fd((s)->sock_fd) || FT_ATOMIC_LOAD_INT64_RELAXED((s)->sock_timeout) <= 0) +#define IS_SELECTABLE(s) (_PyIsSelectable_fd((s)->sock_fd) || get_sock_timeout(s) <= 0) #endif // SCM_RIGHTS, sendmsg(), recvmsg() and sethostname() don't work properly on @@ -1069,7 +1075,7 @@ sock_call_ex(PySocketSockObject *s, /* retry sock_func() */ } - if (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout) > 0 + if (get_sock_timeout(s) > 0 && (CHECK_ERRNO(EWOULDBLOCK) || CHECK_ERRNO(EAGAIN))) { /* False positive: sock_func() failed with EWOULDBLOCK or EAGAIN. @@ -1096,7 +1102,7 @@ sock_call(PySocketSockObject *s, void *data) { return sock_call_ex(s, writing, func, data, 0, NULL, - FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)); + get_sock_timeout(s)); } @@ -1160,6 +1166,9 @@ new_sockobject(socket_state *state, SOCKET_T fd, int family, int type, if (s == NULL) { return NULL; } +#ifdef Py_GIL_DISABLED + s->sock_timeout_mutex = (PyMutex){0}; +#endif if (init_sockobject(state, s, fd, family, type, proto) == -1) { Py_DECREF(s); return NULL; @@ -3170,10 +3179,15 @@ sock_setblocking(PyObject *self, PyObject *arg) if (block < 0) return NULL; - PySocketSockObject *s = _PySocketSockObject_CAST(self); - FT_ATOMIC_STORE_INT64_RELAXED(s->sock_timeout, - _PyTime_FromSeconds(block ? -1 : 0)); - if (internal_setblocking(s, block) == -1) { + PySocketSockObject *s = _PySocketSockObject_CAST(self); + FT_MUTEX_LOCK(&s->sock_timeout_mutex); + int result = internal_setblocking(s, block); + if (result == 0) { + FT_ATOMIC_STORE_INT64_RELEASE( + s->sock_timeout, _PyTime_FromSeconds(block ? -1 : 0)); + } + FT_MUTEX_UNLOCK(&s->sock_timeout_mutex); + if (result < 0) { return NULL; } Py_RETURN_NONE; @@ -3193,8 +3207,8 @@ setblocking(False) is equivalent to settimeout(0.0)."); static PyObject * sock_getblocking(PyObject *self, PyObject *Py_UNUSED(ignored)) { - PySocketSockObject *s = _PySocketSockObject_CAST(self); - if (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)) { + PySocketSockObject *s = _PySocketSockObject_CAST(self); + if (get_sock_timeout(s)) { Py_RETURN_TRUE; } else { @@ -3264,8 +3278,6 @@ sock_settimeout(PyObject *self, PyObject *arg) return NULL; PySocketSockObject *s = _PySocketSockObject_CAST(self); - FT_ATOMIC_STORE_INT64_RELAXED(s->sock_timeout, timeout); - int block = timeout < 0; /* Blocking mode for a Python socket object means that operations like :meth:`recv` or :meth:`sendall` will block the execution of @@ -3288,7 +3300,13 @@ sock_settimeout(PyObject *self, PyObject *arg) ``> 0`` ``True`` non-blocking */ - if (internal_setblocking(s, block) == -1) { + FT_MUTEX_LOCK(&s->sock_timeout_mutex); + int result = internal_setblocking(s, block); + if (result == 0) { + FT_ATOMIC_STORE_INT64_RELEASE(s->sock_timeout, timeout); + } + FT_MUTEX_UNLOCK(&s->sock_timeout_mutex); + if (result < 0) { return NULL; } Py_RETURN_NONE; @@ -3308,7 +3326,7 @@ static PyObject * sock_gettimeout_impl(PyObject *self, void *Py_UNUSED(ignored)) { PySocketSockObject *s = _PySocketSockObject_CAST(self); - PyTime_t sock_timeout = FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout); + PyTime_t sock_timeout = get_sock_timeout(s); if (sock_timeout < 0) { Py_RETURN_NONE; } @@ -3710,11 +3728,10 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, If the socket is non-blocking, raise InterruptedError. The caller is responsible to wait until the connection completes, fails or timed out (it's the case in asyncio for example). */ - wait_connect = (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout) != 0 - && IS_SELECTABLE(s)); + wait_connect = (get_sock_timeout(s) != 0 && IS_SELECTABLE(s)); } else { - wait_connect = (FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout) > 0 + wait_connect = (get_sock_timeout(s) > 0 && err == SOCK_INPROGRESS_ERR && IS_SELECTABLE(s)); } @@ -3733,15 +3750,13 @@ internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen, if (raise) { /* socket.connect() raises an exception on error */ if (sock_call_ex(s, 1, sock_connect_impl, NULL, - 1, NULL, - FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)) < 0) + 1, NULL, get_sock_timeout(s)) < 0) return -1; } else { /* socket.connect_ex() returns the error code on error */ if (sock_call_ex(s, 1, sock_connect_impl, NULL, - 1, &err, - FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout)) < 0) + 1, &err, get_sock_timeout(s)) < 0) return err; } return 0; @@ -4696,7 +4711,7 @@ _socket_socket_sendall_impl(PySocketSockObject *s, Py_buffer *pbuf, char *buf; Py_ssize_t len, n; struct sock_send ctx; - PyTime_t timeout = FT_ATOMIC_LOAD_INT64_RELAXED(s->sock_timeout); + PyTime_t timeout = get_sock_timeout(s); int has_timeout = (timeout > 0); PyTime_t deadline = 0; int deadline_initialized = 0; @@ -5612,6 +5627,9 @@ sock_new(PyTypeObject *type, PyObject *args, PyObject *kwds) if (new != NULL) { ((PySocketSockObject *)new)->sock_fd = INVALID_SOCKET; ((PySocketSockObject *)new)->sock_timeout = _PyTime_FromSeconds(-1); +#ifdef Py_GIL_DISABLED + ((PySocketSockObject *)new)->sock_timeout_mutex = (PyMutex){0}; +#endif ((PySocketSockObject *)new)->errorhandler = &set_error; #ifdef MS_WINDOWS ((PySocketSockObject *)new)->quickack = 0; diff --git a/Modules/socketmodule.h b/Modules/socketmodule.h index ac770889ae87f2c..935065fa8bb3990 100644 --- a/Modules/socketmodule.h +++ b/Modules/socketmodule.h @@ -346,6 +346,9 @@ typedef struct { sets a Python exception */ PyTime_t sock_timeout; /* Operation timeout in seconds; 0.0 means non-blocking */ +#ifdef Py_GIL_DISABLED + PyMutex sock_timeout_mutex; +#endif struct _socket_state *state; #ifdef MS_WINDOWS int quickack; From 7491e7f039cab03ed69f3c0fb8ee2fbe5140a910 Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Fri, 24 Jul 2026 16:39:52 +0300 Subject: [PATCH 5/6] added docs entry --- Doc/library/socket.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index 836aa91bb0885b1..99fed7008f5b8c7 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -2094,6 +2094,24 @@ Socket Objects For further information, please consult the :ref:`notes on socket timeouts `. + .. note:: + + Do not call :meth:`setblocking` or :meth:`settimeout` while another + thread is performing an operation on the same socket. Changing the + blocking mode concurrently with operations such as :meth:`connect`, + :meth:`accept`, :meth:`recv`, or :meth:`send` can cause the operation + to block unexpectedly or to fail with a non-blocking error. + + Applications must use external synchronization when changing a + socket's blocking mode or timeout. This also applies to socket + objects and file descriptors that share the same underlying socket, + since the blocking mode is shared at the operating-system level. + + Reading a socket's current timeout or blocking mode with + :meth:`gettimeout` or :meth:`getblocking` remains safe to do + concurrently, only *changing* it during an operation requires the + synchronization described above. + .. versionchanged:: 3.7 The method no longer toggles :const:`SOCK_NONBLOCK` flag on :attr:`socket.type`. From 3055bce0d1204cb22df89d44cc05ce388be683eb Mon Sep 17 00:00:00 2001 From: Timofey Ivankov Date: Fri, 24 Jul 2026 16:44:43 +0300 Subject: [PATCH 6/6] merge --- Lib/test/test_free_threading/test_socket.py | 44 ++------------------- 1 file changed, 3 insertions(+), 41 deletions(-) diff --git a/Lib/test/test_free_threading/test_socket.py b/Lib/test/test_free_threading/test_socket.py index bad7087328570ec..622e94337a678a9 100644 --- a/Lib/test/test_free_threading/test_socket.py +++ b/Lib/test/test_free_threading/test_socket.py @@ -9,9 +9,9 @@ @threading_helper.requires_working_threading() class TestSocketTimeoutRaces(unittest.TestCase): - # gh-153935: the socket timeout was read/written non-atomically, so reading - # it (gettimeout()/getblocking() or during a socket operation) raced with - # settimeout()/setblocking() on the same socket. + # gh-153852: the socket timeout was read/written non-atomically, so reading + # it with gettimeout()/getblocking() raced with settimeout()/setblocking() + # on the same socket. def new_socket(self): s = socket.socket() @@ -41,44 +41,6 @@ def test_getblocking_vs_settimeout(self): s = self.new_socket() self.check_race(s.getblocking, lambda: s.settimeout(0)) - def test_operation_vs_settimeout(self): - a, b = socket.socketpair() - self.addCleanup(a.close) - self.addCleanup(b.close) - a.setblocking(False) - - def recv(): - try: - a.recv(64) - except OSError: - pass - - self.check_race(recv, lambda: a.settimeout(0)) - - def test_sendall_vs_settimeout(self): - a, b = socket.socketpair() - self.addCleanup(a.close) - self.addCleanup(b.close) - a.setblocking(False) - - def sendall(): - try: - a.sendall(b"x" * 64) - except OSError: - pass - - self.check_race(sendall, lambda: a.settimeout(0)) - - def test_connect_vs_settimeout(self): - lsock = socket.socket() - lsock.bind(("127.0.0.1", 0)) - lsock.listen() - self.addCleanup(lsock.close) - addr = lsock.getsockname() - s = self.new_socket() - - self.check_race(lambda: s.connect_ex(addr), lambda: s.settimeout(0)) - if __name__ == "__main__": unittest.main()