https://github.com/python/cpython/commit/8d050f98f58617c99a09e1a75a6502bcd0ad6bba
commit: 8d050f98f58617c99a09e1a75a6502bcd0ad6bba
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-05-29T18:56:00+05:30
summary:

[3.14] gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset 
(GH-150270) (#150569)

gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset (GH-150270)
(cherry picked from commit c72d5ea638731ec29723ded2d26ec7f997f06f17)

Co-authored-by: Grant Herman <[email protected]>
Co-authored-by: Victor Stinner <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-05-22-17-09-28.gh-issue-150107.GD72-D.rst
M Lib/asyncio/base_events.py
M Lib/asyncio/proactor_events.py
M Lib/asyncio/unix_events.py
M Lib/asyncio/windows_events.py
M Lib/test/test_asyncio/test_sendfile.py

diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py
index b83b84181fd24dc..d22996f562e20d2 100644
--- a/Lib/asyncio/base_events.py
+++ b/Lib/asyncio/base_events.py
@@ -963,7 +963,7 @@ async def _sock_sendfile_native(self, sock, file, offset, 
count):
             f"and file {file!r} combination")
 
     async def _sock_sendfile_fallback(self, sock, file, offset, count):
-        if offset:
+        if hasattr(file, 'seek'):
             file.seek(offset)
         blocksize = (
             min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE)
@@ -1278,7 +1278,6 @@ async def sendfile(self, transport, file, offset=0, 
count=None,
             raise RuntimeError(
                 f"fallback is disabled and native sendfile is not "
                 f"supported for transport {transport!r}")
-
         return await self._sendfile_fallback(transport, file,
                                              offset, count)
 
@@ -1287,7 +1286,7 @@ async def _sendfile_native(self, transp, file, offset, 
count):
             "sendfile syscall is not supported")
 
     async def _sendfile_fallback(self, transp, file, offset, count):
-        if offset:
+        if hasattr(file, 'seek'):
             file.seek(offset)
         blocksize = min(count, 16384) if count else 16384
         buf = bytearray(blocksize)
diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py
index f404273c3ae5c1d..6b94975d0046ea3 100644
--- a/Lib/asyncio/proactor_events.py
+++ b/Lib/asyncio/proactor_events.py
@@ -756,8 +756,7 @@ async def _sock_sendfile_native(self, sock, file, offset, 
count):
                 offset += blocksize
                 total_sent += blocksize
         finally:
-            if total_sent > 0:
-                file.seek(offset)
+            file.seek(offset)
 
     async def _sendfile_native(self, transp, file, offset, count):
         resume_reading = transp.is_reading()
diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py
index 1c1458127db5ac1..f667c9647c6407c 100644
--- a/Lib/asyncio/unix_events.py
+++ b/Lib/asyncio/unix_events.py
@@ -384,12 +384,12 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, 
sock, fileno,
             # order to simplify the common case.
             self.remove_writer(registered_fd)
         if fut.cancelled():
-            self._sock_sendfile_update_filepos(fileno, offset, total_sent)
+            self._sock_sendfile_update_filepos(fileno, offset)
             return
         if count:
             blocksize = count - total_sent
             if blocksize <= 0:
-                self._sock_sendfile_update_filepos(fileno, offset, total_sent)
+                self._sock_sendfile_update_filepos(fileno, offset)
                 fut.set_result(total_sent)
                 return
 
@@ -423,20 +423,20 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, 
sock, fileno,
                 # plain send().
                 err = exceptions.SendfileNotAvailableError(
                     "os.sendfile call failed")
-                self._sock_sendfile_update_filepos(fileno, offset, total_sent)
+                self._sock_sendfile_update_filepos(fileno, offset)
                 fut.set_exception(err)
             else:
-                self._sock_sendfile_update_filepos(fileno, offset, total_sent)
+                self._sock_sendfile_update_filepos(fileno, offset)
                 fut.set_exception(exc)
         except (SystemExit, KeyboardInterrupt):
             raise
         except BaseException as exc:
-            self._sock_sendfile_update_filepos(fileno, offset, total_sent)
+            self._sock_sendfile_update_filepos(fileno, offset)
             fut.set_exception(exc)
         else:
             if sent == 0:
                 # EOF
-                self._sock_sendfile_update_filepos(fileno, offset, total_sent)
+                self._sock_sendfile_update_filepos(fileno, offset)
                 fut.set_result(total_sent)
             else:
                 offset += sent
@@ -447,9 +447,9 @@ def _sock_sendfile_native_impl(self, fut, registered_fd, 
sock, fileno,
                                 fd, sock, fileno,
                                 offset, count, blocksize, total_sent)
 
-    def _sock_sendfile_update_filepos(self, fileno, offset, total_sent):
-        if total_sent > 0:
-            os.lseek(fileno, offset, os.SEEK_SET)
+    def _sock_sendfile_update_filepos(self, fileno, offset):
+        # After this helper runs, the source fd's lseek pointer is at offset."
+        os.lseek(fileno, offset, os.SEEK_SET)
 
     def _sock_add_cancellation_callback(self, fut, sock):
         def cb(fut):
diff --git a/Lib/asyncio/windows_events.py b/Lib/asyncio/windows_events.py
index 5f75b17d8ca649b..0bf7732136f1f8e 100644
--- a/Lib/asyncio/windows_events.py
+++ b/Lib/asyncio/windows_events.py
@@ -610,6 +610,9 @@ def sendfile(self, sock, file, offset, count):
         ov = _overlapped.Overlapped(NULL)
         offset_low = offset & 0xffff_ffff
         offset_high = (offset >> 32) & 0xffff_ffff
+        # TransmitFile ignores OVERLAPPED.Offset for handles not opened with
+        # FILE_FLAG_OVERLAPPED, so seek the CRT file pointer to match.
+        file.seek(offset)
         ov.TransmitFile(sock.fileno(),
                         msvcrt.get_osfhandle(file.fileno()),
                         offset_low, offset_high,
diff --git a/Lib/test/test_asyncio/test_sendfile.py 
b/Lib/test/test_asyncio/test_sendfile.py
index dcd963b3355ef86..7afd7de3bb936e6 100644
--- a/Lib/test/test_asyncio/test_sendfile.py
+++ b/Lib/test/test_asyncio/test_sendfile.py
@@ -228,6 +228,61 @@ def test_sock_sendfile_zero_size(self):
         self.assertEqual(ret, 0)
         self.assertEqual(self.file.tell(), 0)
 
+    def check_sock_sendfile_offset(self, data, offset, force_fallback=False):
+        sock, proto = self.prepare_socksendfile()
+        with tempfile.TemporaryFile() as f:
+            f.write(data)
+            f.flush()
+            self.assertEqual(f.tell(), len(data))
+
+            if force_fallback:
+                async def _sock_sendfile_fail(sock, file, offset, count):
+                    raise asyncio.exceptions.SendfileNotAvailableError()
+                with support.swap_attr(self.loop, '_sock_sendfile_native', 
_sock_sendfile_fail):
+                    ret = self.run_loop(self.loop.sock_sendfile(sock, f, 
offset, None))
+            else:
+                ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, 
None))
+
+            self.assertEqual(f.tell(), len(data))
+
+        sock.close()
+        self.run_loop(proto.wait_closed())
+
+        self.assertEqual(ret, len(data) - offset)
+
+
+    def test_sock_sendfile_offset(self):
+        data = b'abcdef'
+        for offset in (0, len(data) // 2, len(data)):
+            for force_fallback in (False, True):
+                with self.subTest(offset=offset, 
force_fallback=force_fallback):
+                    self.check_sock_sendfile_offset(data, offset, 
force_fallback)
+
+    def check_sendfile_offset(self, offset, fallback):
+        srv_proto, cli_proto = self.prepare_sendfile()
+        self.file.seek(123)
+        coro = self.loop.sendfile(cli_proto.transport, self.file, offset, 
fallback=fallback)
+        try:
+            ret = self.run_loop(coro)
+        except asyncio.SendfileNotAvailableError:
+            if fallback:
+                raise
+            cli_proto.transport.close()
+            self.run_loop(srv_proto.done)
+            return
+        cli_proto.transport.close()
+        self.run_loop(srv_proto.done)
+        self.assertEqual(ret, len(self.DATA) - offset)
+        self.assertEqual(srv_proto.nbytes, len(self.DATA) - offset)
+        self.assertEqual(srv_proto.data, self.DATA[offset:])
+        self.assertEqual(self.file.tell(), len(self.DATA))
+
+    def test_sendfile_offset(self):
+        for offset in (0, len(self.DATA) // 2, len(self.DATA)):
+            for fallback in (False, True):
+                with self.subTest(offset=offset, fallback=fallback):
+                    self.check_sendfile_offset(offset, fallback)
+
     def test_sock_sendfile_mix_with_regular_send(self):
         buf = b"mix_regular_send" * (4 * 1024)  # 64 KiB
         sock, proto = self.prepare_socksendfile()
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-22-17-09-28.gh-issue-150107.GD72-D.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-22-17-09-28.gh-issue-150107.GD72-D.rst
new file mode 100644
index 000000000000000..a13f249e48cc021
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-22-17-09-28.gh-issue-150107.GD72-D.rst
@@ -0,0 +1,3 @@
+:mod:`asyncio`: ``sendfile()`` and ``sock_sendfile()`` event loop methods
+now call ``file.seek(offset)`` if *file* has a ``seek()`` method,
+even if *offset* is ``0`` (default value).

_______________________________________________
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]

Reply via email to