From eb26e24a5eb2c1fc69695ed6f17ee5ded5f103c5 Mon Sep 17 00:00:00 2001 From: Jason G <41053218+gianghungtien@users.noreply.github.com> Date: Tue, 21 Jul 2026 20:50:20 +0700 Subject: [PATCH] gh-69573: Check the return value of _putch() and _putwch() in msvcrt msvcrt.putch() and msvcrt.putwch() discarded the value returned by the underlying CRT functions, so a failure was silently ignored. The most common case is a process that has no console attached, for example when running under pythonw.exe: _putch() then returns EOF and _putwch() returns WEOF without writing anything. Both functions now raise OSError on failure, which is consistent with ungetch() and ungetwch() in the same module. The CRT sets neither errno nor the Windows last error for this failure, so a generic OSError is raised when no error code is available. --- Doc/library/msvcrt.rst | 12 +++++- Lib/test/test_msvcrt.py | 37 +++++++++++++++++++ ...6-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst | 4 ++ PC/msvcrtmodule.c | 35 ++++++++++++++++-- 4 files changed, 84 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst diff --git a/Doc/library/msvcrt.rst b/Doc/library/msvcrt.rst index 6b49c1a9ccd6e11..f4a1125554a0513 100644 --- a/Doc/library/msvcrt.rst +++ b/Doc/library/msvcrt.rst @@ -135,13 +135,23 @@ Console I/O .. function:: putch(char) - Print the byte string *char* to the console without buffering. + Print the byte string *char* to the console without buffering. Raises + :exc:`OSError` on failure, for example when the process has no console + attached. + + .. versionchanged:: next + Failures are now reported by raising :exc:`OSError` instead of being + silently ignored. .. function:: putwch(unicode_char) Wide char variant of :func:`putch`, accepting a Unicode value. + .. versionchanged:: next + Failures are now reported by raising :exc:`OSError` instead of being + silently ignored. + .. function:: ungetch(char) diff --git a/Lib/test/test_msvcrt.py b/Lib/test/test_msvcrt.py index fef86ce323e54d5..a588b0ff46014a1 100644 --- a/Lib/test/test_msvcrt.py +++ b/Lib/test/test_msvcrt.py @@ -61,6 +61,19 @@ def test_get_osfhandle(self): c_encoded = b'\x57\x5b' # utf-16-le (which windows internally used) encoded char for this CJK char +def has_console(): + # A process created without a console (for example by pythonw.exe, or with + # the DETACHED_PROCESS creation flag) cannot write to the console. + try: + with open('CONOUT$', 'w'): + return True + except OSError: + return False + + +requires_console = unittest.skipUnless(has_console(), 'requires a console') + + class TestConsoleIO(unittest.TestCase): # CREATE_NEW_CONSOLE creates a "popup" window. @requires_resource('gui') @@ -106,12 +119,36 @@ def test_getche(self): def test_getwche(self): self.check_getwch('getwche') + @requires_console def test_putch(self): msvcrt.putch(b'c') + @requires_console def test_putwch(self): msvcrt.putwch(c) + def test_putch_without_console(self): + # gh-69573: putch() and putwch() must report the error instead of + # silently ignoring it when the process has no console attached. + code = dedent(''' + import msvcrt + import sys + + for name, arg in (('putch', b'c'), ('putwch', 'c')): + func = getattr(msvcrt, name) + try: + func(arg) + except OSError: + pass + else: + sys.exit(f'msvcrt.{name}() did not raise OSError') + ''') + # DETACHED_PROCESS: the child process is created without a console. + proc = subprocess.run([sys.executable, '-c', code], + creationflags=subprocess.DETACHED_PROCESS, + capture_output=True, text=True) + self.assertEqual(proc.returncode, 0, proc.stderr) + class TestOther(unittest.TestCase): def test_heap_min(self): diff --git a/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst b/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst new file mode 100644 index 000000000000000..441718032cc0086 --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst @@ -0,0 +1,4 @@ +:func:`msvcrt.putch` and :func:`msvcrt.putwch` now check the return value of +the underlying ``_putch()`` and ``_putwch()`` C functions and raise +:exc:`OSError` on failure, for example when the process has no console +attached, instead of silently ignoring the error. diff --git a/PC/msvcrtmodule.c b/PC/msvcrtmodule.c index 8826e7e85a7f5ab..02f16d41b1457b1 100644 --- a/PC/msvcrtmodule.c +++ b/PC/msvcrtmodule.c @@ -317,6 +317,22 @@ msvcrt_getwche_impl(PyObject *module) #endif /* MS_WINDOWS_DESKTOP */ +/* Raise an OSError for a failed _putch()/_putwch() call. + + These functions fail, for example, when the process has no console + attached, but the CRT reports the failure without setting errno (and + without setting the Windows last error either), so fall back to a + generic error message in that case. */ +static PyObject * +set_console_write_error(void) +{ + if (errno != 0) { + return PyErr_SetFromErrno(PyExc_OSError); + } + PyErr_SetString(PyExc_OSError, "write to console failed"); + return NULL; +} + /*[clinic input] msvcrt.putch @@ -330,9 +346,16 @@ static PyObject * msvcrt_putch_impl(PyObject *module, char char_value) /*[clinic end generated code: output=92ec9b81012d8f60 input=ec078dd10cb054d6]*/ { + int res; + _Py_BEGIN_SUPPRESS_IPH - _putch(char_value); + errno = 0; + res = _putch(char_value); _Py_END_SUPPRESS_IPH + + if (res == EOF) { + return set_console_write_error(); + } Py_RETURN_NONE; } @@ -351,11 +374,17 @@ static PyObject * msvcrt_putwch_impl(PyObject *module, int unicode_char) /*[clinic end generated code: output=a3bd1a8951d28eee input=996ccd0bbcbac4c3]*/ { + wint_t res; + _Py_BEGIN_SUPPRESS_IPH - _putwch(unicode_char); + errno = 0; + res = _putwch(unicode_char); _Py_END_SUPPRESS_IPH - Py_RETURN_NONE; + if (res == WEOF) { + return set_console_write_error(); + } + Py_RETURN_NONE; } #endif /* MS_WINDOWS_DESKTOP */