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): 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; }