From fb2f0bbc3b35264f09cc2cb2934b7987527a6bc2 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 11 Sep 2026 14:19:35 +0200 Subject: [PATCH 01/13] gh-157265: tarfile: Honor None result of filter for link fallbacks (GH-157266) Co-authored-by: Stan Ulbrych --- Lib/tarfile.py | 4 ++- Lib/test/test_tarfile.py | 31 +++++++++++++++++-- ...-09-10-13-38-11.gh-issue-157265.-vYuMp.rst | 3 ++ 3 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst diff --git a/Lib/tarfile.py b/Lib/tarfile.py index 6e092f1dcee5e88..a4f9ce3311f6dad 100644 --- a/Lib/tarfile.py +++ b/Lib/tarfile.py @@ -2841,9 +2841,11 @@ def makelink_with_filter(self, tarinfo, targetpath, "makelink_with_filter: if filter_function is not None, " + "extraction_root must also not be None") try: - filter_function( + filtered = filter_function( unfiltered.replace(name=tarinfo.name, deep=False), extraction_root) + if filtered is None: + return filtered = filter_function(unfiltered, extraction_root) except _FILTER_ERRORS as cause: raise LinkFallbackError(tarinfo, unfiltered.name) from cause diff --git a/Lib/test/test_tarfile.py b/Lib/test/test_tarfile.py index be5abfe211fb929..598179983a7cbc5 100644 --- a/Lib/test/test_tarfile.py +++ b/Lib/test/test_tarfile.py @@ -4621,9 +4621,15 @@ def test_sneaky_hardlink_fallback(self): for filter in 'tar', 'fully_trusted': with self.subTest(filter), self.check_context(arc.open(), filter): if not os_helper.can_symlink(): - self.expect_file("a/t/dummy") - self.expect_file("b/") - self.expect_file("c/") + if filter == 'tar': + self.expect_exception( + tarfile.LinkFallbackError, + "link 'boom' would be extracted as a copy of " + + "'c/escape', which was rejected") + else: + self.expect_file("a/t/dummy") + self.expect_file("b/") + self.expect_file("c/") else: self.expect_file("a/t/dummy") self.expect_file("b/") @@ -4820,6 +4826,25 @@ def testing_filter(member, path): if os_helper.can_chmod(): self.assertFalse(path.stat().st_mode & stat.S_IWUSR) + @symlink_test + def test_extract_filters_target_none(self): + # Test that when extract() falls back to extracting (rather than + # linking) a hardlink target, the member is skipped if the filter + # returns None. + with ArchiveMaker() as arc: + arc.add('a/b/s', symlink_to='../escape') + arc.add('q', hardlink_to='a/b/s') + def filter_unsafe_members(member, path): + try: + return tarfile.data_filter(member, path) + except tarfile.FilterError as error: + return None + with self.check_context(arc.open(), filter_unsafe_members): + if os_helper.can_symlink(): + self.expect_file('a/b/s', symlink_to='../escape') + else: + self.expect_file('a/b/') # symlink is not extracted + def test_link_fallback_normalizes(self): # Make sure hardlink fallbacks work for non-normalized paths for all # filters diff --git a/Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst b/Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst new file mode 100644 index 000000000000000..ba27e47f734bfb1 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-09-10-13-38-11.gh-issue-157265.-vYuMp.rst @@ -0,0 +1,3 @@ +In :mod:`tarfile`, when extracting a link falls back to extracting a member +of the archive, skip the member when the filter function returns None when +called with the extracted member's name replaced with the link's. From 59cba59b153b2980930999d20ccf97e44fb6edf5 Mon Sep 17 00:00:00 2001 From: Duprat Date: Fri, 11 Sep 2026 14:36:33 +0200 Subject: [PATCH 02/13] gh-108549: fix asyncio.Task cancellation swallowing SystemExit and KeyboardInterrupt (#156309) --- Lib/asyncio/tasks.py | 4 +++- Lib/test/test_asyncio/test_tasks.py | 24 +++++++++++++++++++ ...-08-24-14-28-47.gh-issue-108549.XZ34WD.rst | 3 +++ Modules/_asynciomodule.c | 9 +++++-- 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index f432cf0afa895a2..8f4dce831c5ccce 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -268,7 +268,9 @@ def __step(self, exc=None): raise exceptions.InvalidStateError( f'__step(): already done: {self!r}, {exc!r}') if self._must_cancel: - if not isinstance(exc, exceptions.CancelledError): + # gh-108549: do not swallow SystemExit and KeyboardInterrupt. + if not isinstance(exc, (exceptions.CancelledError, + SystemExit, KeyboardInterrupt)): exc = self._make_cancelled_error() self._must_cancel = False self._fut_waiter = None diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index 9c111da8c27f162..fa3eaef0ac03375 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1891,6 +1891,30 @@ async def notmuch(): self.loop.run_until_complete(task), 'ko') + def test_step_dont_swallow_systemexit_or_keyboardinterrupt(self): + # see gh-108549: do not swallow SystemExit and KeyboardInterrupt + # in Task.__step when the current task must be cancelled. + async def sub_task(exc): + raise exc + + async def current_task(exc): + try: + await asyncio.create_task(sub_task(exc)) + except exc: + pass + except BaseException as e: + self.fail(f'{exc} is expected, instead of {type(e)}') + return "ok" + + for exc in (SystemExit, KeyboardInterrupt): + with self.subTest(exc): + t = self.new_task(self.loop, current_task(exc)) + self.assertRaises(exc, self.loop.run_until_complete, t) + t.cancel() + test_utils.run_briefly(self.loop) + self.assertTrue(not t.cancelled()) + self.assertEqual(t.result(), "ok") + def test_step_result_future(self): # If coroutine returns future, task waits on this future. diff --git a/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst new file mode 100644 index 000000000000000..c6b7605f1b0eb56 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-24-14-28-47.gh-issue-108549.XZ34WD.rst @@ -0,0 +1,3 @@ +Fix :class:`asyncio.Task`, when the task has a pending cancellation, +replace **exc** exception with a :exc:`asyncio.CancelledError` unless it is +:exc:`SystemExit` or :exc:`KeyboardInterrupt`, which must propagate unchanged. diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c index a380f8ac72b32f4..8c90b0b1517ae4e 100644 --- a/Modules/_asynciomodule.c +++ b/Modules/_asynciomodule.c @@ -3051,8 +3051,13 @@ task_step_impl(asyncio_state *state, TaskObj *task, PyObject *exc) if (task->task_must_cancel) { assert(exc != Py_None); - if (!exc || !PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError)) { - /* exc was not a CancelledError */ + /* Replace exc with a CancelledError unless it already is one, or + it is SystemExit/KeyboardInterrupt, which must propagate + unchanged (gh-108549). */ + if (!exc || + (!PyErr_GivenExceptionMatches(exc, state->asyncio_CancelledError) && + !PyErr_GivenExceptionMatches(exc, PyExc_KeyboardInterrupt) && + !PyErr_GivenExceptionMatches(exc, PyExc_SystemExit))) { exc = create_cancelled_error(state, (FutureObj*)task); if (!exc) { From 1c641ff2c3e57acaae27d36b6b30839e585ac090 Mon Sep 17 00:00:00 2001 From: Shardul Deshpande Date: Fri, 11 Sep 2026 18:12:45 +0530 Subject: [PATCH 03/13] gh-157157: Make test_taskgroup_cancel_keeps_outer_cancellation deterministic (#157158) --- Lib/test/test_asyncio/test_taskgroups.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_asyncio/test_taskgroups.py b/Lib/test/test_asyncio/test_taskgroups.py index 1515672393816b4..252504c6afb3fbd 100644 --- a/Lib/test/test_asyncio/test_taskgroups.py +++ b/Lib/test/test_asyncio/test_taskgroups.py @@ -1200,11 +1200,17 @@ async def child(tg): async def test_taskgroup_cancel_keeps_outer_cancellation(self): # gh-155433: any cancellation from outside the group must propagate. + cancelling = asyncio.Event() + release = asyncio.Event() + async def child(): try: await asyncio.sleep(10) finally: - await asyncio.sleep(0.1) + # The group is cancelling: it has cancelled its parent task + # and is waiting for this task to finish. + cancelling.set() + await release.wait() async def body(): async with asyncio.TaskGroup() as tg: @@ -1213,8 +1219,9 @@ async def body(): tg.cancel() task = asyncio.create_task(body()) - await asyncio.sleep(0.01) + await cancelling.wait() task.cancel('message') + release.set() with self.assertRaises(asyncio.CancelledError) as cm: await task self.assertEqual('message', cm.exception.args[0]) From 392ad7ed46a4cbaa4bfe0bdc4a38ccd2911a599d Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:50:35 +0300 Subject: [PATCH 04/13] gh-157213: Fix stale asyncio.gather() edges in the await graph (#157214) --- Lib/asyncio/tasks.py | 9 ++++++++ Lib/test/test_asyncio/test_graph.py | 23 +++++++++++++++++++ Lib/test/test_asyncio/test_tasks.py | 19 +++++++++++++++ ...-09-09-13-46-38.gh-issue-157213.rYg7pO.rst | 2 ++ 4 files changed, 53 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 8f4dce831c5ccce..3ef3a578d93ab49 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -772,6 +772,11 @@ def cancel(self, msg=None): return ret +def _discard_awaited_by(children, waiter, outer): + for fut in children: + futures.future_discard_from_awaited_by(fut, waiter) + + def gather(*coros_or_futures, return_exceptions=False): """Return a future aggregating results from the given coroutines/futures. @@ -905,6 +910,10 @@ def _done_callback(fut, cur_task=cur_task): children.append(fut) outer = _GatheringFuture(children, loop=loop) + if cur_task is not None: + # gh-157213: a child outliving gather() must lose the awaited-by edge + outer.add_done_callback( + functools.partial(_discard_awaited_by, children, cur_task)) # Run done callbacks after GatheringFuture created so any post-processing # can be performed at this point # optimization: in the special case that *all* futures finished eagerly, diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index 36841672e1f0f65..f0fda848927dd40 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -202,6 +202,29 @@ async def main(): ] ]) + async def test_stack_gather_survivor(self): + # gh-157213: a child that outlives gather() must not be shown as awaited + + async def fail(): + raise ValueError + + async def survivor(): + await asyncio.Future() + + t = asyncio.create_task(survivor(), name='survivor') + with self.assertRaises(ValueError): + await asyncio.gather(t, fail()) + + self.assertEqual(capture_test_stack(fut=t)[0], [ + 'T', + ['a survivor'], + [] + ]) + + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + async def test_stack_shield(self): stack_for_shield = None diff --git a/Lib/test/test_asyncio/test_tasks.py b/Lib/test/test_asyncio/test_tasks.py index fa3eaef0ac03375..86d90359fa4e585 100644 --- a/Lib/test/test_asyncio/test_tasks.py +++ b/Lib/test/test_asyncio/test_tasks.py @@ -1231,6 +1231,25 @@ async def coro(): self.loop.run_until_complete(self.new_task(self.loop, coro())) + def test_gather_discards_awaited_by_for_pending(self): + # gh-157213: a child outliving gather() must lose the awaited-by edge + async def fail(): + raise ValueError + + async def survivor(): + await asyncio.Future() + + async def coro(): + t = self.new_task(self.loop, survivor()) + with self.assertRaises(ValueError): + await asyncio.gather(t, fail()) + self.assertFalse(t._asyncio_awaited_by) + t.cancel() + with self.assertRaises(asyncio.CancelledError): + await t + + self.loop.run_until_complete(self.new_task(self.loop, coro())) + def test_wait_really_done(self): # there is possibility that some tasks in the pending list # became done but their callbacks haven't all been called yet diff --git a/Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst b/Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst new file mode 100644 index 000000000000000..801a76c3f3e7ad6 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-09-13-46-38.gh-issue-157213.rYg7pO.rst @@ -0,0 +1,2 @@ +Fix :func:`asyncio.gather` leaving stale await-graph edges on children that +outlive it. From 9ab9181509fdf0f2af9f3d8204272e3bd37b633b Mon Sep 17 00:00:00 2001 From: thexai <58434170+thexai@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:35:18 +0200 Subject: [PATCH 05/13] gh-152433: Windows: allow build ``overlapped.c`` for UWP (#153036) --- ...026-07-04-18-57-47.gh-issue-152433.tO5k5t.rst | 1 + Modules/overlapped.c | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 Misc/NEWS.d/next/Windows/2026-07-04-18-57-47.gh-issue-152433.tO5k5t.rst diff --git a/Misc/NEWS.d/next/Windows/2026-07-04-18-57-47.gh-issue-152433.tO5k5t.rst b/Misc/NEWS.d/next/Windows/2026-07-04-18-57-47.gh-issue-152433.tO5k5t.rst new file mode 100644 index 000000000000000..fa814a5fb3bbc0a --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-07-04-18-57-47.gh-issue-152433.tO5k5t.rst @@ -0,0 +1 @@ +Allow build :mod:`!_overlapped` module for Universal Windows Platform. diff --git a/Modules/overlapped.c b/Modules/overlapped.c index 646cb66605295e7..f8b654594ebfc66 100644 --- a/Modules/overlapped.c +++ b/Modules/overlapped.c @@ -35,6 +35,10 @@ #define T_HANDLE T_POINTER +#ifndef HasOverlappedIoCompleted +#define HasOverlappedIoCompleted(lpOverlapped) (lpOverlapped)->Internal != STATUS_PENDING +#endif + /*[python input] class pointer_converter(CConverter): format_unit = '"F_POINTER"' @@ -351,6 +355,9 @@ _overlapped_RegisterWaitWithQueue_impl(PyObject *module, HANDLE Object, DWORD Milliseconds) /*[clinic end generated code: output=c2ace732e447fe45 input=2dd4efee44abe8ee]*/ { +#ifndef MS_WINDOWS_DESKTOP + return NULL; +#else HANDLE NewWaitObject; struct PostCallbackData data = {CompletionPort, Overlapped}, *pdata; @@ -373,6 +380,7 @@ _overlapped_RegisterWaitWithQueue_impl(PyObject *module, HANDLE Object, } return Py_BuildValue(F_HANDLE, NewWaitObject); +#endif } /*[clinic input] @@ -388,6 +396,9 @@ static PyObject * _overlapped_UnregisterWait_impl(PyObject *module, HANDLE WaitHandle) /*[clinic end generated code: output=ec90cd955a9a617d input=a56709544cb2df0f]*/ { +#ifndef MS_WINDOWS_DESKTOP + Py_RETURN_NONE; +#else BOOL ret; Py_BEGIN_ALLOW_THREADS @@ -397,6 +408,7 @@ _overlapped_UnregisterWait_impl(PyObject *module, HANDLE WaitHandle) if (!ret) return SetFromWindowsErr(0); Py_RETURN_NONE; +#endif } /*[clinic input] @@ -414,6 +426,9 @@ _overlapped_UnregisterWaitEx_impl(PyObject *module, HANDLE WaitHandle, HANDLE Event) /*[clinic end generated code: output=2e3d84c1d5f65b92 input=953cddc1de50fab9]*/ { +#ifndef MS_WINDOWS_DESKTOP + Py_RETURN_NONE; +#else BOOL ret; Py_BEGIN_ALLOW_THREADS @@ -423,6 +438,7 @@ _overlapped_UnregisterWaitEx_impl(PyObject *module, HANDLE WaitHandle, if (!ret) return SetFromWindowsErr(0); Py_RETURN_NONE; +#endif } /* From 5172fa4cd9f7063b2d3b774dc4a5c0e9eceda612 Mon Sep 17 00:00:00 2001 From: thexai <58434170+thexai@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:35:34 +0200 Subject: [PATCH 06/13] gh-152433: Windows: make ``dynload_win.c`` UWP compatible (#152797) --- ...-07-01-17-31-05.gh-issue-152433.dod-DF.rst | 1 + Python/dynload_win.c | 34 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2026-07-01-17-31-05.gh-issue-152433.dod-DF.rst diff --git a/Misc/NEWS.d/next/Windows/2026-07-01-17-31-05.gh-issue-152433.dod-DF.rst b/Misc/NEWS.d/next/Windows/2026-07-01-17-31-05.gh-issue-152433.dod-DF.rst new file mode 100644 index 000000000000000..e948f5508396ea6 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-07-01-17-31-05.gh-issue-152433.dod-DF.rst @@ -0,0 +1 @@ +make ``dynload_win.c`` UWP compatible. diff --git a/Python/dynload_win.c b/Python/dynload_win.c index 1c2544e94160ac2..423a2d2ef21e903 100644 --- a/Python/dynload_win.c +++ b/Python/dynload_win.c @@ -164,12 +164,16 @@ _Py_CheckPython3(void) static int python3_checked = 0; static HANDLE hPython3; #define MAXPATHLEN 512 - wchar_t py3path[MAXPATHLEN+1]; if (python3_checked) { return hPython3 != NULL; } python3_checked = 1; +#ifndef MS_WINDOWS_DESKTOP + // LoadPackagedLibrary doesn't accept absolute paths so load dll name from current app dir + hPython3 = LoadPackagedLibrary(PY3_DLLNAME, 0); +#else + wchar_t py3path[MAXPATHLEN + 1]; /* If there is a python3.dll next to the python3y.dll, use that DLL */ if (PyWin_DLLhModule && GetModuleFileNameW(PyWin_DLLhModule, py3path, MAXPATHLEN)) { @@ -202,6 +206,8 @@ _Py_CheckPython3(void) hPython3 = LoadLibraryExW(py3path, NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS); } } +#endif + return hPython3 != NULL; #undef MAXPATHLEN #endif /* PY3_DLLNAME */ @@ -272,6 +278,20 @@ _Py_CheckPython3t(void) #endif /* Py_ENABLE_SHARED */ +static wchar_t* _Py_AbsolutePath_To_RelativePath(wchar_t* abs_path) +{ + wchar_t* rel_path = NULL; + wchar_t process_path[512] = { 0 }; + if (GetModuleFileNameW(NULL, process_path, 512)) + { + wchar_t* path = wcsrchr(process_path, L'\\'); + path[1] = L'\0'; // strip process name + if (wcsstr(abs_path, process_path)) + rel_path = &abs_path[wcslen(process_path)]; + } + return rel_path; +} + dl_funcptr _PyImport_FindSharedFuncptrWindows(const char *prefix, const char *shortname, PyObject *pathname, FILE *fp) @@ -299,14 +319,24 @@ dl_funcptr _PyImport_FindSharedFuncptrWindows(const char *prefix, old_mode = SetErrorMode(SEM_FAILCRITICALERRORS); #endif + Py_BEGIN_ALLOW_THREADS +#ifndef MS_WINDOWS_DESKTOP + // UWP does not allow absolute paths due security restrictions. + // If path is contained inside process path (sub folder), use the relative path instead. + wchar_t* rel_path = _Py_AbsolutePath_To_RelativePath(wpathname); + if (rel_path) + hDLL = LoadPackagedLibrary(rel_path, 0); + else + hDLL = LoadPackagedLibrary(wpathname, 0); +#else /* bpo-36085: We use LoadLibraryEx with restricted search paths to avoid DLL preloading attacks and enable use of the AddDllDirectory function. We add SEARCH_DLL_LOAD_DIR to ensure DLLs adjacent to the PYD are preferred. */ - Py_BEGIN_ALLOW_THREADS hDLL = LoadLibraryExW(wpathname, NULL, LOAD_LIBRARY_SEARCH_DEFAULT_DIRS | LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR); +#endif Py_END_ALLOW_THREADS PyMem_Free(wpathname); From fff3c4fcac9ecff3d183ca46968ad8b94e364701 Mon Sep 17 00:00:00 2001 From: thexai <58434170+thexai@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:35:47 +0200 Subject: [PATCH 07/13] gh-152433: Windows: change ``winreg`` to conditional import for UWP compatibility (#154918) --- Lib/importlib/_bootstrap_external.py | 5 ++++- .../Windows/2026-07-31-18-40-43.gh-issue-152433.Pq3-U-.rst | 2 ++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Windows/2026-07-31-18-40-43.gh-issue-152433.Pq3-U-.rst diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index 176652230c092a8..484e0457eb0d732 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -33,7 +33,10 @@ _MS_WINDOWS = (sys.platform == 'win32') if _MS_WINDOWS: import nt as _os - import winreg + try: + import winreg + except ImportError: + winreg = None else: import posix as _os diff --git a/Misc/NEWS.d/next/Windows/2026-07-31-18-40-43.gh-issue-152433.Pq3-U-.rst b/Misc/NEWS.d/next/Windows/2026-07-31-18-40-43.gh-issue-152433.Pq3-U-.rst new file mode 100644 index 000000000000000..4506bccd6b249a4 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-07-31-18-40-43.gh-issue-152433.Pq3-U-.rst @@ -0,0 +1,2 @@ +Changed :mod:`!winreg` to conditional import for Universal Windows Platform +compatibility. From 4c9f2675ba13274526d2ae0d467aa05ca48b2971 Mon Sep 17 00:00:00 2001 From: Hai Zhu Date: Fri, 11 Sep 2026 21:37:26 +0800 Subject: [PATCH 08/13] gh-157247: Block perf trampoline activation when the main interpreter's JIT is enabled (#157258) --- Lib/test/test_perf_profiler.py | 21 +++++++++++++++++++ ...-09-10-08-21-07.gh-issue-157247.Zv4bz2.rst | 2 ++ Python/sysmodule.c | 4 +++- 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-08-21-07.gh-issue-157247.Zv4bz2.rst diff --git a/Lib/test/test_perf_profiler.py b/Lib/test/test_perf_profiler.py index 425c76dd01ed7c2..a2e67726e959590 100644 --- a/Lib/test/test_perf_profiler.py +++ b/Lib/test/test_perf_profiler.py @@ -318,6 +318,27 @@ def test_sys_api_with_existing_perf_jit_trampoline(self): """ assert_python_ok("-c", code, PYTHON_JIT="0") + @unittest.skipUnless( + "-D_Py_JIT" in (sysconfig.get_config_var("PY_CORE_CFLAGS") or "").split(), + "requires a real JIT (_Py_JIT)", + ) + def test_sys_api_perf_jit_backend_in_subinterpreter(self): + # gh-157247: a subinterpreter must not bypass the JIT/perf exclusion. + code = """if 1: + import sys + from contextlib import closing + from concurrent import interpreters + + assert sys._jit.is_enabled(), "expected the JIT to be enabled" + + with closing(interpreters.create()) as interp: + interp.exec( + "import sys; sys.activate_stack_trampoline('perf_jit')") + """ + rc, out, err = assert_python_failure("-c", code, PYTHON_JIT="1") + self.assertIn( + b"Cannot activate the perf trampoline if the JIT is active", err) + def is_unwinding_reliable_with_frame_pointers(): cflags = sysconfig.get_config_var("PY_CORE_CFLAGS") diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-08-21-07.gh-issue-157247.Zv4bz2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-08-21-07.gh-issue-157247.Zv4bz2.rst new file mode 100644 index 000000000000000..005132576a134bd --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-09-10-08-21-07.gh-issue-157247.Zv4bz2.rst @@ -0,0 +1,2 @@ +Fix :func:`sys.activate_stack_trampoline` allowing activation from a +subinterpreter while the JIT is enabled in the main interpreter. diff --git a/Python/sysmodule.c b/Python/sysmodule.c index 10087c1ccaac17d..03e5ac7415beb9d 100644 --- a/Python/sysmodule.c +++ b/Python/sysmodule.c @@ -2348,7 +2348,9 @@ sys_activate_stack_trampoline_impl(PyObject *module, const char *backend) { #ifdef PY_HAVE_PERF_TRAMPOLINE #ifdef _Py_JIT - if (_PyInterpreterState_GET()->jit) { + // Perf state is process-wide, and only the main interpreter can enable + // the JIT. Check it even when called from a subinterpreter (gh-157247). + if (_PyInterpreterState_Main()->jit) { PyErr_SetString(PyExc_ValueError, "Cannot activate the perf trampoline if the JIT is active"); return NULL; } From 43b4a9c4bb2a7dd45c56aaff059b7ae7a7a93efd Mon Sep 17 00:00:00 2001 From: Timofei Ivankov <128279579+deadlovelll@users.noreply.github.com> Date: Fri, 11 Sep 2026 16:54:27 +0300 Subject: [PATCH 09/13] gh-157058: Fix missing awaited-by edge in asyncio.wait_for(fut, 0) (#157059) --- Lib/asyncio/tasks.py | 5 ++++ Lib/test/test_asyncio/test_graph.py | 27 +++++++++++++++++++ ...-09-07-12-24-11.gh-issue-157058.2SoQU7.rst | 2 ++ 3 files changed, 34 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index 3ef3a578d93ab49..29c5d9af9b4029c 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -543,6 +543,10 @@ async def _cancel_and_wait(fut): cb = functools.partial(_release_waiter, waiter) fut.add_done_callback(cb) + # gh-157058: awaiting the waiter leaves no edge on fut, add it here + cur_task = current_task() + futures.future_add_to_awaited_by(fut, cur_task) + try: fut.cancel() # We cannot wait on *fut* directly to make @@ -550,6 +554,7 @@ async def _cancel_and_wait(fut): await waiter finally: fut.remove_done_callback(cb) + futures.future_discard_from_awaited_by(fut, cur_task) class _AsCompletedIterator: diff --git a/Lib/test/test_asyncio/test_graph.py b/Lib/test/test_asyncio/test_graph.py index f0fda848927dd40..a442a346ff06d91 100644 --- a/Lib/test/test_asyncio/test_graph.py +++ b/Lib/test/test_asyncio/test_graph.py @@ -173,6 +173,33 @@ class FakeCoro: self.assertEqual(len(result.call_stack), 2) + async def test_stack_wait_for_non_positive_timeout(self): + # gh-157058: wait_for(fut, 0) must still record the waiter + cleanup = asyncio.Future() + + async def worker(): + try: + await asyncio.Future() + finally: + await cleanup + + async def probe(t): + await asyncio.wait_for(t, 0) + + t = asyncio.ensure_future(worker()) + p = asyncio.create_task(probe(t), name='probe') + for _ in range(5): + await asyncio.sleep(0) + + stack = capture_test_stack(fut=t) + + cleanup.set_result(None) + await asyncio.gather(p, t, return_exceptions=True) + + self.assertEqual(stack[0][2], [ + ['T', ['a _cancel_and_wait', 'a wait_for', 'a probe'], []], + ]) + async def test_stack_gather(self): stack_for_deep = None diff --git a/Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst b/Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst new file mode 100644 index 000000000000000..9122ae0919dcb11 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-07-12-24-11.gh-issue-157058.2SoQU7.rst @@ -0,0 +1,2 @@ +:func:`asyncio.wait_for` with a non-positive timeout now records the waiter +in the call graph. From b6e8088624c078cc0504199cab4e6b328c01bbb9 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Fri, 11 Sep 2026 23:09:24 +0900 Subject: [PATCH 10/13] base64: optimize b16decode (#157310) --- Lib/base64.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/base64.py b/Lib/base64.py index fa562f74a810345..347807de00d74f3 100644 --- a/Lib/base64.py +++ b/Lib/base64.py @@ -304,7 +304,8 @@ def b16decode(s, casefold=False, *, ignorechars=b''): for b in b'abcdef': if b in s and b not in ignorechars: raise binascii.Error('Non-base16 digit found') - s = s.translate(None, delete=b'abcdef') + if ignorechars: + s = s.translate(None, delete=b'abcdef') return binascii.unhexlify(s, ignorechars=ignorechars) # From 2360bda599981745dbefb17162a7237372bde8be Mon Sep 17 00:00:00 2001 From: Owen Carey <37121709+owenthcarey@users.noreply.github.com> Date: Fri, 11 Sep 2026 08:09:48 -0700 Subject: [PATCH 11/13] gh-124205: Align descriptions of classes in 'Warning Categories' (#157255) Co-authored-by: Stan Ulbrych --- Doc/library/warnings.rst | 43 +++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/Doc/library/warnings.rst b/Doc/library/warnings.rst index d5f89102d5853bb..1e8acb0cb51c918 100644 --- a/Doc/library/warnings.rst +++ b/Doc/library/warnings.rst @@ -68,45 +68,48 @@ The following warnings category classes are currently defined: +----------------------------------+-----------------------------------------------+ | Class | Description | +==================================+===============================================+ -| :exc:`Warning` | This is the base class of all warning | -| | category classes. It is a subclass of | -| | :exc:`Exception`. | +| :exc:`Warning` | Base class for warning categories. It is a | +| | subclass of :exc:`Exception`. | +----------------------------------+-----------------------------------------------+ -| :exc:`UserWarning` | The default category for :func:`warn`. | +| :exc:`UserWarning` | Base class for warnings generated by user | +| | code. The default category for :func:`warn`. | +----------------------------------+-----------------------------------------------+ -| :exc:`DeprecationWarning` | Base category for warnings about deprecated | +| :exc:`DeprecationWarning` | Base class for warnings about deprecated | | | features when those warnings are intended for | | | other Python developers (ignored by default, | | | unless triggered by code in ``__main__``). | +----------------------------------+-----------------------------------------------+ -| :exc:`SyntaxWarning` | Base category for warnings about dubious | -| | syntactic features (typically emitted when | -| | compiling Python source code, and hence | -| | may not be suppressed by runtime filters) | +| :exc:`PendingDeprecationWarning` | Base class for warnings about features | +| | that will be deprecated in the future | +| | (ignored by default). | +----------------------------------+-----------------------------------------------+ -| :exc:`RuntimeWarning` | Base category for warnings about dubious | -| | runtime features. | +| :exc:`SyntaxWarning` | Base class for warnings about dubious syntax | +| | (typically emitted when compiling Python | +| | source code, and hence may not be suppressed | +| | by runtime filters). | +----------------------------------+-----------------------------------------------+ -| :exc:`FutureWarning` | Base category for warnings about deprecated | +| :exc:`RuntimeWarning` | Base class for warnings about dubious runtime | +| | behavior. | ++----------------------------------+-----------------------------------------------+ +| :exc:`FutureWarning` | Base class for warnings about deprecated | | | features when those warnings are intended for | | | end users of applications that are written in | | | Python. | +----------------------------------+-----------------------------------------------+ -| :exc:`PendingDeprecationWarning` | Base category for warnings about features | -| | that will be deprecated in the future | -| | (ignored by default). | -+----------------------------------+-----------------------------------------------+ -| :exc:`ImportWarning` | Base category for warnings triggered during | +| :exc:`ImportWarning` | Base class for warnings triggered during | | | the process of importing a module (ignored by | | | default). | +----------------------------------+-----------------------------------------------+ -| :exc:`UnicodeWarning` | Base category for warnings related to | +| :exc:`UnicodeWarning` | Base class for warnings related to | | | Unicode. | +----------------------------------+-----------------------------------------------+ -| :exc:`BytesWarning` | Base category for warnings related to | +| :exc:`EncodingWarning` | Base class for warnings related to encodings. | +| | See :ref:`io-encoding-warning` for details. | ++----------------------------------+-----------------------------------------------+ +| :exc:`BytesWarning` | Base class for warnings related to | | | :class:`bytes` and :class:`bytearray`. | +----------------------------------+-----------------------------------------------+ -| :exc:`ResourceWarning` | Base category for warnings related to | +| :exc:`ResourceWarning` | Base class for warnings related to | | | resource usage (ignored by default). | +----------------------------------+-----------------------------------------------+ From d85fa1af6a1e7889bcada7246144e4220428c44a Mon Sep 17 00:00:00 2001 From: Jade <68784313+jang-hs@users.noreply.github.com> Date: Sat, 12 Sep 2026 00:41:09 +0900 Subject: [PATCH 12/13] gh-115426: Fix cross-references to `socket.socket.close()` (#157295) --- Doc/library/socket.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/socket.rst b/Doc/library/socket.rst index fb9249df4be33ef..054ccc556740c20 100644 --- a/Doc/library/socket.rst +++ b/Doc/library/socket.rst @@ -1497,7 +1497,7 @@ Socket Objects of :meth:`socket.getpeername` but not the actual OS resource. Unlike :func:`socket.fromfd`, *fileno* will return the same socket and not a duplicate. This may help close a detached socket using - :meth:`socket.close`. + :meth:`~socket.socket.close`. The newly created socket is :ref:`non-inheritable `. @@ -1544,7 +1544,7 @@ Socket Objects .. versionchanged:: 3.2 Support for the :term:`context manager` protocol was added. Exiting the - context manager is equivalent to calling :meth:`~socket.close`. + context manager is equivalent to calling :meth:`~socket.socket.close`. .. method:: accept() @@ -1769,7 +1769,7 @@ Socket Objects Closing the file object returned by :meth:`makefile` won't close the original socket unless all other file objects have been closed and - :meth:`socket.close` has been called on the socket object. + :meth:`~socket.socket.close` has been called on the socket object. .. note:: From e5d4fa281c573b764b827f3defae260787024e43 Mon Sep 17 00:00:00 2001 From: Sergey B Kirpichev Date: Fri, 11 Sep 2026 18:46:59 +0300 Subject: [PATCH 13/13] gh-155526: Don't check errno in abs(complex) (#155527) abs(complex) no longer raises OverflowError if errno was set to ERANGE by some library call but abs() doesn't overflow. _Py_c_abs() no longer sets errno to zero on success, but rather leaves it unchanged. Co-authored-by: Victor Stinner Co-authored-by: hpkfft.com --- Doc/c-api/complex.rst | 3 ++ Doc/whatsnew/3.16.rst | 4 ++ Lib/test/test_capi/test_complex.py | 40 +++++++++++++------ Lib/test/test_complex.py | 25 ++++++++++++ ...-09-10-15-19-16.gh-issue-155526.kMKhyp.rst | 2 + ...-08-11-06-04-26.gh-issue-155526.W7ZHXu.rst | 3 ++ Modules/_testcapi/complex.c | 1 - Modules/cmathmodule.c | 2 +- Objects/complexobject.c | 15 ++++--- 9 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 Misc/NEWS.d/next/C_API/2026-09-10-15-19-16.gh-issue-155526.kMKhyp.rst create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-06-04-26.gh-issue-155526.W7ZHXu.rst diff --git a/Doc/c-api/complex.rst b/Doc/c-api/complex.rst index 10f96c7cb75e882..7a11e6c8a7a13b4 100644 --- a/Doc/c-api/complex.rst +++ b/Doc/c-api/complex.rst @@ -197,3 +197,6 @@ the :ref:`Number Protocol ` API or use native complex types, like Set :c:data:`errno` to :c:macro:`!ERANGE` on overflows. .. deprecated:: 3.15 + + .. versionchanged:: next + This function leaves :c:data:`errno` unchanged on success. diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 69fb89ab56446e0..1098b152e51eb41 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -1018,6 +1018,10 @@ Porting to Python 3.16 if the value cannot be marshalled. (Contributed by Serhiy Storchaka in :gh:`155907`.) +* :c:func:`_Py_c_abs` no longer sets :c:data:`errno` to zero on success, + but rather leaves it unchanged. + (Contributed by Sergey B Kirpichev in :gh:`155526`.) + Deprecated C APIs ----------------- diff --git a/Lib/test/test_capi/test_complex.py b/Lib/test/test_capi/test_complex.py index c3189a67cc7e2d3..0aca6006bec06be 100644 --- a/Lib/test/test_capi/test_complex.py +++ b/Lib/test/test_capi/test_complex.py @@ -281,18 +281,34 @@ def test_py_c_abs(self): # Test _Py_c_abs() _py_c_abs = _testcapi._py_c_abs - self.assertEqual(_py_c_abs(-1), (1.0, 0)) - self.assertEqual(_py_c_abs(1j), (1.0, 0)) - - self.assertEqual(_py_c_abs(complex('+inf+1j')), (INF, 0)) - self.assertEqual(_py_c_abs(complex('-inf+1j')), (INF, 0)) - self.assertEqual(_py_c_abs(complex('1.25+infj')), (INF, 0)) - self.assertEqual(_py_c_abs(complex('1.25-infj')), (INF, 0)) - - self.assertTrue(isnan(_py_c_abs(complex('1.25+nanj'))[0])) - self.assertTrue(isnan(_py_c_abs(complex('nan-1j'))[0])) - - self.assertEqual(_py_c_abs(complex(*[DBL_MAX]*2))[1], errno.ERANGE) + def c_abs(num): + # On success, _Py_c_abs() doesn't use errno and leaves errno + # unchanged + _testcapi.set_errno(0) + result, errno = _py_c_abs(num) + self.assertEqual(errno, 0) + return result + + try: + self.assertEqual(c_abs(-1), 1.0) + self.assertEqual(c_abs(1j), 1.0) + self.assertEqual(c_abs(complex('+inf+1j')), INF) + self.assertEqual(c_abs(complex('-inf+1j')), INF) + self.assertEqual(c_abs(complex('1.25+infj')), INF) + self.assertEqual(c_abs(complex('1.25-infj')), INF) + self.assertTrue(isnan(c_abs(complex('1.25+nanj')))) + self.assertTrue(isnan(c_abs(complex('nan-1j')))) + + # Set errno to ERANGE on overflow + _testcapi.set_errno(0) + self.assertEqual(_py_c_abs(complex(*[DBL_MAX]*2)), + (INF, errno.ERANGE)) + + # Preserve errno on success + _testcapi.set_errno(errno.EACCES) + self.assertEqual(_py_c_abs(1j), (1.0, errno.EACCES)) + finally: + _testcapi.set_errno(0) if __name__ == "__main__": diff --git a/Lib/test/test_complex.py b/Lib/test/test_complex.py index 3d02bb6ec2389ba..c8d348a48225981 100644 --- a/Lib/test/test_complex.py +++ b/Lib/test/test_complex.py @@ -1,6 +1,8 @@ +import errno import unittest import sys from test import support +from test.support import import_helper from test.support.testcase import ComplexesAreIdenticalMixin from test.support.numbers import ( VALID_UNDERSCORE_LITERALS, @@ -9,6 +11,7 @@ from random import random from math import isnan, copysign +import cmath import operator INF = float("inf") @@ -860,8 +863,30 @@ def test_abs(self): for num in nums: self.assertAlmostEqual((num.real**2 + num.imag**2) ** 0.5, abs(num)) + for x in 0.0, -0.0, INF, -INF, NAN: + for y in 0.0, -0.0, INF, -INF, NAN: + with self.subTest(x=x, y=y): + z = complex(x, y) + r = abs(z) + if cmath.isfinite(z): + self.assertFloatsAreIdentical(r, 0.0) + elif cmath.isinf(z): + self.assertEqual(r, INF) + else: + self.assertTrue(cmath.isnan(z)) + self.assertTrue(isnan(r)) + self.assertRaises(OverflowError, abs, complex(DBL_MAX, DBL_MAX)) + def test_abs_errno_handling(self): + _testcapi = import_helper.import_module('_testcapi') + z = complex('nan') + _testcapi.set_errno(errno.ERANGE) + try: + self.assertTrue(isnan(abs(z))) + finally: + _testcapi.set_errno(0) + def test_repr_str(self): def test(v, expected, test_fn=self.assertEqual): test_fn(repr(v), expected) diff --git a/Misc/NEWS.d/next/C_API/2026-09-10-15-19-16.gh-issue-155526.kMKhyp.rst b/Misc/NEWS.d/next/C_API/2026-09-10-15-19-16.gh-issue-155526.kMKhyp.rst new file mode 100644 index 000000000000000..1a2c2737bbaf97f --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-09-10-15-19-16.gh-issue-155526.kMKhyp.rst @@ -0,0 +1,2 @@ +:c:func:`_Py_c_abs` no longer sets :c:data:`errno` to zero on success, but +rather leaves it unchanged. Patch by Sergey B Kirpichev. diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-06-04-26.gh-issue-155526.W7ZHXu.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-06-04-26.gh-issue-155526.W7ZHXu.rst new file mode 100644 index 000000000000000..ae0c08973a8195b --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-11-06-04-26.gh-issue-155526.W7ZHXu.rst @@ -0,0 +1,3 @@ +Fix spurious :exc:`OverflowError` for ``abs(nanj)`` in case :c:data:`errno` was +previously set to :c:macro:`!ERANGE` by some library call. +Patch by Sergey B Kirpichev. diff --git a/Modules/_testcapi/complex.c b/Modules/_testcapi/complex.c index fb5234d03cf0676..f1bbeb4804a1918 100644 --- a/Modules/_testcapi/complex.c +++ b/Modules/_testcapi/complex.c @@ -76,7 +76,6 @@ _py_c_abs(PyObject *Py_UNUSED(module), PyObject* obj) return NULL; } - errno = 0; res = _Py_c_abs(complex); return Py_BuildValue("di", res, errno); } diff --git a/Modules/cmathmodule.c b/Modules/cmathmodule.c index f6e1475b00ecfbb..e756b550e7753e1 100644 --- a/Modules/cmathmodule.c +++ b/Modules/cmathmodule.c @@ -1029,8 +1029,8 @@ cmath_polar_impl(PyObject *module, Py_complex z) { double r, phi; - errno = 0; phi = atan2(z.imag, z.real); /* should not cause any exception */ + errno = 0; r = _Py_c_abs(z); /* sets errno to ERANGE on overflow */ if (errno != 0) return math_error(); diff --git a/Objects/complexobject.c b/Objects/complexobject.c index 9328baf013c972a..4d2b5dc8e4613f3 100644 --- a/Objects/complexobject.c +++ b/Objects/complexobject.c @@ -379,8 +379,9 @@ c_powi(Py_complex x, long n) double _Py_c_abs(Py_complex z) { - /* sets errno = ERANGE on overflow; otherwise errno = 0 */ + /* sets errno = ERANGE on overflow */ double result; + int saved_errno = errno; if (!isfinite(z.real) || !isfinite(z.imag)) { /* C99 rules: if either the real or the imaginary part is an @@ -388,23 +389,24 @@ _Py_c_abs(Py_complex z) NaN. */ if (isinf(z.real)) { result = fabs(z.real); - errno = 0; + errno = saved_errno; return result; } if (isinf(z.imag)) { result = fabs(z.imag); - errno = 0; + errno = saved_errno; return result; } /* either the real or imaginary part is a NaN, and neither is infinite. Result should be NaN. */ + errno = saved_errno; return Py_NAN; } result = hypot(z.real, z.imag); if (!isfinite(result)) errno = ERANGE; else - errno = 0; + errno = saved_errno; return result; } @@ -812,7 +814,10 @@ static PyObject * complex_abs(PyObject *op) { PyComplexObject *v = _PyComplexObject_CAST(op); - double result = _Py_c_abs(v->cval); + double result; + + errno = 0; + result = _Py_c_abs(v->cval); if (errno == ERANGE) { PyErr_SetString(PyExc_OverflowError, "absolute value too large");