I have noted errors “OS Error 61: No data” on some file-heavy workloads on AWS FSX Lustre. They have also been encountered in this bug report.

Symptom

The same error can be seen in Python programs reporting:

OSError: [Errno 61] No data available:

Or in rust programs reporting:

No data available (os error 61)

In both systems the error is encountered only when copying a file from the Lustre filesystem.

Underlying cause

The immediate cause is use of the sendfile syscall. Very occasionally when reading from the Lustre filesystem the call returns ENODATA and neither the Python nor the rust libraries fall back on standard read()/write() but instead throw an exception.

copy_file_range(3, NULL, 4, NULL, 1073741824, 0) = -1 EXDEV (Invalid cross-device link)
sendfile(4, 3, NULL, 2147479552)        = -1 ENODATA (No data available)
close(4)                                = 0
close(3)                                = 0
write(2, "Error: ", 7Error: )                  = 7
write(2, "Os", 2Os)                       = 2

Fix for Python

I’ve contributed a patch to fix this in Python: https://github.com/python/cpython/pull/139417

From 0ba60ba2594aad4fa339a8e34f7ec6f839b90664 Mon Sep 17 00:00:00 2001
From: Bojan Nikolic <bn204@cam.ac.uk>
Date: Mon, 29 Sep 2025 12:33:46 +0000
Subject: [PATCH] GH-139416: Fix copyfile failure due to sendfile + Lustre

---
 Lib/shutil.py                                 | 18 ++++++++++
 Lib/test/test_shutil.py                       | 36 +++++++++++++++++++
 ...-09-29-14-30-00.gh-issue-139416.bnzz33.rst |  3 ++
 3 files changed, 57 insertions(+)
 create mode 100644 Misc/NEWS.d/next/Library/2025-09-29-14-30-00.gh-issue-139416.bnzz33.rst

diff --git a/Lib/shutil.py b/Lib/shutil.py
index 8d8fe145567..2da3b9e0478 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -213,6 +213,24 @@ def _fastcopy_sendfile(fsrc, fdst):
                 _USE_CP_SENDFILE = False
                 raise _GiveupOnFastCopy(err)
 
+            if err.errno == errno.ENODATA:
+                # In rare cases, sendfile() on Linux Lustre returns
+                # ENODATA.
+                _USE_CP_SENDFILE = False
+
+                # 'infd' and 'outfd' are assumed to be seekable, as
+                # they are checked to be "regular" files.
+                dstpos = os.lseek(outfd, 0, os.SEEK_CUR)
+                if dstpos > 0:
+                    # Some data has already been written but we use
+                    # sendfile in a mode that does not update the
+                    # input fd position when reading. Hence seek the
+                    # input fd to the correct position before falling
+                    # back on POSIX read/write method.
+                    os.lseek(infd, dstpos, os.SEEK_SET)
+
+                raise _GiveupOnFastCopy(err)
+
             if err.errno == errno.ENOSPC:  # filesystem is full
                 raise err from None
 
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index ebb6cf88336..fff24cfdfbc 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -3365,6 +3365,42 @@ def test_file2file_not_supported(self):
         finally:
             shutil._USE_CP_SENDFILE = True
 
+    def test_exception_on_enodata_call(self):
+        # Test logic when sendfile(2) call returns ENODATA error on
+        # the not-first call on the file and we need to fall back to
+        # traditional POSIX while preserving the position of where we
+        # got to in writing
+        def syscall(*args, **kwargs):
+            if flag:
+                raise OSError(errno.ENODATA, "yo")
+            flag.append(None)
+            return eval(self.PATCHPOINT)(*args, **kwargs)
+
+        flag = []
+        orig_syscall = eval(self.PATCHPOINT)
+        # Reduce block size so that multiple syscalls are needed
+        fstat_mock = unittest.mock.Mock()
+        fstat_mock.st_size = 65536 + 1
+        with unittest.mock.patch('os.fstat', return_value=fstat_mock):
+            with (
+                unittest.mock.patch(self.PATCHPOINT, create=True,
+                                    side_effect=syscall),
+                self.get_files() as (src, dst)
+            ):
+                self.assertRaises(_GiveupOnFastCopy,
+                                  self.zerocopy_fun, src, dst)
+
+            # Reset flag so that second syscall fails again
+            flag.clear()
+            with unittest.mock.patch(self.PATCHPOINT, create=True,
+                                     side_effect=syscall) as m2:
+                shutil._USE_CP_SENDFILE = True
+                shutil.copyfile(TESTFN, TESTFN2)
+                m2.assert_called()
+        shutil._USE_CP_SENDFILE = True
+        self.assertEqual(flag, [None])
+        self.assertEqual(read_file(TESTFN2, binary=True), self.FILEDATA)
+
 
 @unittest.skipUnless(shutil._USE_CP_COPY_FILE_RANGE, "os.copy_file_range() not supported")
 class TestZeroCopyCopyFileRange(_ZeroCopyFileLinuxTest, unittest.TestCase):
diff --git a/Misc/NEWS.d/next/Library/2025-09-29-14-30-00.gh-issue-139416.bnzz33.rst b/Misc/NEWS.d/next/Library/2025-09-29-14-30-00.gh-issue-139416.bnzz33.rst
new file mode 100644
index 00000000000..8c7a26aece6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-29-14-30-00.gh-issue-139416.bnzz33.rst
@@ -0,0 +1,3 @@
+:func:`shutil.copyfile`: Detect problem seen on Lustre filesystems
+giving "Errno 61" due to the sendfile(2) optimised implementation and
+fall back to the Posix standard read/write implementation.
-- 
2.34.1

Workaround

On Python the use of sendfile for copy can be disabled at runtime, see shutils. See also https://www.ibm.com/support/pages/apar/IJ29942 for same workaround for similar problem on GPFS.

Motivation

Use of sendfile() instead of read()/write() is motivated by the performance improvement arising from avoiding the need to copy data to user-space and back to the kernel.

results

Other relevant infomration

  • sendfile issue on another distributed filesystem: https://bugs.python.org/issue43743#msg393429