diff --git a/pkg/sentry/syscalls/linux/linux64.go b/pkg/sentry/syscalls/linux/linux64.go index da61ec6fa91..27469426689 100644 --- a/pkg/sentry/syscalls/linux/linux64.go +++ b/pkg/sentry/syscalls/linux/linux64.go @@ -361,7 +361,7 @@ var AMD64 = &kernel.SyscallTable{ // Syscalls implemented after 325 are "backports" from versions // of Linux after 4.4. - 326: syscalls.ErrorWithEvent("copy_file_range", linuxerr.ENOSYS, "", nil), + 326: syscalls.Supported("copy_file_range", CopyFileRange), 327: syscalls.PartiallySupportedPoint("preadv2", Preadv2, PointPreadv2, "RWF flags are not supported.", []string{"gvisor.dev/issue/2601"}), 328: syscalls.PartiallySupportedPoint("pwritev2", Pwritev2, PointPwritev2, "RWF flags are not supported.", []string{"gvisor.dev/issue/2601"}), 329: syscalls.ErrorWithEvent("pkey_mprotect", linuxerr.ENOSYS, "", nil), @@ -678,7 +678,7 @@ var ARM64 = &kernel.SyscallTable{ 284: syscalls.PartiallySupported("mlock2", Mlock2, "Stub implementation. The sandbox lacks appropriate permissions.", nil), // Syscalls after 284 are "backports" from versions of Linux after 4.4. - 285: syscalls.ErrorWithEvent("copy_file_range", linuxerr.ENOSYS, "", nil), + 285: syscalls.Supported("copy_file_range", CopyFileRange), 286: syscalls.PartiallySupportedPoint("preadv2", Preadv2, PointPreadv2, "RWF flags are not supported.", []string{"gvisor.dev/issue/2601"}), 287: syscalls.PartiallySupportedPoint("pwritev2", Pwritev2, PointPwritev2, "RWF flags are not supported.", []string{"gvisor.dev/issue/2601"}), 288: syscalls.ErrorWithEvent("pkey_mprotect", linuxerr.ENOSYS, "", nil), diff --git a/pkg/sentry/syscalls/linux/sys_splice.go b/pkg/sentry/syscalls/linux/sys_splice.go index 707f02cd094..a0c3bdff2d2 100644 --- a/pkg/sentry/syscalls/linux/sys_splice.go +++ b/pkg/sentry/syscalls/linux/sys_splice.go @@ -475,6 +475,239 @@ func Sendfile(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintpt return uintptr(total), nil, HandleIOError(t, total != 0, err, linuxerr.ERESTARTSYS, "sendfile", inFile) } +// copyFileRangeBufSize is the size of the transient buffer used to move bytes +// between the input and output files in copy_file_range(2). +const copyFileRangeBufSize = 64 << 10 // 64 KB + +// copyFileRangeStat returns the stat of fd for copy_file_range, rejecting +// directories with EISDIR and non-regular files with EINVAL. +func copyFileRangeStat(t *kernel.Task, fd *vfs.FileDescription) (linux.Statx, error) { + stat, err := fd.Stat(t, vfs.StatOptions{Mask: linux.STATX_TYPE | linux.STATX_INO | linux.STATX_SIZE}) + if err != nil { + return stat, err + } + if stat.Mask&linux.STATX_TYPE == 0 { + return stat, linuxerr.EINVAL + } + switch stat.Mode & linux.S_IFMT { + case linux.S_IFREG: + return stat, nil + case linux.S_IFDIR: + return stat, linuxerr.EISDIR + default: + return stat, linuxerr.EINVAL + } +} + +// CopyFileRange implements Linux syscall copy_file_range(2). +// +// Uses a bounded sentry buffer to perform a generic copy across any backing +// filesystem. Check ordering mirrors Linux fs/read_write.c. +func CopyFileRange(t *kernel.Task, sysno uintptr, args arch.SyscallArguments) (uintptr, *kernel.SyscallControl, error) { + inFD := args[0].Int() + inOffsetAddr := args[1].Pointer() + outFD := args[2].Int() + outOffsetAddr := args[3].Pointer() + // count is treated as unsigned. + count := uint64(args[4].SizeT()) + flags := args[5].Uint() + + // Look up FDs first so bad FDs take precedence over invalid flags. + inFile := t.GetFile(inFD) + if inFile == nil { + return 0, nil, linuxerr.EBADF + } + defer inFile.DecRef(t) + + outFile := t.GetFile(outFD) + if outFile == nil { + return 0, nil, linuxerr.EBADF + } + defer outFile.DecRef(t) + + // The flags argument must be zero. + if flags != 0 { + return 0, nil, linuxerr.EINVAL + } + + // Both descriptors must refer to regular files (directories return EISDIR). + inStat, err := copyFileRangeStat(t, inFile) + if err != nil { + return 0, nil, err + } + outStat, err := copyFileRangeStat(t, outFile) + if err != nil { + return 0, nil, err + } + + if !inFile.IsReadable() || !outFile.IsWritable() { + return 0, nil, linuxerr.EBADF + } + // Append-only output returns EBADF. + if outFile.StatusFlags()&linux.O_APPEND != 0 { + return 0, nil, linuxerr.EBADF + } + + // Copy in offsets if provided; negative offsets overflow later. + inOffset := int64(-1) + haveInOffset := inOffsetAddr != 0 + if haveInOffset { + if inFile.Options().DenyPRead { + return 0, nil, linuxerr.ESPIPE + } + var offsetP primitive.Int64 + if _, err := offsetP.CopyIn(t, inOffsetAddr); err != nil { + return 0, nil, err + } + inOffset = int64(offsetP) + } + outOffset := int64(-1) + haveOutOffset := outOffsetAddr != 0 + if haveOutOffset { + if outFile.Options().DenyPWrite { + return 0, nil, linuxerr.ESPIPE + } + var offsetP primitive.Int64 + if _, err := offsetP.CopyIn(t, outOffsetAddr); err != nil { + return 0, nil, err + } + outOffset = int64(offsetP) + } + + // Determine starting offsets. + startIn, startOut := inOffset, outOffset + if !haveInOffset { + if startIn, err = inFile.Seek(t, 0, linux.SEEK_CUR); err != nil { + return 0, nil, err + } + } + if !haveOutOffset { + if startOut, err = outFile.Seek(t, 0, linux.SEEK_CUR); err != nil { + return 0, nil, err + } + } + + // Ensure ranges do not wrap. + if uint64(startIn)+count < uint64(startIn) || uint64(startOut)+count < uint64(startOut) { + return 0, nil, linuxerr.EOVERFLOW + } + // A zero count cannot wrap, so reject negative positions here. + if startIn < 0 || startOut < 0 { + return 0, nil, linuxerr.EINVAL + } + + // Clamp count to remaining bytes before checking overlap. + if inStat.Mask&linux.STATX_SIZE != 0 { + if size := int64(inStat.Size); startIn >= size { + count = 0 + } else if remaining := uint64(size - startIn); count > remaining { + count = remaining + } + } + if count > uint64(linux.MAX_RW_COUNT) { + count = uint64(linux.MAX_RW_COUNT) + } + + // Overlapping ranges within the same file are not permitted. + if inStat.Mask&outStat.Mask&linux.STATX_INO != 0 && + inStat.Ino == outStat.Ino && + inStat.DevMajor == outStat.DevMajor && + inStat.DevMinor == outStat.DevMinor && + uint64(startOut)+count > uint64(startIn) && startOut < startIn+int64(count) { + return 0, nil, linuxerr.EINVAL + } + + if count == 0 { + return 0, nil, nil + } + limit := int64(count) + + // Regular files do not block; no dualWaiter needed. + var ( + total int64 + cprErr error + ) + bufBacking := make([]byte, min(limit, copyFileRangeBufSize)) + for total < limit { + buf := bufBacking[:min(limit-total, int64(len(bufBacking)))] + + var readN int64 + if haveInOffset { + readN, cprErr = inFile.PRead(t, usermem.BytesIOSequence(buf), inOffset, vfs.ReadOptions{}) + } else { + readN, cprErr = inFile.Read(t, usermem.BytesIOSequence(buf), vfs.ReadOptions{}) + } + if readN == 0 { + // EOF or no progress. + break + } + + // Write all read bytes. + var written int64 + for written < readN { + var writeN int64 + if haveOutOffset { + writeN, cprErr = outFile.PWrite(t, usermem.BytesIOSequence(buf[written:readN]), outOffset+written, vfs.WriteOptions{}) + } else { + writeN, cprErr = outFile.Write(t, usermem.BytesIOSequence(buf[written:readN]), vfs.WriteOptions{}) + } + written += writeN + if cprErr != nil { + break + } + } + + if notWritten := readN - written; notWritten > 0 && !haveInOffset { + // Rewind unwritten bytes from the input file offset. + if _, seekErr := inFile.Seek(t, -notWritten, linux.SEEK_CUR); seekErr != nil { + log.Warningf("copy_file_range failed to roll back input file offset: %v", seekErr) + } + } + if haveInOffset { + inOffset += written + } + if haveOutOffset { + outOffset += written + } + total += written + + if written < readN { + break + } + if cprErr == nil && t.Interrupted() { + cprErr = linuxerr.ErrInterrupted + break + } + if cprErr != nil { + break + } + } + + // Copy out the updated offsets. + if haveInOffset { + offsetP := primitive.Int64(inOffset) + if _, err := offsetP.CopyOut(t, inOffsetAddr); err != nil { + return 0, nil, err + } + } + if haveOutOffset { + offsetP := primitive.Int64(outOffset) + if _, err := offsetP.CopyOut(t, outOffsetAddr); err != nil { + return 0, nil, err + } + } + + if total != 0 && cprErr != nil && cprErr != io.EOF && !linuxerr.Equals(linuxerr.ErrWouldBlock, cprErr) { + // A partial copy succeeded, so report it rather than the error. + log.Debugf("copy_file_range completed a partial copy with error: %v", cprErr) + cprErr = nil + } + + // We can only pass a single file to HandleIOError, so pick inFile + // arbitrarily. This is used only for debugging purposes. + return uintptr(total), nil, HandleIOError(t, total != 0, cprErr, linuxerr.ERESTARTSYS, "copy_file_range", inFile) +} + // dualWaiter is used to wait on one or both vfs.FileDescriptions. It is not // thread-safe, and does not take a reference on the vfs.FileDescriptions. // diff --git a/test/syscalls/BUILD b/test/syscalls/BUILD index 661f9b0f8fc..d9841eaceeb 100644 --- a/test/syscalls/BUILD +++ b/test/syscalls/BUILD @@ -175,6 +175,11 @@ syscall_test( use_tmpfs = True, ) +syscall_test( + add_overlay = True, + test = "//test/syscalls/linux:copy_file_range_test", +) + syscall_test( add_fusefs = True, add_overlay = True, diff --git a/test/syscalls/linux/BUILD b/test/syscalls/linux/BUILD index 6fd5698f0b0..6a976da18b6 100644 --- a/test/syscalls/linux/BUILD +++ b/test/syscalls/linux/BUILD @@ -661,6 +661,22 @@ cc_binary( ], ) +cc_binary( + name = "copy_file_range_test", + testonly = 1, + srcs = ["copy_file_range.cc"], + linkstatic = 1, + malloc = "//test/util:errno_safe_allocator", + deps = select_gtest() + [ + "//test/util:file_descriptor", + "//test/util:posix_error", + "//test/util:temp_path", + "//test/util:test_main", + "//test/util:test_util", + "@com_google_absl//absl/strings", + ], +) + cc_binary( name = "creat_test", testonly = 1, diff --git a/test/syscalls/linux/copy_file_range.cc b/test/syscalls/linux/copy_file_range.cc new file mode 100644 index 00000000000..bfc0cbf5341 --- /dev/null +++ b/test/syscalls/linux/copy_file_range.cc @@ -0,0 +1,416 @@ +// Copyright 2026 The gVisor Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include + +#include + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "absl/strings/string_view.h" +#include "test/util/file_descriptor.h" +#include "test/util/posix_error.h" +#include "test/util/temp_path.h" +#include "test/util/test_util.h" + +namespace gvisor { +namespace testing { + +namespace { + +#ifndef SYS_copy_file_range +#if defined(__x86_64__) +#define SYS_copy_file_range 326 +#elif defined(__aarch64__) +#define SYS_copy_file_range 285 +#else +#error "Unknown architecture" +#endif +#endif // SYS_copy_file_range + +// Direct syscall invocation to bypass any glibc emulation. +ssize_t CopyFileRange(int fd_in, off_t* off_in, int fd_out, off_t* off_out, + size_t len, unsigned int flags) { + return syscall(SYS_copy_file_range, fd_in, off_in, fd_out, off_out, len, + flags); +} + +// Returns the full contents of the file at `path`. +PosixErrorOr ReadWholeFile(absl::string_view path) { + ASSIGN_OR_RETURN_ERRNO(FileDescriptor fd, Open(std::string(path), O_RDONLY)); + std::string contents; + char buf[1024]; + ssize_t n; + while ((n = read(fd.get(), buf, sizeof(buf))) > 0) { + contents.append(buf, n); + } + if (n < 0) { + return PosixError(errno, "read"); + } + return contents; +} + +constexpr absl::string_view kData = "0123456789abcdefghijklmnopqrstuvwxyz"; + +class CopyFileRangeTest : public ::testing::Test { + protected: + void SetUp() override { + in_file_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), kData, TempPath::kDefaultFileMode)); + out_file_ = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + } + + PosixErrorOr In(int flags = O_RDONLY) { + return Open(in_file_.path(), flags); + } + PosixErrorOr Out(int flags = O_WRONLY) { + return Open(out_file_.path(), flags); + } + + TempPath in_file_; + TempPath out_file_; +}; + +// Implicit offsets advance file positions on both descriptors. +TEST_F(CopyFileRangeTest, BasicNullOffsets) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT( + CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, kData.size(), 0), + SyscallSucceedsWithValue(kData.size())); + + EXPECT_THAT(lseek(inf.get(), 0, SEEK_CUR), + SyscallSucceedsWithValue(kData.size())); + EXPECT_THAT(lseek(outf.get(), 0, SEEK_CUR), + SyscallSucceedsWithValue(kData.size())); + + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), kData); +} + +// Explicit offsets do not move file positions. +TEST_F(CopyFileRangeTest, ExplicitOffsetsDoNotMoveFilePositions) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t in_off = 10; + off_t out_off = 0; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, outf.get(), &out_off, 5, 0), + SyscallSucceedsWithValue(5)); + + EXPECT_EQ(in_off, 15); + EXPECT_EQ(out_off, 5); + EXPECT_THAT(lseek(inf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(0)); + EXPECT_THAT(lseek(outf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(0)); + + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), + kData.substr(10, 5)); +} + +// Mixing an explicit input offset with an implicit output offset is allowed. +TEST_F(CopyFileRangeTest, MixedOffsetsImplicitOutput) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t in_off = 4; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, outf.get(), nullptr, 6, 0), + SyscallSucceedsWithValue(6)); + + EXPECT_EQ(in_off, 10); + EXPECT_THAT(lseek(inf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(0)); + EXPECT_THAT(lseek(outf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(6)); + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), + kData.substr(4, 6)); +} + +// Mixing an implicit input offset with an explicit output offset is allowed. +TEST_F(CopyFileRangeTest, MixedOffsetsImplicitInput) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + ASSERT_THAT(lseek(inf.get(), 4, SEEK_SET), SyscallSucceedsWithValue(4)); + + off_t out_off = 0; + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), &out_off, 6, 0), + SyscallSucceedsWithValue(6)); + + EXPECT_EQ(out_off, 6); + EXPECT_THAT(lseek(inf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(10)); + EXPECT_THAT(lseek(outf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(0)); + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), + kData.substr(4, 6)); +} + +// Count larger than input length is clamped to EOF. +TEST_F(CopyFileRangeTest, ShortenedToEOF) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT( + CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 1 << 20, 0), + SyscallSucceedsWithValue(kData.size())); + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), kData); +} + +// Starting at or past EOF copies nothing. +TEST_F(CopyFileRangeTest, StartAtEOF) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t in_off = kData.size(); + off_t out_off = 0; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, outf.get(), &out_off, 100, 0), + SyscallSucceedsWithValue(0)); + EXPECT_EQ(in_off, static_cast(kData.size())); + EXPECT_EQ(out_off, 0); +} + +TEST_F(CopyFileRangeTest, ZeroCount) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 0, 0), + SyscallSucceedsWithValue(0)); + EXPECT_THAT(lseek(inf.get(), 0, SEEK_CUR), SyscallSucceedsWithValue(0)); +} + +// Output offset beyond EOF extends the file. +TEST_F(CopyFileRangeTest, OutputOffsetBeyondEOFExtendsFile) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t in_off = 0; + off_t out_off = 8; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, outf.get(), &out_off, 4, 0), + SyscallSucceedsWithValue(4)); + + const std::string got = + ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())); + ASSERT_EQ(got.size(), 12); + EXPECT_EQ(got.substr(0, 8), std::string(8, '\0')); + EXPECT_EQ(got.substr(8), kData.substr(0, 4)); +} + +// Copies issued in multiple calls resume correctly. +TEST_F(CopyFileRangeTest, Resumable) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + size_t total = 0; + while (total < kData.size()) { + const ssize_t n = CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, + kData.size() - total, 0); + ASSERT_THAT(n, SyscallSucceeds()); + if (n == 0) break; + total += n; + } + EXPECT_EQ(total, kData.size()); + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), kData); +} + +TEST_F(CopyFileRangeTest, NonZeroFlagsIsEINVAL) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 1, 1), + SyscallFailsWithErrno(EINVAL)); +} + +TEST_F(CopyFileRangeTest, BadFDIsEBADF) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(-1, nullptr, outf.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EBADF)); + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, -1, nullptr, 1, 0), + SyscallFailsWithErrno(EBADF)); +} + +TEST_F(CopyFileRangeTest, UnreadableInputIsEBADF) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In(O_WRONLY)); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EBADF)); +} + +TEST_F(CopyFileRangeTest, UnwritableOutputIsEBADF) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out(O_RDONLY)); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EBADF)); +} + +// Unlike sendfile(2), copy_file_range(2) rejects append-only output with EBADF. +TEST_F(CopyFileRangeTest, AppendOnlyOutputIsEBADF) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = + ASSERT_NO_ERRNO_AND_VALUE(Out(O_WRONLY | O_APPEND)); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EBADF)); +} + +TEST_F(CopyFileRangeTest, DirectoryInputIsEISDIR) { + const FileDescriptor dirf = ASSERT_NO_ERRNO_AND_VALUE( + Open(GetAbsoluteTestTmpdir(), O_RDONLY | O_DIRECTORY)); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(dirf.get(), nullptr, outf.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EISDIR)); +} + +TEST_F(CopyFileRangeTest, PipeIsEINVAL) { + int fds[2]; + ASSERT_THAT(pipe(fds), SyscallSucceeds()); + const FileDescriptor rfd(fds[0]); + const FileDescriptor wfd(fds[1]); + + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(rfd.get(), nullptr, outf.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EINVAL)); + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, wfd.get(), nullptr, 1, 0), + SyscallFailsWithErrno(EINVAL)); +} + +// Negative offset with non-zero count returns EOVERFLOW. +TEST_F(CopyFileRangeTest, NegativeOffsetIsEOVERFLOW) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t bad = -1; + off_t good = 0; + EXPECT_THAT(CopyFileRange(inf.get(), &bad, outf.get(), &good, 1, 0), + SyscallFailsWithErrno(EOVERFLOW)); + EXPECT_THAT(CopyFileRange(inf.get(), &good, outf.get(), &bad, 1, 0), + SyscallFailsWithErrno(EOVERFLOW)); +} + +// Negative offset with zero count returns EINVAL. +TEST_F(CopyFileRangeTest, NegativeOffsetZeroCountIsEINVAL) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t bad = -1; + off_t good = 0; + EXPECT_THAT(CopyFileRange(inf.get(), &bad, outf.get(), &good, 0, 0), + SyscallFailsWithErrno(EINVAL)); +} + +// Overlapping ranges within the same file return EINVAL. +TEST_F(CopyFileRangeTest, SameFileOverlapIsEINVAL) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In(O_RDWR)); + + off_t in_off = 0; + off_t out_off = 4; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, inf.get(), &out_off, 10, 0), + SyscallFailsWithErrno(EINVAL)); +} + +// Non-overlapping ranges within the same file succeed. +TEST_F(CopyFileRangeTest, SameFileNoOverlapSucceeds) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In(O_RDWR)); + + off_t in_off = 0; + off_t out_off = 20; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, inf.get(), &out_off, 10, 0), + SyscallSucceedsWithValue(10)); + + const std::string got = + ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(in_file_.path())); + EXPECT_EQ(got.substr(20, 10), kData.substr(0, 10)); +} + +// Overlapping ranges across different FDs for the same file return EINVAL. +TEST_F(CopyFileRangeTest, SameFileTwoFDsOverlapIsEINVAL) { + const FileDescriptor a = ASSERT_NO_ERRNO_AND_VALUE(In(O_RDWR)); + const FileDescriptor b = ASSERT_NO_ERRNO_AND_VALUE(In(O_RDWR)); + + off_t in_off = 0; + off_t out_off = 1; + EXPECT_THAT(CopyFileRange(a.get(), &in_off, b.get(), &out_off, 10, 0), + SyscallFailsWithErrno(EINVAL)); +} + +// Count is clamped to input size rather than rejected. +TEST_F(CopyFileRangeTest, HugeCountIsShortenedNotRejected) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, + static_cast(1) << 62, 0), + SyscallSucceedsWithValue(kData.size())); +} + +// Offset + count overflow returns EOVERFLOW. +TEST_F(CopyFileRangeTest, OverflowingRangeIsEOVERFLOW) { + const FileDescriptor inf = ASSERT_NO_ERRNO_AND_VALUE(In()); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + off_t in_off = 0; + off_t out_off = 1; + EXPECT_THAT(CopyFileRange(inf.get(), &in_off, outf.get(), &out_off, + static_cast(-1), 0), + SyscallFailsWithErrno(EOVERFLOW)); +} + +// An empty input file copies nothing. +TEST_F(CopyFileRangeTest, EmptyInput) { + const TempPath empty = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFile()); + const FileDescriptor inf = + ASSERT_NO_ERRNO_AND_VALUE(Open(empty.path(), O_RDONLY)); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + EXPECT_THAT(CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, 100, 0), + SyscallSucceedsWithValue(0)); +} + +// Large copies exceeding internal buffer size succeed. +TEST_F(CopyFileRangeTest, LargeCopy) { + constexpr size_t kSize = 4 << 20; // 4MiB, well past the internal buffer. + std::string big(kSize, '\0'); + for (size_t i = 0; i < kSize; ++i) { + big[i] = static_cast(i % 251); + } + const TempPath src = ASSERT_NO_ERRNO_AND_VALUE(TempPath::CreateFileWith( + GetAbsoluteTestTmpdir(), big, TempPath::kDefaultFileMode)); + + const FileDescriptor inf = + ASSERT_NO_ERRNO_AND_VALUE(Open(src.path(), O_RDONLY)); + const FileDescriptor outf = ASSERT_NO_ERRNO_AND_VALUE(Out()); + + size_t total = 0; + while (total < kSize) { + const ssize_t n = CopyFileRange(inf.get(), nullptr, outf.get(), nullptr, + kSize - total, 0); + ASSERT_THAT(n, SyscallSucceeds()); + if (n == 0) break; + total += n; + } + EXPECT_EQ(total, kSize); + EXPECT_EQ(ASSERT_NO_ERRNO_AND_VALUE(ReadWholeFile(out_file_.path())), big); +} + +} // namespace + +} // namespace testing +} // namespace gvisor