From a8e1d25355004ba8790e34dbf7fb5268674e7f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Mon, 17 Aug 2026 20:44:40 +0800 Subject: [PATCH 1/2] gh-155852: Do not cancel remaining Executor.map calls on a callable's TimeoutError (GH-155853) A TimeoutError raised by the mapped callable was re-raised like the map(timeout=...) wait timeout, aborting the iteration and cancelling the remaining calls, unlike every other exception since gh-108518. A wait timeout only occurs while the future is still running, so a TimeoutError from an already-finished future is treated as the callable's own result. --- Lib/concurrent/futures/_base.py | 11 +++++++---- Lib/test/test_concurrent_futures/executor.py | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index cc335d9aa1ea55d..e728b8e0a91f744 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -309,10 +309,13 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED): def _result_or_cancel(fut, timeout=None): try: try: - return (fut.result(timeout), None) - except TimeoutError: - raise - except BaseException as exc: + # fut.exception() returns the call's own error but raises + # TimeoutError only for a map() timeout. + exc = fut.exception(timeout) + if exc is not None: + return (None, exc) + return (fut.result(), None) + except CancelledError as exc: return (None, exc) finally: fut.cancel() diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index 5d9f27c83bf9a81..ff7bd0db0c2199c 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -29,6 +29,12 @@ def raiser(exception, msg='std'): raise exception(msg) +def timeout_on_one(x): + if x == 1: + raise TimeoutError + return x + + class FalseyBoolException(Exception): def __bool__(self): return False @@ -87,6 +93,20 @@ def test_map_exception(self): self.assertRaises(StopIteration, next, i) self.assertRaises(StopIteration, next, i) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() + def test_map_timeout_from_callable(self): + # A TimeoutError from the callable is not the map() timeout, whether + # or not a map() timeout is set. + for timeout in (None, support.SHORT_TIMEOUT): + with self.subTest(timeout=timeout): + i = self.executor.map(timeout_on_one, [0, 1, 2, 3], + timeout=timeout) + self.assertEqual(next(i), 0) + self.assertRaises(TimeoutError, next, i) + self.assertEqual(next(i), 2) + self.assertEqual(next(i), 3) + self.assertRaises(StopIteration, next, i) + @warnings_helper.ignore_fork_in_thread_deprecation_warnings() @support.requires_resource('walltime') def test_map_timeout(self): From f40043e0953323675843a3c275511596f30c80e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?tonghuaroot=20=28=E7=AB=A5=E8=AF=9D=29?= Date: Mon, 17 Aug 2026 21:02:53 +0800 Subject: [PATCH 2/2] gh-153603: Fix out-of-bounds read in the ISO-2022 decoder for an unknown charset (GH-153604) The designation-table scan compiled its terminator only under Py_DEBUG, so a release build walked off the table for an unknown charset set via setstate(). Make the terminator unconditional and report the byte as undecodable. --- Lib/test/test_multibytecodec.py | 18 ++++++++++++++++++ ...6-07-11-13-24-49.gh-issue-153603.YbKlry.rst | 3 +++ Modules/cjkcodecs/_codecs_iso2022.c | 13 +++++++------ 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-11-13-24-49.gh-issue-153603.YbKlry.rst diff --git a/Lib/test/test_multibytecodec.py b/Lib/test/test_multibytecodec.py index d7a233377bdb023..6b032fc8604eefd 100644 --- a/Lib/test/test_multibytecodec.py +++ b/Lib/test/test_multibytecodec.py @@ -306,6 +306,24 @@ def test_setstate_validates_input(self): self.assertRaises(TypeError, decoder.setstate, (b"1234", "invalid")) self.assertRaises(UnicodeDecodeError, decoder.setstate, (b"123456789", 0)) + def test_setstate_invalid_designation(self): + # gh-153603: an unknown charset designation in the state must not crash + # the decoder. 0xff is not a registered charset mark and 0x21 ('!') is + # a GL byte that triggers the designation lookup. + for name in ('iso-2022-jp', 'iso-2022-kr'): + with self.subTest(codec=name): + decoder = codecs.getincrementaldecoder(name)() + decoder.setstate((b'', 0xff)) + with self.assertRaises(UnicodeDecodeError) as cm: + decoder.decode(b'!', final=True) + self.assertEqual(cm.exception.reason, + 'illegal multibyte sequence') + self.assertEqual((cm.exception.start, cm.exception.end), (0, 1)) + # One illegal byte is reported, so error handlers still work. + decoder = codecs.getincrementaldecoder(name)(errors='replace') + decoder.setstate((b'', 0xff)) + self.assertEqual(decoder.decode(b'!', final=True), '\ufffd') + class Test_StreamReader(unittest.TestCase): def test_bug1728403(self): try: diff --git a/Misc/NEWS.d/next/Library/2026-07-11-13-24-49.gh-issue-153603.YbKlry.rst b/Misc/NEWS.d/next/Library/2026-07-11-13-24-49.gh-issue-153603.YbKlry.rst new file mode 100644 index 000000000000000..5c60a302883931c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-11-13-24-49.gh-issue-153603.YbKlry.rst @@ -0,0 +1,3 @@ +Fix a crash in the ISO-2022 decoders when decoding a byte after an unknown +charset designation is set via the decoder's ``setstate`` method. +Patch by tonghuaroot. diff --git a/Modules/cjkcodecs/_codecs_iso2022.c b/Modules/cjkcodecs/_codecs_iso2022.c index b1984df2695b172..e79e6e768445552 100644 --- a/Modules/cjkcodecs/_codecs_iso2022.c +++ b/Modules/cjkcodecs/_codecs_iso2022.c @@ -533,15 +533,16 @@ DECODER(iso2022) dsg = dsgcache; else { for (dsg = CONFIG_DESIGNATIONS; - dsg->mark != charset -#ifdef Py_DEBUG - && dsg->mark != '\0' -#endif - ; dsg++) + dsg->mark != charset && dsg->mark != '\0'; + dsg++) { /* noop */ } - assert(dsg->mark != '\0'); + if (dsg->mark == '\0') { + /* Unknown charset designation from a corrupt + setstate(); no width to trust, report one byte. */ + return 1; + } dsgcache = dsg; }