]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-69573: Check the return value of msvcrt._putch() and _putwch() (GH-154364)
authorJason G <41053218+gianghungtien@users.noreply.github.com>
Thu, 23 Jul 2026 15:52:42 +0000 (22:52 +0700)
committerGitHub <noreply@github.com>
Thu, 23 Jul 2026 15:52:42 +0000 (15:52 +0000)
Doc/library/msvcrt.rst
Lib/test/test_msvcrt.py
Misc/NEWS.d/next/Windows/2026-07-21-10-42-18.gh-issue-69573.Kv7QpX.rst [new file with mode: 0644]
PC/msvcrtmodule.c

index 6b49c1a9ccd6e110d00ce13c5ba0d470f0ac77b8..f4a1125554a05133e9edf7e21935578a31b1cbea 100644 (file)
@@ -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)
 
index fef86ce323e54d55f1d9989b3b59b9fa6b0a99a0..a588b0ff46014a113f2fbeaa48d3a0d0a961e573 100644 (file)
@@ -61,6 +61,19 @@ c = '\u5b57'  # unicode CJK char (meaning 'character') for 'wide-char' tests
 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 @@ class TestConsoleIO(unittest.TestCase):
     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 (file)
index 0000000..4417180
--- /dev/null
@@ -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.
index 8826e7e85a7f5abfa53c965658e062ef47309db5..02f16d41b1457b1855a21478c87c8b3fa5d39a92 100644 (file)
@@ -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 */