gh-86768: Raise OSError when seeking a pipe on Windows (GH-133137)

serhiy-storchaka <[email protected]> Tue, 11 Aug 2026 10:48:07 -0400 (EDT)
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/d65bf5174423a0abc288d9fa5f3ff0fcaf2bc49f
commit: d65bf5174423a0abc288d9fa5f3ff0fcaf2bc49f
branch: main
author: An Long <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-11T17:47:52+03:00
summary:

gh-86768: Raise OSError when seeking a pipe on Windows (GH-133137)

Previously os.lseek() and file seek() silently succeeded for pipes, and
seekable() wrongly returned True.

Co-authored-by: Serhiy Storchaka <[email protected]>

files:
A Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst
M Doc/whatsnew/3.16.rst
M Lib/test/test_os/test_os.py
M Lib/test/test_winapi.py
M Modules/_io/fileio.c
M Modules/posixmodule.c

diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index c16f4ca04f757f9..b017535b96979d9 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -828,6 +828,13 @@ that may require changes to your code.
   :exc:`TypeError`.
   (Contributed by Serhiy Storchaka in :gh:`152587`.)
 
+* On Windows, seeking a pipe now fails instead of silently appearing to
+  succeed: :func:`os.lseek` and :meth:`~io.IOBase.seek` raise :exc:`OSError`,
+  and :meth:`~io.IOBase.seekable` returns ``False``.  As a consequence,
+  opening a pipe in a read-write binary mode (``'r+b'`` or ``'w+b'``) now
+  raises :exc:`io.UnsupportedOperation` unless buffering is disabled.
+  (Contributed by An Long in :gh:`86768`.)
+
 
 Build changes
 =============
diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py
index 4c1ab96065587e5..7a49cfa0c29ec5b 100644
--- a/Lib/test/test_os/test_os.py
+++ b/Lib/test/test_os/test_os.py
@@ -2993,6 +2993,14 @@ def test_ftruncate(self):
     def test_lseek(self):
         self.check(os.lseek, 0, 0)
 
+    @unittest.skipUnless(hasattr(os, 'lseek'), 'test needs os.lseek()')
+    @unittest.skipUnless(hasattr(os, 'pipe'), "need os.pipe()")
+    def test_lseek_on_pipe(self):
+        rfd, wfd = os.pipe()
+        self.addCleanup(os.close, rfd)
+        self.addCleanup(os.close, wfd)
+        self.assertRaises(OSError, os.lseek, rfd, 123, os.SEEK_END)
+
     @unittest.skipUnless(hasattr(os, 'read'), 'test needs os.read()')
     def test_read(self):
         self.check(os.read, 1)
diff --git a/Lib/test/test_winapi.py b/Lib/test/test_winapi.py
index 0ae03a3bf505f73..60f7881a0b0ab99 100644
--- a/Lib/test/test_winapi.py
+++ b/Lib/test/test_winapi.py
@@ -152,7 +152,7 @@ def test_namedpipe(self):
         # Pipe instance is available, so this passes
         _winapi.WaitNamedPipe(pipe_name, 0)
 
-        with open(pipe_name, 'w+b') as pipe2:
+        with open(pipe_name, 'w+b', buffering=0) as pipe2:
             # No instances available, so this times out
             # (WinError 121 does not get mapped to TimeoutError)
             with self.assertRaises(OSError):
diff --git a/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst b/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst
new file mode 100644
index 000000000000000..6085f47c49d5ed7
--- /dev/null
+++ b/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst
@@ -0,0 +1,6 @@
+:func:`os.lseek` and :meth:`~io.IOBase.seek` of file objects now raise
+:exc:`OSError` for pipes on Windows, and :meth:`~io.IOBase.seekable` now
+returns ``False`` for them.  Previously seeking a pipe silently appeared to
+succeed.  As a consequence, opening a pipe in a read-write binary mode
+(``'r+b'`` or ``'w+b'``) now raises :exc:`io.UnsupportedOperation` unless
+buffering is disabled.
diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c
index 3aeb30dfe24a357..e8e9c132dd3465e 100644
--- a/Modules/_io/fileio.c
+++ b/Modules/_io/fileio.c
@@ -992,7 +992,14 @@ portable_lseek(fileio *self, PyObject *posobj, int whence, bool suppress_pipe_er
     Py_BEGIN_ALLOW_THREADS
     _Py_BEGIN_SUPPRESS_IPH
 #ifdef MS_WINDOWS
-    res = _lseeki64(fd, pos, whence);
+    HANDLE h = (HANDLE)_get_osfhandle(fd);
+    if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) {
+        res = -1;
+        errno = ESPIPE;
+    }
+    else {
+        res = _lseeki64(fd, pos, whence);
+    }
 #else
     res = lseek(fd, pos, whence);
 #endif
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index a9dd8647545bc9c..9e84fd400527ea2 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -12037,7 +12037,7 @@ static Py_off_t
 os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how)
 /*[clinic end generated code: output=971e1efb6b30bd2f input=32ea0788da7cb44b]*/
 {
-    Py_off_t result;
+    Py_off_t result = -1;
 
 #ifdef SEEK_SET
     /* Turn 0, 1, 2 into SEEK_{SET,CUR,END} */
@@ -12051,14 +12051,21 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how)
     Py_BEGIN_ALLOW_THREADS
     _Py_BEGIN_SUPPRESS_IPH
 #ifdef MS_WINDOWS
-    result = _lseeki64(fd, position, how);
+    HANDLE h = (HANDLE)_get_osfhandle(fd);
+    if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) {
+        errno = ESPIPE;
+    }
+    else {
+        result = _lseeki64(fd, position, how);
+    }
 #else
     result = lseek(fd, position, how);
 #endif
     _Py_END_SUPPRESS_IPH
     Py_END_ALLOW_THREADS
-    if (result < 0)
+    if (result < 0) {
         posix_error();
+    }
 
     return result;
 }

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]