Skip to content

Commit 2bb4544

Browse files
gh-150107: Fix asyncio sendfile fallback ignoring non-zero offset
The fallback paths in BaseEventLoop._sock_sendfile_fallback and _sendfile_fallback only seeked the file when offset was truthy, so an offset of 0 was respected but later non-zero offsets were dropped when the file lacked seek tracking. Seek whenever the file supports seek(). Also seek the CRT file pointer on Windows TransmitFile, which ignores OVERLAPPED.Offset for handles not opened with FILE_FLAG_OVERLAPPED. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent c7cab73 commit 2bb4544

3 files changed

Lines changed: 31 additions & 3 deletions

File tree

Lib/asyncio/base_events.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -969,7 +969,7 @@ async def _sock_sendfile_native(self, sock, file, offset, count):
969969
f"and file {file!r} combination")
970970

971971
async def _sock_sendfile_fallback(self, sock, file, offset, count):
972-
if offset:
972+
if hasattr(file, 'seek'):
973973
file.seek(offset)
974974
blocksize = (
975975
min(count, constants.SENDFILE_FALLBACK_READBUFFER_SIZE)
@@ -1286,7 +1286,6 @@ async def sendfile(self, transport, file, offset=0, count=None,
12861286
raise RuntimeError(
12871287
f"fallback is disabled and native sendfile is not "
12881288
f"supported for transport {transport!r}")
1289-
12901289
return await self._sendfile_fallback(transport, file,
12911290
offset, count)
12921291

@@ -1295,7 +1294,7 @@ async def _sendfile_native(self, transp, file, offset, count):
12951294
"sendfile syscall is not supported")
12961295

12971296
async def _sendfile_fallback(self, transp, file, offset, count):
1298-
if offset:
1297+
if hasattr(file, 'seek'):
12991298
file.seek(offset)
13001299
blocksize = min(count, 16384) if count else 16384
13011300
buf = bytearray(blocksize)

Lib/asyncio/windows_events.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,9 @@ def sendfile(self, sock, file, offset, count):
610610
ov = _overlapped.Overlapped(NULL)
611611
offset_low = offset & 0xffff_ffff
612612
offset_high = (offset >> 32) & 0xffff_ffff
613+
# TransmitFile ignores OVERLAPPED.Offset for handles not opened with
614+
# FILE_FLAG_OVERLAPPED, so seek the CRT file pointer to match.
615+
file.seek(offset)
613616
ov.TransmitFile(sock.fileno(),
614617
msvcrt.get_osfhandle(file.fileno()),
615618
offset_low, offset_high,

Lib/test/test_asyncio/test_sendfile.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,32 @@ def test_sock_sendfile_zero_size(self):
228228
self.assertEqual(ret, 0)
229229
self.assertEqual(self.file.tell(), 0)
230230

231+
def check_sock_sendfile_offset(self, data, offset, force_fallback=False):
232+
sock, proto = self.prepare_socksendfile()
233+
with tempfile.TemporaryFile() as f:
234+
f.write(data)
235+
f.flush()
236+
self.assertEqual(f.tell(), len(data))
237+
238+
if force_fallback:
239+
async def _sock_sendfile_fail(sock, file, offset, count):
240+
raise asyncio.exceptions.SendfileNotAvailableError()
241+
with support.swap_attr(self.loop, '_sock_sendfile_native', _sock_sendfile_fail):
242+
ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None))
243+
else:
244+
ret = self.run_loop(self.loop.sock_sendfile(sock, f, offset, None))
245+
self.assertEqual(f.tell(), len(data))
246+
sock.close()
247+
self.run_loop(proto.wait_closed())
248+
self.assertEqual(ret, len(data) - offset)
249+
250+
def test_sock_sendfile_offset(self):
251+
data = b'abcdef'
252+
for offset in (0, len(data) // 2, len(data)):
253+
for force_fallback in (False, True):
254+
with self.subTest(offset=offset, force_fallback=force_fallback):
255+
self.check_sock_sendfile_offset(data, offset, force_fallback)
256+
231257
def test_sock_sendfile_mix_with_regular_send(self):
232258
buf = b"mix_regular_send" * (4 * 1024) # 64 KiB
233259
sock, proto = self.prepare_socksendfile()

0 commit comments

Comments
 (0)