diff --git a/Lib/test/test_free_threading/test_functions.py b/Lib/test/test_free_threading/test_functions.py new file mode 100644 index 000000000000000..65a90a17bd5b8db --- /dev/null +++ b/Lib/test/test_free_threading/test_functions.py @@ -0,0 +1,98 @@ +import random +import unittest +from unittest import TestCase + +from test.support import threading_helper + +threading_helper.requires_working_threading(module=True) + +NUM_THREADS = 8 +ITERS = 200 + + +def random_string(): + return ''.join(random.choice('0123456789ABCDEF') for _ in range(10)) + + +def template_a(): pass +def template_b(): pass + + +class TestFTFunctionAttributes(TestCase): + + def stress_attribute(self, attr, make_value): + def target(x=1): + return x + + def writer(): + for _ in range(ITERS): + setattr(target, attr, make_value()) + getattr(target, attr) + + threading_helper.run_concurrently(writer, NUM_THREADS) + + def test_name(self): + self.stress_attribute("__name__", random_string) + + def test_qualname(self): + self.stress_attribute("__qualname__", random_string) + + def test_code(self): + codes = (template_a.__code__, template_b.__code__) + self.stress_attribute("__code__", lambda: random.choice(codes)) + + def test_defaults(self): + self.stress_attribute("__defaults__", lambda: (random_string(),)) + + def test_kwdefaults(self): + self.stress_attribute("__kwdefaults__", lambda: {"x": random_string()}) + + def test_annotations(self): + self.stress_attribute("__annotations__", + lambda: {"x": random_string()}) + + def test_annotate(self): + self.stress_attribute("__annotate__", + lambda: (lambda format: {"x": str})) + + def test_type_params(self): + self.stress_attribute("__type_params__", lambda: (random_string(),)) + + def test_annotations_and_annotate(self): + # The __annotations__ and __annotate__ setters clear each other. + def target(): pass + + def set_annotations(): + for _ in range(ITERS): + target.__annotations__ = {"x": random_string()} + target.__annotations__ + + def set_annotate(): + for _ in range(ITERS): + target.__annotate__ = lambda format: {"x": str} + target.__annotate__ + + threading_helper.run_concurrently( + [set_annotations, set_annotate] * (NUM_THREADS // 2)) + + def test_call_while_replacing_defaults(self): + # The eval loop reads __defaults__ and __kwdefaults__ without holding + # a lock while pushing a frame. + def target(x="init", *, y="init"): + return x, y + + def writer(): + for _ in range(ITERS): + target.__defaults__ = (random_string(),) + target.__kwdefaults__ = {"y": random_string()} + + def caller(): + for _ in range(ITERS): + target() + + threading_helper.run_concurrently( + [writer, caller] * (NUM_THREADS // 2)) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py index bc7f1ac12b42874..38bc681a267b951 100644 --- a/Lib/test/test_xml_etree.py +++ b/Lib/test/test_xml_etree.py @@ -3477,6 +3477,37 @@ def test_find_xpath(self): self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()-0]') self.assertRaisesRegex(SyntaxError, 'XPath', e.find, './tag[last()+1]') + def test_find_xpath_index_no_quadratic_complexity(self): + class CountingElement(ET.Element): + findall_calls = 0 + def findall(self, *args, **kwargs): + type(self).findall_calls += 1 + return super().findall(*args, **kwargs) + + def work(n, pattern): + root = CountingElement("root") + for _ in range(n): + ET.SubElement(root, "a") + CountingElement.findall_calls = 0 + root.findall(pattern) + return CountingElement.findall_calls + + for pattern in [".//a[1]", ".//a[last()]"]: + w1 = work(1024, pattern) + w2 = work(2048, pattern) + w3 = work(4096, pattern) + + self.assertGreater(w1, 0) + r1 = w2 / w1 + r2 = w3 / w2 + # Doubling N must not ~double the parent.findall calls. + # Linear-in-N call counts indicate the cache is missing. + self.assertLess( + max(r1, r2), 1.5, + msg=f"Possible quadratic behavior on {pattern!r}: " + f"calls={w1, w2, w3} ratios={r1, r2}", + ) + def test_findall(self): e = ET.XML(SAMPLE_XML) e[2] = ET.XML(SAMPLE_SECTION) diff --git a/Lib/xml/etree/ElementPath.py b/Lib/xml/etree/ElementPath.py index dc6bd28c03137de..de1fd203ff6a0f6 100644 --- a/Lib/xml/etree/ElementPath.py +++ b/Lib/xml/etree/ElementPath.py @@ -324,15 +324,22 @@ def select_negated(context, result): index = -1 def select(context, result): parent_map = get_parent_map(context) + cache = {} for elem in result: try: parent = parent_map[elem] + except KeyError: + continue + key = (parent, elem.tag) + if key not in cache: # FIXME: what if the selector is "*" ? - elems = list(parent.findall(elem.tag)) - if elems[index] is elem: - yield elem - except (IndexError, KeyError): - pass + elems = parent.findall(elem.tag) + try: + cache[key] = elems[index] + except IndexError: + cache[key] = None + if cache[key] is elem: + yield elem return select raise SyntaxError("invalid predicate") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-07-28-10-00-00.gh-issue-133931.fnQzxD.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-28-10-00-00.gh-issue-133931.fnQzxD.rst new file mode 100644 index 000000000000000..139bdd86b84892f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-07-28-10-00-00.gh-issue-133931.fnQzxD.rst @@ -0,0 +1,2 @@ +Fix data races when setting attributes of function objects +on the :term:`free threaded ` build. diff --git a/Misc/NEWS.d/next/Library/2026-07-21-08-31-55.gh-issue-154324.vDxV5U.rst b/Misc/NEWS.d/next/Library/2026-07-21-08-31-55.gh-issue-154324.vDxV5U.rst new file mode 100644 index 000000000000000..388e4deb7c41989 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-21-08-31-55.gh-issue-154324.vDxV5U.rst @@ -0,0 +1,3 @@ +Fix :func:`os.sendfile` on illumos: +it no longer reports a successful transfer +when the underlying system call failed without writing any data. diff --git a/Misc/NEWS.d/next/Security/2026-06-30-13-24-13.gh-issue-152674.-2QVoL.rst b/Misc/NEWS.d/next/Security/2026-06-30-13-24-13.gh-issue-152674.-2QVoL.rst new file mode 100644 index 000000000000000..69e73008af79f14 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-06-30-13-24-13.gh-issue-152674.-2QVoL.rst @@ -0,0 +1,6 @@ +The :class:`xml.etree.ElementTree.Element` methods +:meth:`~xml.etree.ElementTree.Element.findall`, +:meth:`~xml.etree.ElementTree.Element.iterfind` and +:meth:`~xml.etree.ElementTree.Element.find` avoid quadratic behavior when +using XPath index predicates (``[1]``, ``[last()]``, ``[last()-N]``) on XML +documents with many same-tag siblings. diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 350f56c393d3b97..f754d0e18b5fb09 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -12614,27 +12614,35 @@ os_sendfile_impl(PyObject *module, int out_fd, int in_fd, PyObject *offobj, return PyLong_FromLong(0); } - // On illumos specifically sendfile() may perform a partial write but - // return -1/an error (in one confirmed case the destination socket - // had a 5 second timeout set and errno was EAGAIN) and it's on the client - // code to check if the offset parameter was modified by sendfile(). - // - // We need this variable to track said change. - off_t original_offset = offset; -#endif + // sendfile() may perform a partial write and still return -1, so the + // number of transferred bytes must be taken from the out parameter. + // sendfile() reports it by adding it to the offset, but does not + // initialize it when the transfer fails before writing any data, so use + // sendfilev(), which reports it explicitly. + sendfilevec_t vec; + size_t xferred; + + vec.sfv_fd = in_fd; + vec.sfv_flag = 0; + vec.sfv_off = offset; + vec.sfv_len = count; do { Py_BEGIN_ALLOW_THREADS - ret = sendfile(out_fd, in_fd, &offset, count); -#if defined(__sun) && defined(__SVR4) - // This handles illumos-specific sendfile() partial write behavior, - // see a comment above for more details. - if (ret < 0 && offset != original_offset) { - ret = offset - original_offset; + xferred = 0; + ret = sendfilev(out_fd, &vec, 1, &xferred); + if (ret < 0 && xferred != 0) { + ret = (Py_ssize_t)xferred; } -#endif Py_END_ALLOW_THREADS } while (ret < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals())); +#else + do { + Py_BEGIN_ALLOW_THREADS + ret = sendfile(out_fd, in_fd, &offset, count); + Py_END_ALLOW_THREADS + } while (ret < 0 && errno == EINTR && !(async_err = PyErr_CheckSignals())); +#endif if (ret < 0) return (!async_err) ? posix_error() : NULL; return PyLong_FromSsize_t(ret); diff --git a/Objects/funcobject.c b/Objects/funcobject.c index 0fffd36ad462dab..49a28e8ad667141 100644 --- a/Objects/funcobject.c +++ b/Objects/funcobject.c @@ -349,20 +349,6 @@ func_clear_version(PyInterpreterState *interp, PyFunctionObject *func) func->func_version = FUNC_VERSION_CLEARED; } -// Called when any of the critical function attributes are changed -static void -_PyFunction_ClearVersion(PyFunctionObject *func) -{ - if (func->func_version < FUNC_VERSION_FIRST_VALID) { - // Version was never set or has already been cleared. - return; - } - PyInterpreterState *interp = _PyInterpreterState_GET(); - _PyEval_StopTheWorld(interp); - func_clear_version(interp, func); - _PyEval_StartTheWorld(interp); -} - void _PyFunction_ClearCodeByVersion(uint32_t version) { @@ -448,10 +434,15 @@ PyFunction_SetDefaults(PyObject *op, PyObject *defaults) PyErr_SetString(PyExc_SystemError, "non-tuple default args"); return -1; } - handle_func_event(PyFunction_EVENT_MODIFY_DEFAULTS, - (PyFunctionObject *) op, defaults); - _PyFunction_ClearVersion((PyFunctionObject *)op); - Py_XSETREF(((PyFunctionObject *)op)->func_defaults, defaults); + PyFunctionObject *func = (PyFunctionObject *)op; + handle_func_event(PyFunction_EVENT_MODIFY_DEFAULTS, func, defaults); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, func); + PyObject *old_defaults = func->func_defaults; + func->func_defaults = defaults; + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_defaults); return 0; } @@ -459,8 +450,11 @@ void PyFunction_SetVectorcall(PyFunctionObject *func, vectorcallfunc vectorcall) { assert(func != NULL); - _PyFunction_ClearVersion(func); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, func); func->vectorcall = vectorcall; + _PyEval_StartTheWorld(interp); } PyObject * @@ -490,10 +484,15 @@ PyFunction_SetKwDefaults(PyObject *op, PyObject *defaults) "non-dict keyword only default args"); return -1; } - handle_func_event(PyFunction_EVENT_MODIFY_KWDEFAULTS, - (PyFunctionObject *) op, defaults); - _PyFunction_ClearVersion((PyFunctionObject *)op); - Py_XSETREF(((PyFunctionObject *)op)->func_kwdefaults, defaults); + PyFunctionObject *func = (PyFunctionObject *)op; + handle_func_event(PyFunction_EVENT_MODIFY_KWDEFAULTS, func, defaults); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, func); + PyObject *old_kwdefaults = func->func_kwdefaults; + func->func_kwdefaults = defaults; + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_kwdefaults); return 0; } @@ -525,8 +524,14 @@ PyFunction_SetClosure(PyObject *op, PyObject *closure) Py_TYPE(closure)->tp_name); return -1; } - _PyFunction_ClearVersion((PyFunctionObject *)op); - Py_XSETREF(((PyFunctionObject *)op)->func_closure, closure); + PyFunctionObject *func = (PyFunctionObject *)op; + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, func); + PyObject *old_closure = func->func_closure; + func->func_closure = closure; + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_closure); return 0; } @@ -605,8 +610,15 @@ PyFunction_SetAnnotations(PyObject *op, PyObject *annotations) return -1; } PyFunctionObject *func = (PyFunctionObject *)op; - Py_XSETREF(func->func_annotations, annotations); - Py_CLEAR(func->func_annotate); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_annotations = func->func_annotations; + func->func_annotations = annotations; + PyObject *old_annotate = func->func_annotate; + func->func_annotate = NULL; + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_annotations); + Py_XDECREF(old_annotate); return 0; } @@ -685,8 +697,13 @@ func_set_code(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) } handle_func_event(PyFunction_EVENT_MODIFY_CODE, op, value); - _PyFunction_ClearVersion(op); - Py_XSETREF(op->func_code, Py_NewRef(value)); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, op); + PyObject *old_code = op->func_code; + op->func_code = Py_NewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_code); return 0; } @@ -708,7 +725,12 @@ func_set_name(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) "__name__ must be set to a string object"); return -1; } - Py_XSETREF(op->func_name, Py_NewRef(value)); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_name = op->func_name; + op->func_name = Py_NewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_name); return 0; } @@ -731,7 +753,12 @@ func_set_qualname(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) return -1; } handle_func_event(PyFunction_EVENT_MODIFY_QUALNAME, (PyFunctionObject *) op, value); - Py_XSETREF(op->func_qualname, Py_NewRef(value)); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_qualname = op->func_qualname; + op->func_qualname = Py_NewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_qualname); return 0; } @@ -772,8 +799,13 @@ func_set_defaults(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) } handle_func_event(PyFunction_EVENT_MODIFY_DEFAULTS, op, value); - _PyFunction_ClearVersion(op); - Py_XSETREF(op->func_defaults, Py_XNewRef(value)); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, op); + PyObject *old_defaults = op->func_defaults; + op->func_defaults = Py_XNewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_defaults); return 0; } @@ -815,8 +847,13 @@ func_set_kwdefaults(PyObject *self, PyObject *value, void *Py_UNUSED(ignored)) } handle_func_event(PyFunction_EVENT_MODIFY_KWDEFAULTS, op, value); - _PyFunction_ClearVersion(op); - Py_XSETREF(op->func_kwdefaults, Py_XNewRef(value)); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + func_clear_version(interp, op); + PyObject *old_kwdefaults = op->func_kwdefaults; + op->func_kwdefaults = Py_XNewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_kwdefaults); return 0; } @@ -854,12 +891,24 @@ function___annotate___set_impl(PyFunctionObject *self, PyObject *value) return -1; } if (Py_IsNone(value)) { - Py_XSETREF(self->func_annotate, value); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_annotate = self->func_annotate; + self->func_annotate = Py_NewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_annotate); return 0; } else if (PyCallable_Check(value)) { - Py_XSETREF(self->func_annotate, Py_XNewRef(value)); - Py_CLEAR(self->func_annotations); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_annotate = self->func_annotate; + self->func_annotate = Py_NewRef(value); + PyObject *old_annotations = self->func_annotations; + self->func_annotations = NULL; + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_annotate); + Py_XDECREF(old_annotations); return 0; } else { @@ -912,8 +961,15 @@ function___annotations___set_impl(PyFunctionObject *self, PyObject *value) "__annotations__ must be set to a dict object"); return -1; } - Py_XSETREF(self->func_annotations, Py_XNewRef(value)); - Py_CLEAR(self->func_annotate); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_annotations = self->func_annotations; + self->func_annotations = Py_XNewRef(value); + PyObject *old_annotate = self->func_annotate; + self->func_annotate = NULL; + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_annotations); + Py_XDECREF(old_annotate); return 0; } @@ -954,7 +1010,12 @@ function___type_params___set_impl(PyFunctionObject *self, PyObject *value) "__type_params__ must be set to a tuple"); return -1; } - Py_XSETREF(self->func_typeparams, Py_NewRef(value)); + PyInterpreterState *interp = _PyInterpreterState_GET(); + _PyEval_StopTheWorld(interp); + PyObject *old_typeparams = self->func_typeparams; + self->func_typeparams = Py_NewRef(value); + _PyEval_StartTheWorld(interp); + Py_XDECREF(old_typeparams); return 0; } diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 01ca459b2eb2b8f..500a1a1949a5a8a 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -3827,8 +3827,8 @@ handle_thread_shutdown_exception(PyThreadState *tstate) assert(tstate != NULL); assert(_PyErr_Occurred(tstate)); PyInterpreterState *interp = tstate->interp; - assert(interp->threads.head != NULL); _PyEval_StopTheWorld(interp); + assert(interp->threads.head != NULL); // We don't have to worry about locking this because the // world is stopped.