]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset (#150270)
authorGrant Herman <grantlouisherman041@gmail.com>
Fri, 29 May 2026 09:40:57 +0000 (05:40 -0400)
committerGitHub <noreply@github.com>
Fri, 29 May 2026 09:40:57 +0000 (15:10 +0530)
Co-authored-by: Victor Stinner <vstinner@python.org>
Lib/asyncio/base_events.py
Lib/asyncio/proactor_events.py
Lib/asyncio/unix_events.py
Lib/asyncio/windows_events.py
Lib/test/test_asyncio/test_sendfile.py
Misc/NEWS.d/next/Core_and_Builtins/2026-05-22-17-09-28.gh-issue-150107.GD72-D.rst [new file with mode: 0644]

index 1fedb066f94c53950bad614a2b3df271b0c83458..3732294c5848f02f08c9bf49b2aa738d961ef959 100644 (file)
@@ -969,7 +969,7 @@ class BaseEventLoop(events.AbstractEventLoop):
             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)
@@ -1286,7 +1286,6 @@ class BaseEventLoop(events.AbstractEventLoop):
             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)
 
@@ -1295,7 +1294,7 @@ class BaseEventLoop(events.AbstractEventLoop):
             "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)
index 2dc1569d7807911736bd0eeb76a5918e3a07ab5d..cf2902b4c76559e61c3c63e215e7dbb498770041 100644 (file)
@@ -756,8 +756,7 @@ class BaseProactorEventLoop(base_events.BaseEventLoop):
                 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()
index 4a638dc47ea21c8e14111d8e1690b5da3e8f3d0c..646ae71bbf5919e8253cee61101ba10897647f96 100644 (file)
@@ -385,12 +385,12 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
             # 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
 
@@ -424,20 +424,20 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
                 # 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
@@ -448,9 +448,9 @@ class _UnixSelectorEventLoop(selector_events.BaseSelectorEventLoop):
                                 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):
index 5f75b17d8ca649be6256f8ce9815c7fb6e0dda62..0bf7732136f1f8ea8ecf4da18e621acc9895be0c 100644 (file)
@@ -610,6 +610,9 @@ class IocpProactor:
         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,
index dcd963b3355ef86a5c69075797b35f4aedd774f1..7afd7de3bb936e698b8ccc82cb094034659f14ea 100644 (file)
@@ -228,6 +228,61 @@ class SockSendfileMixin(SendfileBase):
         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 (file)
index 0000000..a13f249
--- /dev/null
@@ -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).