[3/3] xfs: actually account for quota changes in xfs_swap_extents
diff mbox series

Message ID 158864102885.182577.15936710415441871446.stgit@magnolia
State New
Headers show
Series
  • xfs: random SWAPEXT fixes
Related show

Commit Message

Darrick J. Wong May 5, 2020, 1:10 a.m. UTC
From: Darrick J. Wong <darrick.wong@oracle.com>

Currently, xfs_swap_extents neither checks for sufficient quota
reservation nor does it actually update quota counts when swapping the
extent forks.  While the primary known user of extent swapping (xfs_fsr)
is careful to ensure that the user/group/project ids of both files
match, this is not required by the kernel.  Consequently, unprivileged
userspace can cause the quota counts to be incorrect.

Fix this by updating quota counts when we swap the extents, and be sure
to make a quota reservation for the difference in blocks so that we can
bail out with EDQUOT early if needed.

Signed-off-by: Darrick J. Wong <darrick.wong@oracle.com>
---
 fs/xfs/libxfs/xfs_bmap.h |   13 +++--
 fs/xfs/xfs_bmap_util.c   |  125 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 134 insertions(+), 4 deletions(-)

Comments

Christoph Hellwig May 6, 2020, 2:57 p.m. UTC | #1
On Mon, May 04, 2020 at 06:10:29PM -0700, Darrick J. Wong wrote:
> From: Darrick J. Wong <darrick.wong@oracle.com>
> 
> Currently, xfs_swap_extents neither checks for sufficient quota
> reservation nor does it actually update quota counts when swapping the
> extent forks.  While the primary known user of extent swapping (xfs_fsr)
> is careful to ensure that the user/group/project ids of both files
> match, this is not required by the kernel.  Consequently, unprivileged
> userspace can cause the quota counts to be incorrect.

Wouldn't be the right fix to enforce an id match?  I think that is a
very sensible limitation.
Darrick J. Wong May 6, 2020, 4:34 p.m. UTC | #2
On Wed, May 06, 2020 at 07:57:28AM -0700, Christoph Hellwig wrote:
> On Mon, May 04, 2020 at 06:10:29PM -0700, Darrick J. Wong wrote:
> > From: Darrick J. Wong <darrick.wong@oracle.com>
> > 
> > Currently, xfs_swap_extents neither checks for sufficient quota
> > reservation nor does it actually update quota counts when swapping the
> > extent forks.  While the primary known user of extent swapping (xfs_fsr)
> > is careful to ensure that the user/group/project ids of both files
> > match, this is not required by the kernel.  Consequently, unprivileged
> > userspace can cause the quota counts to be incorrect.
> 
> Wouldn't be the right fix to enforce an id match?  I think that is a
> very sensible limitation.

One could do that, but at a cost of breaking any userspace program that
was using XFS_IOC_SWAPEXT and was not aware that the ids had to match
(possibly due to the lack of documentation...)

It was trivial enough to port this from the atomic file update series,
so I decided to post the least functionality-reducing version and see
what happens.

Thoughts?

--D
Christoph Hellwig May 7, 2020, 6:02 a.m. UTC | #3
On Wed, May 06, 2020 at 09:34:24AM -0700, Darrick J. Wong wrote:
> On Wed, May 06, 2020 at 07:57:28AM -0700, Christoph Hellwig wrote:
> > On Mon, May 04, 2020 at 06:10:29PM -0700, Darrick J. Wong wrote:
> > > From: Darrick J. Wong <darrick.wong@oracle.com>
> > > 
> > > Currently, xfs_swap_extents neither checks for sufficient quota
> > > reservation nor does it actually update quota counts when swapping the
> > > extent forks.  While the primary known user of extent swapping (xfs_fsr)
> > > is careful to ensure that the user/group/project ids of both files
> > > match, this is not required by the kernel.  Consequently, unprivileged
> > > userspace can cause the quota counts to be incorrect.
> > 
> > Wouldn't be the right fix to enforce an id match?  I think that is a
> > very sensible limitation.
> 
> One could do that, but at a cost of breaking any userspace program that
> was using XFS_IOC_SWAPEXT and was not aware that the ids had to match
> (possibly due to the lack of documentation...)

I don't really expect that to be the case.  I'd throw in the check
and a printk_once warning, and I bet a beer at the next conference
(if there ever is one :)) that no one will trigger it.
Darrick J. Wong May 12, 2020, 11:42 p.m. UTC | #4
On Wed, May 06, 2020 at 11:02:05PM -0700, Christoph Hellwig wrote:
> On Wed, May 06, 2020 at 09:34:24AM -0700, Darrick J. Wong wrote:
> > On Wed, May 06, 2020 at 07:57:28AM -0700, Christoph Hellwig wrote:
> > > On Mon, May 04, 2020 at 06:10:29PM -0700, Darrick J. Wong wrote:
> > > > From: Darrick J. Wong <darrick.wong@oracle.com>
> > > > 
> > > > Currently, xfs_swap_extents neither checks for sufficient quota
> > > > reservation nor does it actually update quota counts when swapping the
> > > > extent forks.  While the primary known user of extent swapping (xfs_fsr)
> > > > is careful to ensure that the user/group/project ids of both files
> > > > match, this is not required by the kernel.  Consequently, unprivileged
> > > > userspace can cause the quota counts to be incorrect.
> > > 
> > > Wouldn't be the right fix to enforce an id match?  I think that is a
> > > very sensible limitation.
> > 
> > One could do that, but at a cost of breaking any userspace program that
> > was using XFS_IOC_SWAPEXT and was not aware that the ids had to match
> > (possibly due to the lack of documentation...)
> 
> I don't really expect that to be the case.  I'd throw in the check
> and a printk_once warning, and I bet a beer at the next conference
> (if there ever is one :)) that no one will trigger it.

<shrug> I guess I can at least see if fstests fails if you don't allow
swapping between extents with different [ugp]ids, but this really feels
like cutting corners off the quota functionality... :P

--D

Patch
diff mbox series

diff --git a/fs/xfs/libxfs/xfs_bmap.h b/fs/xfs/libxfs/xfs_bmap.h
index f3259ad5c22c..463346caca46 100644
--- a/fs/xfs/libxfs/xfs_bmap.h
+++ b/fs/xfs/libxfs/xfs_bmap.h
@@ -158,6 +158,13 @@  static inline int xfs_bmapi_whichfork(int bmapi_flags)
 	{ BMAP_ATTRFORK,	"ATTR" }, \
 	{ BMAP_COWFORK,		"COW" }
 
+/* Return true if the extent is an allocated extent, written or not. */
+static inline bool xfs_bmap_is_mapped_extent(struct xfs_bmbt_irec *irec)
+{
+	return irec->br_startblock != HOLESTARTBLOCK &&
+		irec->br_startblock != DELAYSTARTBLOCK &&
+		!isnullstartblock(irec->br_startblock);
+}
 
 /*
  * Return true if the extent is a real, allocated extent, or false if it is  a
@@ -165,10 +172,8 @@  static inline int xfs_bmapi_whichfork(int bmapi_flags)
  */
 static inline bool xfs_bmap_is_real_extent(struct xfs_bmbt_irec *irec)
 {
-	return irec->br_state != XFS_EXT_UNWRITTEN &&
-		irec->br_startblock != HOLESTARTBLOCK &&
-		irec->br_startblock != DELAYSTARTBLOCK &&
-		!isnullstartblock(irec->br_startblock);
+	return xfs_bmap_is_mapped_extent(irec) &&
+	       irec->br_state != XFS_EXT_UNWRITTEN;
 }
 
 /*
diff --git a/fs/xfs/xfs_bmap_util.c b/fs/xfs/xfs_bmap_util.c
index 2774939e176d..bcac7530d1ac 100644
--- a/fs/xfs/xfs_bmap_util.c
+++ b/fs/xfs/xfs_bmap_util.c
@@ -1358,6 +1358,8 @@  xfs_swap_extent_rmap(
 
 		/* Unmap the old blocks in the source file. */
 		while (tirec.br_blockcount) {
+			int64_t		ip_delta = 0, tip_delta = 0;
+
 			ASSERT(tp->t_firstblock == NULLFSBLOCK);
 			trace_xfs_swap_extent_rmap_remap_piece(tip, &tirec);
 
@@ -1388,6 +1390,23 @@  xfs_swap_extent_rmap(
 					irec.br_blockcount);
 			trace_xfs_swap_extent_rmap_remap_piece(tip, &uirec);
 
+			/* Update quota accounting. */
+			if (xfs_bmap_is_mapped_extent(&irec)) {
+				tip_delta += irec.br_blockcount;
+				ip_delta -= irec.br_blockcount;
+			}
+			if (xfs_bmap_is_mapped_extent(&uirec)) {
+				tip_delta -= uirec.br_blockcount;
+				ip_delta += uirec.br_blockcount;
+			}
+
+			if (tip_delta)
+				xfs_trans_mod_dquot_byino(tp, tip,
+						XFS_TRANS_DQ_BCOUNT, tip_delta);
+			if (ip_delta)
+				xfs_trans_mod_dquot_byino(tp, ip,
+						XFS_TRANS_DQ_BCOUNT, ip_delta);
+
 			/* Remove the mapping from the donor file. */
 			xfs_bmap_unmap_extent(tp, tip, &uirec);
 
@@ -1435,6 +1454,7 @@  xfs_swap_extent_forks(
 {
 	xfs_filblks_t		aforkblks = 0;
 	xfs_filblks_t		taforkblks = 0;
+	int64_t			temp_blks;
 	xfs_extnum_t		junk;
 	uint64_t		tmp;
 	int			error;
@@ -1476,6 +1496,15 @@  xfs_swap_extent_forks(
 	 */
 	swap(ip->i_df, tip->i_df);
 
+	/* Update quota accounting. */
+	temp_blks = tip->i_d.di_nblocks - taforkblks + aforkblks;
+	xfs_trans_mod_dquot_byino(tp, ip, XFS_TRANS_DQ_BCOUNT,
+			temp_blks - ip->i_d.di_nblocks);
+
+	temp_blks = ip->i_d.di_nblocks + taforkblks - aforkblks;
+	xfs_trans_mod_dquot_byino(tp, tip, XFS_TRANS_DQ_BCOUNT,
+			temp_blks - tip->i_d.di_nblocks);
+
 	/*
 	 * Fix the on-disk inode values
 	 */
@@ -1566,6 +1595,93 @@  xfs_swap_change_owner(
 	return error;
 }
 
+/*
+ * Obtain a quota reservation to make sure we don't hit EDQUOT.  We can skip
+ * this if quota enforcement is disabled or if both inodes' dquots are the
+ * same.
+ */
+STATIC int
+xfs_swap_extents_prep_quota(
+	struct xfs_trans	*tp,
+	struct xfs_inode	*ip,
+	struct xfs_inode	*tip)
+{
+	struct xfs_mount	*mp = ip->i_mount;
+	xfs_filblks_t		aforkblks = 0;
+	xfs_filblks_t		taforkblks = 0;
+	xfs_filblks_t		ip_mapped, tip_mapped;
+	xfs_extnum_t		junk;
+	int			error;
+
+	/*
+	 * Don't bother with a quota reservation if we're not enforcing them
+	 * or the two inodes have the same dquots.
+	 */
+	if (!(mp->m_qflags & XFS_ALL_QUOTA_ENFD) ||
+	    (ip->i_udquot == tip->i_udquot &&
+	     ip->i_gdquot == tip->i_gdquot &&
+	     ip->i_pdquot == tip->i_pdquot))
+		return 0;
+
+	/*
+	 * Count the number of extended attribute blocks
+	 */
+	if ( ((XFS_IFORK_Q(ip) != 0) && (ip->i_d.di_anextents > 0)) &&
+	     (ip->i_d.di_aformat != XFS_DINODE_FMT_LOCAL)) {
+		error = xfs_bmap_count_blocks(tp, ip, XFS_ATTR_FORK, &junk,
+				&aforkblks);
+		if (error)
+			return error;
+	}
+	if ( ((XFS_IFORK_Q(tip) != 0) && (tip->i_d.di_anextents > 0)) &&
+	     (tip->i_d.di_aformat != XFS_DINODE_FMT_LOCAL)) {
+		error = xfs_bmap_count_blocks(tp, tip, XFS_ATTR_FORK, &junk,
+				&taforkblks);
+		if (error)
+			return error;
+	}
+
+	/* Figure out how many blocks we'll move out of each file. */
+	ip_mapped = ip->i_d.di_nblocks - aforkblks;
+	tip_mapped = tip->i_d.di_nblocks - taforkblks;
+
+	/*
+	 * For each file, compute the net gain in the number of blocks that
+	 * will be mapped into that file and reserve that much quota.  The
+	 * quota counts must be able to absorb at least that much space.
+	 */
+	if (tip_mapped > ip_mapped) {
+		error = xfs_trans_reserve_quota_nblks(tp, ip,
+				tip_mapped - ip_mapped, 0,
+				XFS_QMOPT_RES_REGBLKS);
+		if (error)
+			return error;
+	}
+
+	if (ip_mapped > tip_mapped) {
+		error = xfs_trans_reserve_quota_nblks(tp, tip,
+				ip_mapped - tip_mapped, 0,
+				XFS_QMOPT_RES_REGBLKS);
+		if (error)
+			return error;
+	}
+
+	/*
+	 * For each file, forcibly reserve the gross gain in mapped blocks so
+	 * that we don't trip over any quota block reservation assertions.
+	 * We must reserve the gross gain because the quota code subtracts from
+	 * bcount the number of blocks that we unmap; it does not add that
+	 * quantity back to the quota block reservation.
+	 */
+	error = xfs_trans_reserve_quota_nblks(tp, ip, ip_mapped, 0,
+			XFS_QMOPT_FORCE_RES | XFS_QMOPT_RES_REGBLKS);
+	if (error)
+		return error;
+
+	return xfs_trans_reserve_quota_nblks(tp, tip, tip_mapped, 0,
+			XFS_QMOPT_FORCE_RES | XFS_QMOPT_RES_REGBLKS);
+}
+
 int
 xfs_swap_extents(
 	struct xfs_inode	*ip,	/* target inode */
@@ -1702,6 +1818,15 @@  xfs_swap_extents(
 		goto out_trans_cancel;
 	}
 
+	/*
+	 * Reserve ourselves some quota if any of them are in enforcing mode.
+	 * In theory we only need enough to satisfy the change in the number
+	 * of blocks between the two ranges being remapped.
+	 */
+	error = xfs_swap_extents_prep_quota(tp, ip, tip);
+	if (error)
+		goto out_trans_cancel;
+
 	/*
 	 * Note the trickiness in setting the log flags - we set the owner log
 	 * flag on the opposite inode (i.e. the inode we are setting the new