git: 0733dd8a9353 - stable/13 - copy_file_range: truncate write if it would exceed RLIMIT_FSIZE

From: Alan Somers <asomers_at_FreeBSD.org>
Date: Wed, 12 Oct 2022 04:50:03 UTC
The branch stable/13 has been updated by asomers:

URL: https://cgit.FreeBSD.org/src/commit/?id=0733dd8a9353a1b9796ffeca0120c5c3642df48b

commit 0733dd8a9353a1b9796ffeca0120c5c3642df48b
Author:     Alan Somers <asomers@FreeBSD.org>
AuthorDate: 2022-09-25 22:53:36 +0000
Commit:     Alan Somers <asomers@FreeBSD.org>
CommitDate: 2022-10-12 04:49:39 +0000

    copy_file_range: truncate write if it would exceed RLIMIT_FSIZE
    
    PR:             266611
    Reviewed by:    kib
    Differential Revision: https://reviews.freebsd.org/D36706
    
    (cherry picked from commit 52360ca32ff90b605ac7481fd79e6a251e8b5116)
---
 sys/fs/fuse/fuse_vnops.c               |  15 +++--
 sys/fs/nfsclient/nfs_clvnops.c         |   8 ++-
 sys/kern/vfs_vnops.c                   |  14 ++--
 tests/sys/fs/fusefs/copy_file_range.cc | 113 +++++++++++++++++++++++++--------
 4 files changed, 115 insertions(+), 35 deletions(-)

diff --git a/sys/fs/fuse/fuse_vnops.c b/sys/fs/fuse/fuse_vnops.c
index 925d4ff16232..f68bbb358ca2 100644
--- a/sys/fs/fuse/fuse_vnops.c
+++ b/sys/fs/fuse/fuse_vnops.c
@@ -812,6 +812,7 @@ fuse_vnop_copy_file_range(struct vop_copy_file_range_args *ap)
 	struct thread *td;
 	struct uio io;
 	off_t outfilesize;
+	ssize_t r = 0;
 	pid_t pid;
 	int err;
 
@@ -859,11 +860,11 @@ fuse_vnop_copy_file_range(struct vop_copy_file_range_args *ap)
 	if (err)
 		goto unlock;
 
+	io.uio_resid = *ap->a_lenp;
 	if (ap->a_fsizetd) {
 		io.uio_offset = *ap->a_outoffp;
-		io.uio_resid = *ap->a_lenp;
-		err = vn_rlimit_fsize(outvp, &io, ap->a_fsizetd);
-		if (err)
+		err = vn_rlimit_fsizex(outvp, &io, 0, &r, ap->a_fsizetd);
+		if (err != 0)
 			goto unlock;
 	}
 
@@ -872,7 +873,7 @@ fuse_vnop_copy_file_range(struct vop_copy_file_range_args *ap)
 		goto unlock;
 
 	err = fuse_inval_buf_range(outvp, outfilesize, *ap->a_outoffp,
-		*ap->a_outoffp + *ap->a_lenp);
+		*ap->a_outoffp + io.uio_resid);
 	if (err)
 		goto unlock;
 
@@ -884,7 +885,7 @@ fuse_vnop_copy_file_range(struct vop_copy_file_range_args *ap)
 	fcfri->nodeid_out = VTOI(outvp);
 	fcfri->fh_out = outfufh->fh_id;
 	fcfri->off_out = *ap->a_outoffp;
-	fcfri->len = *ap->a_lenp;
+	fcfri->len = io.uio_resid;
 	fcfri->flags = 0;
 
 	err = fdisp_wait_answ(&fdi);
@@ -916,6 +917,10 @@ fallback:
 		    ap->a_incred, ap->a_outcred, ap->a_fsizetd);
 	}
 
+	/*
+	 * No need to call vn_rlimit_fsizex_res before return, since the uio is
+	 * local.
+	 */
 	return (err);
 }
 
diff --git a/sys/fs/nfsclient/nfs_clvnops.c b/sys/fs/nfsclient/nfs_clvnops.c
index 155028a81716..4edfee540cc9 100644
--- a/sys/fs/nfsclient/nfs_clvnops.c
+++ b/sys/fs/nfsclient/nfs_clvnops.c
@@ -3803,6 +3803,7 @@ nfs_copy_file_range(struct vop_copy_file_range_args *ap)
 	struct uio io;
 	struct nfsmount *nmp;
 	size_t len, len2;
+	ssize_t r;
 	int error, inattrflag, outattrflag, ret, ret2;
 	off_t inoff, outoff;
 	bool consecutive, must_commit, tryoutcred;
@@ -3862,7 +3863,12 @@ generic_copy:
 	 */
 	io.uio_offset = *ap->a_outoffp;
 	io.uio_resid = *ap->a_lenp;
-	error = vn_rlimit_fsize(outvp, &io, ap->a_fsizetd);
+	error = vn_rlimit_fsizex(outvp, &io, 0, &r, ap->a_fsizetd);
+	*ap->a_lenp = io.uio_resid;
+	/*
+	 * No need to call vn_rlimit_fsizex_res before return, since the uio is
+	 * local.
+	 */
 
 	/*
 	 * Flush the input file so that the data is up to date before
diff --git a/sys/kern/vfs_vnops.c b/sys/kern/vfs_vnops.c
index af4320f44eb5..190a48291517 100644
--- a/sys/kern/vfs_vnops.c
+++ b/sys/kern/vfs_vnops.c
@@ -3236,12 +3236,11 @@ vn_generic_copy_file_range(struct vnode *invp, off_t *inoffp,
 {
 	struct vattr va, inva;
 	struct mount *mp;
-	struct uio io;
 	off_t startoff, endoff, xfer, xfer2;
 	u_long blksize;
 	int error, interrupted;
 	bool cantseek, readzeros, eof, lastblock, holetoeof;
-	ssize_t aresid;
+	ssize_t aresid, r = 0;
 	size_t copylen, len, rem, savlen;
 	char *dat;
 	long holein, holeout;
@@ -3270,13 +3269,20 @@ vn_generic_copy_file_range(struct vnode *invp, off_t *inoffp,
 		error = vn_lock(outvp, LK_EXCLUSIVE);
 	if (error == 0) {
 		/*
-		 * If fsize_td != NULL, do a vn_rlimit_fsize() call,
+		 * If fsize_td != NULL, do a vn_rlimit_fsizex() call,
 		 * now that outvp is locked.
 		 */
 		if (fsize_td != NULL) {
+			struct uio io;
+
 			io.uio_offset = *outoffp;
 			io.uio_resid = len;
-			error = vn_rlimit_fsize(outvp, &io, fsize_td);
+			error = vn_rlimit_fsizex(outvp, &io, 0, &r, fsize_td);
+			len = savlen = io.uio_resid;
+			/*
+			 * No need to call vn_rlimit_fsizex_res before return,
+			 * since the uio is local.
+			 */
 		}
 		if (VOP_PATHCONF(outvp, _PC_MIN_HOLE_SIZE, &holeout) != 0)
 			holeout = 0;
diff --git a/tests/sys/fs/fusefs/copy_file_range.cc b/tests/sys/fs/fusefs/copy_file_range.cc
index 7e1189648de3..84101ca6c984 100644
--- a/tests/sys/fs/fusefs/copy_file_range.cc
+++ b/tests/sys/fs/fusefs/copy_file_range.cc
@@ -44,22 +44,6 @@ using namespace testing;
 
 class CopyFileRange: public FuseTest {
 public:
-static sig_atomic_t s_sigxfsz;
-
-void SetUp() {
-	s_sigxfsz = 0;
-	FuseTest::SetUp();
-}
-
-void TearDown() {
-	struct sigaction sa;
-
-	bzero(&sa, sizeof(sa));
-	sa.sa_handler = SIG_DFL;
-	sigaction(SIGXFSZ, &sa, NULL);
-
-	FuseTest::TearDown();
-}
 
 void expect_maybe_lseek(uint64_t ino)
 {
@@ -114,12 +98,6 @@ void expect_write(uint64_t ino, uint64_t offset, uint64_t isize,
 
 };
 
-sig_atomic_t CopyFileRange::s_sigxfsz = 0;
-
-void sigxfsz_handler(int __unused sig) {
-	CopyFileRange::s_sigxfsz = 1;
-}
-
 
 class CopyFileRange_7_27: public CopyFileRange {
 public:
@@ -137,6 +115,37 @@ virtual void SetUp() {
 }
 };
 
+class CopyFileRangeRlimitFsize: public CopyFileRange {
+public:
+static sig_atomic_t s_sigxfsz;
+struct rlimit	m_initial_limit;
+
+virtual void SetUp() {
+	s_sigxfsz = 0;
+	getrlimit(RLIMIT_FSIZE, &m_initial_limit);
+	CopyFileRange::SetUp();
+}
+
+void TearDown() {
+	struct sigaction sa;
+
+	setrlimit(RLIMIT_FSIZE, &m_initial_limit);
+
+	bzero(&sa, sizeof(sa));
+	sa.sa_handler = SIG_DFL;
+	sigaction(SIGXFSZ, &sa, NULL);
+
+	FuseTest::TearDown();
+}
+
+};
+
+sig_atomic_t CopyFileRangeRlimitFsize::s_sigxfsz = 0;
+
+void sigxfsz_handler(int __unused sig) {
+	CopyFileRangeRlimitFsize::s_sigxfsz = 1;
+}
+
 TEST_F(CopyFileRange, eio)
 {
 	const char FULLPATH1[] = "mountpoint/src.txt";
@@ -313,8 +322,11 @@ TEST_F(CopyFileRange, fallback)
 	ASSERT_EQ(len, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
 }
 
-/* fusefs should respect RLIMIT_FSIZE */
-TEST_F(CopyFileRange, rlimit_fsize)
+/*
+ * copy_file_range should send SIGXFSZ and return EFBIG when the operation
+ * would exceed the limit imposed by RLIMIT_FSIZE.
+ */
+TEST_F(CopyFileRangeRlimitFsize, signal)
 {
 	const char FULLPATH1[] = "mountpoint/src.txt";
 	const char RELPATH1[] = "src.txt";
@@ -344,7 +356,7 @@ TEST_F(CopyFileRange, rlimit_fsize)
 	).Times(0);
 
 	rl.rlim_cur = fsize2;
-	rl.rlim_max = 10 * fsize2;
+	rl.rlim_max = m_initial_limit.rlim_max;
 	ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
 	ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
 
@@ -355,6 +367,57 @@ TEST_F(CopyFileRange, rlimit_fsize)
 	EXPECT_EQ(1, s_sigxfsz);
 }
 
+/*
+ * When crossing the RLIMIT_FSIZE boundary, writes should be truncated, not
+ * aborted.
+ * https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=266611
+ */
+TEST_F(CopyFileRangeRlimitFsize, truncate)
+{
+	const char FULLPATH1[] = "mountpoint/src.txt";
+	const char RELPATH1[] = "src.txt";
+	const char FULLPATH2[] = "mountpoint/dst.txt";
+	const char RELPATH2[] = "dst.txt";
+	struct rlimit rl;
+	const uint64_t ino1 = 42;
+	const uint64_t ino2 = 43;
+	const uint64_t fh1 = 0xdeadbeef1a7ebabe;
+	const uint64_t fh2 = 0xdeadc0de88c0ffee;
+	off_t fsize1 = 1 << 20;		/* 1 MiB */
+	off_t fsize2 = 1 << 19;		/* 512 KiB */
+	off_t start1 = 1 << 18;
+	off_t start2 = fsize2;
+	ssize_t len = 65536;
+	off_t limit = start2 + len / 2;
+	int fd1, fd2;
+
+	expect_lookup(RELPATH1, ino1, S_IFREG | 0644, fsize1, 1);
+	expect_lookup(RELPATH2, ino2, S_IFREG | 0644, fsize2, 1);
+	expect_open(ino1, 0, 1, fh1);
+	expect_open(ino2, 0, 1, fh2);
+	EXPECT_CALL(*m_mock, process(
+		ResultOf([=](auto in) {
+			return (in.header.opcode == FUSE_COPY_FILE_RANGE &&
+				(off_t)in.body.copy_file_range.off_out == start2 &&
+				in.body.copy_file_range.len == (size_t)len / 2
+			);
+		}, Eq(true)),
+		_)
+	).WillOnce(Invoke(ReturnImmediate([=](auto in __unused, auto& out) {
+		SET_OUT_HEADER_LEN(out, write);
+		out.body.write.size = len / 2;
+	})));
+
+	rl.rlim_cur = limit;
+	rl.rlim_max = m_initial_limit.rlim_max;
+	ASSERT_EQ(0, setrlimit(RLIMIT_FSIZE, &rl)) << strerror(errno);
+	ASSERT_NE(SIG_ERR, signal(SIGXFSZ, sigxfsz_handler)) << strerror(errno);
+
+	fd1 = open(FULLPATH1, O_RDONLY);
+	fd2 = open(FULLPATH2, O_WRONLY);
+	ASSERT_EQ(len / 2, copy_file_range(fd1, &start1, fd2, &start2, len, 0));
+}
+
 TEST_F(CopyFileRange, ok)
 {
 	const char FULLPATH1[] = "mountpoint/src.txt";