git: 3af6f55735ce - main - tests: Add some regression tests for copy_file_range()

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Tue, 12 Aug 2025 21:45:29 UTC
The branch main has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=3af6f55735cef7df72ca3f4ecf2b0027abb5fcb8

commit 3af6f55735cef7df72ca3f4ecf2b0027abb5fcb8
Author:     Mark Johnston <markj@FreeBSD.org>
AuthorDate: 2025-08-09 21:21:41 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2025-08-12 21:45:24 +0000

    tests: Add some regression tests for copy_file_range()
    
    These cover a few bugs that have cropped up, including the ones fixed by
    commits 4046ad6bb0e and 2319ca6a0181.
    
    PR:             276045
    Reviewed by:    rmacklem
    MFC after:      2 weeks
    Differential Revision:  https://reviews.freebsd.org/D51856
---
 tests/sys/kern/Makefile          |   2 +
 tests/sys/kern/copy_file_range.c | 231 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 233 insertions(+)

diff --git a/tests/sys/kern/Makefile b/tests/sys/kern/Makefile
index 336e73f29835..9044b1e7e4f2 100644
--- a/tests/sys/kern/Makefile
+++ b/tests/sys/kern/Makefile
@@ -8,6 +8,7 @@ TESTSRC=	${SRCTOP}/contrib/netbsd-tests/kernel
 TESTSDIR=	${TESTSBASE}/sys/kern
 
 ATF_TESTS_C+=	basic_signal
+ATF_TESTS_C+=	copy_file_range
 .if ${MACHINE_ARCH} != "i386" && ${MACHINE_ARCH} != "powerpc" && \
 	${MACHINE_ARCH} != "powerpcspe"
 # No support for atomic_load_64 on i386 or (32-bit) powerpc
@@ -81,6 +82,7 @@ PROGS+=		coredump_phnum_helper
 PROGS+=		pdeathsig_helper
 PROGS+=		sendfile_helper
 
+LIBADD.copy_file_range+=		md
 LIBADD.jail_lookup_root+=		jail util
 CFLAGS.sys_getrandom+=			-I${SRCTOP}/sys/contrib/zstd/lib
 LIBADD.sys_getrandom+=			zstd
diff --git a/tests/sys/kern/copy_file_range.c b/tests/sys/kern/copy_file_range.c
new file mode 100644
index 000000000000..ca52eaf668e3
--- /dev/null
+++ b/tests/sys/kern/copy_file_range.c
@@ -0,0 +1,231 @@
+/*
+ * Copyright (c) 2025 Mark Johnston <markj@FreeBSD.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/mman.h>
+#include <sys/stat.h>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+#include <sha256.h>
+
+/*
+ * Create a file with random data and size between 1B and 32MB.  Return a file
+ * descriptor for the file.
+ */
+static int
+genfile(void)
+{
+	char buf[256], file[NAME_MAX];
+	size_t sz;
+	int fd;
+
+	sz = (random() % (32 * 1024 * 1024ul)) + 1;
+
+	snprintf(file, sizeof(file), "testfile.XXXXXX");
+	fd = mkstemp(file);
+	ATF_REQUIRE(fd != -1);
+
+	while (sz > 0) {
+		ssize_t n;
+		int error;
+
+		error = getentropy(buf, sizeof(buf));
+		ATF_REQUIRE(error == 0);
+		n = write(fd, buf, sizeof(buf) < sz ? sizeof(buf) : sz);
+		ATF_REQUIRE(n > 0);
+
+		sz -= n;
+	}
+
+	ATF_REQUIRE(lseek(fd, 0, SEEK_SET) == 0);
+	return (fd);
+}
+
+/*
+ * Return true if the file data in the two file descriptors is the same,
+ * false otherwise.
+ */
+static bool
+cmpfile(int fd1, int fd2)
+{
+	struct stat st1, st2;
+	void *addr1, *addr2;
+	size_t sz;
+	int res;
+
+	ATF_REQUIRE(fstat(fd1, &st1) == 0);
+	ATF_REQUIRE(fstat(fd2, &st2) == 0);
+	if (st1.st_size != st2.st_size)
+		return (false);
+
+	sz = st1.st_size;
+	addr1 = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd1, 0);
+	ATF_REQUIRE(addr1 != MAP_FAILED);
+	addr2 = mmap(NULL, sz, PROT_READ, MAP_PRIVATE, fd2, 0);
+	ATF_REQUIRE(addr2 != MAP_FAILED);
+
+	res = memcmp(addr1, addr2, sz);
+
+	ATF_REQUIRE(munmap(addr1, sz) == 0);
+	ATF_REQUIRE(munmap(addr2, sz) == 0);
+
+	return (res == 0);
+}
+
+/*
+ * Exercise a few error paths in the copy_file_range() syscall.
+ */
+ATF_TC_WITHOUT_HEAD(copy_file_range_invalid);
+ATF_TC_BODY(copy_file_range_invalid, tc)
+{
+	off_t off1, off2;
+	int fd1, fd2;
+
+	fd1 = genfile();
+	fd2 = genfile();
+
+	/* Can't copy a file to itself without explicit offsets. */
+	ATF_REQUIRE_ERRNO(EINVAL,
+	    copy_file_range(fd1, NULL, fd1, NULL, SSIZE_MAX, 0) == -1);
+
+	/* When copying a file to itself, ranges cannot overlap. */
+	off1 = off2 = 0;
+	ATF_REQUIRE_ERRNO(EINVAL,
+	    copy_file_range(fd1, &off1, fd1, &off2, 1, 0) == -1);
+
+	/* Negative offsets are not allowed. */
+	off1 = -1;
+	off2 = 0;
+	ATF_REQUIRE_ERRNO(EINVAL,
+	    copy_file_range(fd1, &off1, fd2, &off2, 42, 0) == -1);
+	ATF_REQUIRE_ERRNO(EINVAL,
+	    copy_file_range(fd2, &off2, fd1, &off1, 42, 0) == -1);
+}
+
+/*
+ * Make sure that copy_file_range() updates the file offsets passed to it.
+ */
+ATF_TC_WITHOUT_HEAD(copy_file_range_offset);
+ATF_TC_BODY(copy_file_range_offset, tc)
+{
+	struct stat sb;
+	off_t off1, off2;
+	ssize_t n;
+	int fd1, fd2;
+
+	off1 = off2 = 0;
+
+	fd1 = genfile();
+	fd2 = open("copy", O_RDWR | O_CREAT, 0644);
+	ATF_REQUIRE(fd2 != -1);
+
+	ATF_REQUIRE(fstat(fd1, &sb) == 0);
+
+	ATF_REQUIRE(lseek(fd1, 0, SEEK_CUR) == 0);
+	ATF_REQUIRE(lseek(fd2, 0, SEEK_CUR) == 0);
+
+	do {
+		off_t ooff1, ooff2;
+
+		ooff1 = off1;
+		ooff2 = off2;
+		n = copy_file_range(fd1, &off1, fd2, &off2, sb.st_size, 0);
+		ATF_REQUIRE(n >= 0);
+		ATF_REQUIRE_EQ(off1, ooff1 + n);
+		ATF_REQUIRE_EQ(off2, ooff2 + n);
+	} while (n != 0);
+
+	/* Offsets should have been adjusted by copy_file_range(). */
+	ATF_REQUIRE_EQ(off1, sb.st_size);
+	ATF_REQUIRE_EQ(off2, sb.st_size);
+	/* Seek offsets should have been left alone. */
+	ATF_REQUIRE(lseek(fd1, 0, SEEK_CUR) == 0);
+	ATF_REQUIRE(lseek(fd2, 0, SEEK_CUR) == 0);
+	/* Make sure the file contents are the same. */
+	ATF_REQUIRE_MSG(cmpfile(fd1, fd2), "file contents differ");
+
+	ATF_REQUIRE(close(fd1) == 0);
+	ATF_REQUIRE(close(fd2) == 0);
+}
+
+/*
+ * Make sure that copying to a larger file doesn't cause it to be truncated.
+ */
+ATF_TC_WITHOUT_HEAD(copy_file_range_truncate);
+ATF_TC_BODY(copy_file_range_truncate, tc)
+{
+	struct stat sb, sb1, sb2;
+	char digest1[65], digest2[65];
+	off_t off;
+	ssize_t n;
+	int fd1, fd2;
+
+	fd1 = genfile();
+	fd2 = genfile();
+
+	ATF_REQUIRE(fstat(fd1, &sb1) == 0);
+	ATF_REQUIRE(fstat(fd2, &sb2) == 0);
+
+	/* fd1 refers to the smaller file. */
+	if (sb1.st_size > sb2.st_size) {
+		int tmp;
+
+		tmp = fd1;
+		fd1 = fd2;
+		fd2 = tmp;
+		ATF_REQUIRE(fstat(fd1, &sb1) == 0);
+		ATF_REQUIRE(fstat(fd2, &sb2) == 0);
+	}
+
+	/*
+	 * Compute a hash of the bytes in the larger file which lie beyond the
+	 * length of the smaller file.
+	 */
+	SHA256_FdChunk(fd2, digest1, sb1.st_size, sb2.st_size - sb1.st_size);
+	ATF_REQUIRE(lseek(fd2, 0, SEEK_SET) == 0);
+
+	do {
+		n = copy_file_range(fd1, NULL, fd2, NULL, SSIZE_MAX, 0);
+		ATF_REQUIRE(n >= 0);
+	} while (n != 0);
+
+	/* Validate file offsets after the copy. */
+	off = lseek(fd1, 0, SEEK_CUR);
+	ATF_REQUIRE(off == sb1.st_size);
+	off = lseek(fd2, 0, SEEK_CUR);
+	ATF_REQUIRE(off == sb1.st_size);
+
+	/* The larger file's size should remain the same. */
+	ATF_REQUIRE(fstat(fd2, &sb) == 0);
+	ATF_REQUIRE(sb.st_size == sb2.st_size);
+
+	/* The bytes beyond the end of the copy should be unchanged. */
+	SHA256_FdChunk(fd2, digest2, sb1.st_size, sb2.st_size - sb1.st_size);
+	ATF_REQUIRE_MSG(strcmp(digest1, digest2) == 0,
+	    "trailing file contents differ after copy_file_range()");
+
+	/*
+	 * Verify that the copy actually replicated bytes from the smaller file.
+	 */
+	ATF_REQUIRE(ftruncate(fd2, sb1.st_size) == 0);
+	ATF_REQUIRE(cmpfile(fd1, fd2));
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp, copy_file_range_invalid);
+	ATF_TP_ADD_TC(tp, copy_file_range_offset);
+	ATF_TP_ADD_TC(tp, copy_file_range_truncate);
+
+	return (atf_no_error());
+}