]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.13] gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset (GH-150270...
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 29 May 2026 13:10:40 +0000 (15:10 +0200)
committerGitHub <noreply@github.com>
Fri, 29 May 2026 13:10:40 +0000 (18:40 +0530)
gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset (GH-150270)
(cherry picked from commit c72d5ea638731ec29723ded2d26ec7f997f06f17)

Co-authored-by: Grant Herman <grantlouisherman041@gmail.com>
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 84cad10636feef6d222442724fdd8f758ef96783..5b5d16a406116202b647d93c53347b79f9574433 100644 (file)
@@ -966,7 +966,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)
@@ -1281,7 +1281,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)
 
@@ -1290,7 +1289,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 f404273c3ae5c1d0427ea1396fc591ed0d619b1e..6b94975d0046ea30e29bc288c0ecd309ccba242e 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 41ccf1b78fb93b8e001253970b79b62dca5be1a7..6e90e68429ea0b9fe31d4637c34d9221ff6a72f8 100644 (file)
@@ -395,12 +395,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
 
@@ -434,20 +434,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
@@ -458,9 +458,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 bf99bc271c7acd6073eb6dda1fffbaec1eca0ec5..a36832f7f9f525867aff8373d494c81c253c0d83 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 2509d4382cdebd7f5e7146caec2838342a15a90d..63ac04f5703fe3fbfd499ff5df8b0832adc92524 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).