From 43956d89af565f0edbca5c4007a59221e921fbf0 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 28 Apr 2025 23:56:41 +0900 Subject: [PATCH 01/14] Implement os.lseek with SetFilePointerEx on Windows --- Modules/posixmodule.c | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 8d7131b2eacf956..22204ed2efa9d95 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11406,7 +11406,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=f096e754c5367504]*/ { - Py_off_t result; + Py_off_t result = 0; #ifdef SEEK_SET /* Turn 0, 1, 2 into SEEK_{SET,CUR,END} */ @@ -11420,7 +11420,36 @@ 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); + switch (how) { + case SEEK_SET: + how = FILE_BEGIN; + break; + case SEEK_CUR: + how = FILE_CURRENT; + break; + case SEEK_END: + how = FILE_END; + break; + } + HANDLE h = (HANDLE)_get_osfhandle(fd); + if (h == INVALID_HANDLE_VALUE) { + result = -1; + } + if (result >= 0) { + if (GetFileType(h) != FILE_TYPE_DISK) { + // Only file is seekable + result = -1; + } + } + if (result >= 0) { + LARGE_INTEGER distance, newdistance; + distance.QuadPart = position; + if (SetFilePointerEx(h, distance, &newdistance, how)) { + result = newdistance.QuadPart; + } else { + result = -1; + } + } #else result = lseek(fd, position, how); #endif From b1a5cbbcf16024cf38682e7290ea891db3f8b8c1 Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 29 Apr 2025 15:23:41 +0900 Subject: [PATCH 02/14] Add test --- Lib/test/test_os.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 333179a71e3cdcb..8a69b2387d0bb18 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -2577,6 +2577,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) From 7fe34903261d31412cbd599d7f7809b4208298e3 Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 29 Apr 2025 18:17:50 +0900 Subject: [PATCH 03/14] Add news entry --- .../next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst 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..6f02f60dc092a7e --- /dev/null +++ b/Misc/NEWS.d/next/Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst @@ -0,0 +1,2 @@ +:exc:`OSError` will be raised if call :func:`os.lseek` with non seekable +object on Windows. From 9a0d10f311c9edf7ee49146df46a82d6f048288a Mon Sep 17 00:00:00 2001 From: AN Long Date: Wed, 30 Apr 2025 00:26:26 +0900 Subject: [PATCH 04/14] Check error --- Modules/posixmodule.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 22204ed2efa9d95..3437cdf30b23529 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -219,6 +219,7 @@ # if defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM) # define HAVE_SYMLINK # endif /* MS_WINDOWS_DESKTOP | MS_WINDOWS_SYSTEM */ +extern int winerror_to_errno(int); #endif @@ -11438,6 +11439,7 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) if (result >= 0) { if (GetFileType(h) != FILE_TYPE_DISK) { // Only file is seekable + errno = ESPIPE; result = -1; } } @@ -11455,8 +11457,14 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) #endif _Py_END_SUPPRESS_IPH Py_END_ALLOW_THREADS - if (result < 0) + if (result < 0) { +#ifdef MS_WINDOWS + if (errno == 0) { + errno = winerror_to_errno(GetLastError()); + } +#endif posix_error(); + } return result; } From 2a8efe28d8df365091faf21b90097cd53cd1982c Mon Sep 17 00:00:00 2001 From: AN Long Date: Sat, 10 May 2025 22:07:58 +0900 Subject: [PATCH 05/14] Simplify the implementation --- Modules/posixmodule.c | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 3437cdf30b23529..75554b7000072a7 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11421,17 +11421,6 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) Py_BEGIN_ALLOW_THREADS _Py_BEGIN_SUPPRESS_IPH #ifdef MS_WINDOWS - switch (how) { - case SEEK_SET: - how = FILE_BEGIN; - break; - case SEEK_CUR: - how = FILE_CURRENT; - break; - case SEEK_END: - how = FILE_END; - break; - } HANDLE h = (HANDLE)_get_osfhandle(fd); if (h == INVALID_HANDLE_VALUE) { result = -1; @@ -11444,13 +11433,7 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) } } if (result >= 0) { - LARGE_INTEGER distance, newdistance; - distance.QuadPart = position; - if (SetFilePointerEx(h, distance, &newdistance, how)) { - result = newdistance.QuadPart; - } else { - result = -1; - } + result = _lseeki64(fd, position, how); } #else result = lseek(fd, position, how); @@ -11458,11 +11441,6 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) _Py_END_SUPPRESS_IPH Py_END_ALLOW_THREADS if (result < 0) { -#ifdef MS_WINDOWS - if (errno == 0) { - errno = winerror_to_errno(GetLastError()); - } -#endif posix_error(); } From 2f851cf874c17d9eb9d877fcdd64c207ffbd42dd Mon Sep 17 00:00:00 2001 From: AN Long Date: Sun, 11 May 2025 22:22:22 +0900 Subject: [PATCH 06/14] Simplify the implmentation --- Modules/posixmodule.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 75554b7000072a7..c790e46ce8ec4c6 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11407,7 +11407,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=f096e754c5367504]*/ { - Py_off_t result = 0; + Py_off_t result = -1; #ifdef SEEK_SET /* Turn 0, 1, 2 into SEEK_{SET,CUR,END} */ @@ -11422,19 +11422,14 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) _Py_BEGIN_SUPPRESS_IPH #ifdef MS_WINDOWS HANDLE h = (HANDLE)_get_osfhandle(fd); - if (h == INVALID_HANDLE_VALUE) { - result = -1; - } - if (result >= 0) { - if (GetFileType(h) != FILE_TYPE_DISK) { + if (h != INVALID_HANDLE_VALUE) { + if (GetFileType(h) == FILE_TYPE_DISK) { + result = _lseeki64(fd, position, how); + } else { // Only file is seekable errno = ESPIPE; - result = -1; } } - if (result >= 0) { - result = _lseeki64(fd, position, how); - } #else result = lseek(fd, position, how); #endif From 6aad19e8a0537df016bc621954b7c3311f42e715 Mon Sep 17 00:00:00 2001 From: AN Long Date: Sun, 11 May 2025 23:39:14 +0900 Subject: [PATCH 07/14] Update by review --- Modules/posixmodule.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index c790e46ce8ec4c6..14580ec7c4c8007 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -219,7 +219,6 @@ # if defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM) # define HAVE_SYMLINK # endif /* MS_WINDOWS_DESKTOP | MS_WINDOWS_SYSTEM */ -extern int winerror_to_errno(int); #endif From 7e1ca5d9208578abb29b2da91c69e72f4f5d7fde Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 9 Feb 2026 23:06:11 +0900 Subject: [PATCH 08/14] Only reject FILE_TYPE_PIPE --- Modules/posixmodule.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 156795376b126aa..1a1a10e532b5612 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11931,11 +11931,10 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) #ifdef MS_WINDOWS HANDLE h = (HANDLE)_get_osfhandle(fd); if (h != INVALID_HANDLE_VALUE) { - if (GetFileType(h) == FILE_TYPE_DISK) { - result = _lseeki64(fd, position, how); - } else { - // Only file is seekable + if (GetFileType(h) == FILE_TYPE_PIPE) { errno = ESPIPE; + } else { + result = _lseeki64(fd, position, how); } } #else From 99ba2b0fb3e307ad8c4c3a6c04da3a660ae28668 Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 9 Feb 2026 23:09:36 +0900 Subject: [PATCH 09/14] Check in fileio.c also --- Modules/_io/fileio.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index 5d7741fdd830a53..c0baef31b88a39f 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -989,7 +989,13 @@ 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 From b17892bf3456f709fa224bb4df1428291e1a6aef Mon Sep 17 00:00:00 2001 From: AN Long Date: Mon, 9 Feb 2026 23:39:11 +0900 Subject: [PATCH 10/14] Format codes --- Modules/posixmodule.c | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 1a1a10e532b5612..62de1417ce4f32f 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11930,12 +11930,10 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) _Py_BEGIN_SUPPRESS_IPH #ifdef MS_WINDOWS HANDLE h = (HANDLE)_get_osfhandle(fd); - if (h != INVALID_HANDLE_VALUE) { - if (GetFileType(h) == FILE_TYPE_PIPE) { - errno = ESPIPE; - } else { - result = _lseeki64(fd, position, how); - } + if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) { + errno = ESPIPE; + } else { + result = _lseeki64(fd, position, how); } #else result = lseek(fd, position, how); From f35b44af5eaa3a32be17c9d7133a11ccbf1eb532 Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 10 Feb 2026 00:18:29 +0900 Subject: [PATCH 11/14] Apply suggestions from code review Co-authored-by: Serhiy Storchaka --- Modules/_io/fileio.c | 3 ++- Modules/posixmodule.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c index c0baef31b88a39f..14f3285e30eb640 100644 --- a/Modules/_io/fileio.c +++ b/Modules/_io/fileio.c @@ -993,7 +993,8 @@ portable_lseek(fileio *self, PyObject *posobj, int whence, bool suppress_pipe_er if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) { res = -1; errno = ESPIPE; - } else { + } + else { res = _lseeki64(fd, pos, whence); } #else diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c index 62de1417ce4f32f..ce6bf7c79dca3a3 100644 --- a/Modules/posixmodule.c +++ b/Modules/posixmodule.c @@ -11932,7 +11932,8 @@ os_lseek_impl(PyObject *module, int fd, Py_off_t position, int how) HANDLE h = (HANDLE)_get_osfhandle(fd); if (h != INVALID_HANDLE_VALUE && GetFileType(h) == FILE_TYPE_PIPE) { errno = ESPIPE; - } else { + } + else { result = _lseeki64(fd, position, how); } #else From d0a2498d28aa37f36d7ea3458339dce296926a38 Mon Sep 17 00:00:00 2001 From: AN Long Date: Tue, 10 Feb 2026 00:45:34 +0900 Subject: [PATCH 12/14] Remove the buffering in named pipe test because pipe is not seekable --- Lib/test/test_winapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_winapi.py b/Lib/test/test_winapi.py index a1c0b80d47e4d44..d2add8621b9b835 100644 --- a/Lib/test/test_winapi.py +++ b/Lib/test/test_winapi.py @@ -144,7 +144,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): From cdb1b1dfde3e45de7181b2663b6a5580d859ffd2 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 16:53:26 +0300 Subject: [PATCH 13/14] Describe the change and its consequences in the NEWS entry --- .../Windows/2025-04-29-17-55-55.gh-issue-86768.uIDTHc.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 index 6f02f60dc092a7e..6085f47c49d5ed7 100644 --- 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 @@ -1,2 +1,6 @@ -:exc:`OSError` will be raised if call :func:`os.lseek` with non seekable -object on Windows. +: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. From def2841709c1998830b3dbe77b29498f176ad955 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 11 Aug 2026 16:57:56 +0300 Subject: [PATCH 14/14] Add a What's New entry --- Doc/whatsnew/3.16.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 9edbccf3adef9e5..bab04e841fa861e 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -819,6 +819,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 =============