summaryrefslogtreecommitdiffstats
path: root/fs/xfs/scrub/refcount.c
diff options
context:
space:
mode:
authorDarrick J. Wong <djwong@kernel.org>2022-10-10 09:06:24 -0700
committerDarrick J. Wong <djwong@kernel.org>2022-10-31 08:58:21 -0700
commit9a50ee4f8db6e4dd0d8d757b7adaf0591776860a (patch)
tree136d1cac30e3b07333be46da4b777edaf300389a /fs/xfs/scrub/refcount.c
parent5a8c345ca8b99a9f54b89991f2f6a20521cb05f4 (diff)
downloadlinux-stable-9a50ee4f8db6e4dd0d8d757b7adaf0591776860a.tar.gz
linux-stable-9a50ee4f8db6e4dd0d8d757b7adaf0591776860a.tar.bz2
linux-stable-9a50ee4f8db6e4dd0d8d757b7adaf0591776860a.zip
xfs: track cow/shared record domains explicitly in xfs_refcount_irec
Just prior to committing the reflink code into upstream, the xfs maintainer at the time requested that I find a way to shard the refcount records into two domains -- one for records tracking shared extents, and a second for tracking CoW staging extents. The idea here was to minimize mount time CoW reclamation by pushing all the CoW records to the right edge of the keyspace, and it was accomplished by setting the upper bit in rc_startblock. We don't allow AGs to have more than 2^31 blocks, so the bit was free. Unfortunately, this was a very late addition to the codebase, so most of the refcount record processing code still treats rc_startblock as a u32 and pays no attention to whether or not the upper bit (the cow flag) is set. This is a weakness is theoretically exploitable, since we're not fully validating the incoming metadata records. Fuzzing demonstrates practical exploits of this weakness. If the cow flag of a node block key record is corrupted, a lookup operation can go to the wrong record block and start returning records from the wrong cow/shared domain. This causes the math to go all wrong (since cow domain is still implicit in the upper bit of rc_startblock) and we can crash the kernel by tricking xfs into jumping into a nonexistent AG and tripping over xfs_perag_get(mp, <nonexistent AG>) returning NULL. To fix this, start tracking the domain as an explicit part of struct xfs_refcount_irec, adjust all refcount functions to check the domain of a returned record, and alter the function definitions to accept them where necessary. Found by fuzzing keys[2].cowflag = add in xfs/464. Signed-off-by: Darrick J. Wong <djwong@kernel.org> Reviewed-by: Dave Chinner <dchinner@redhat.com>
Diffstat (limited to 'fs/xfs/scrub/refcount.c')
-rw-r--r--fs/xfs/scrub/refcount.c23
1 files changed, 10 insertions, 13 deletions
diff --git a/fs/xfs/scrub/refcount.c b/fs/xfs/scrub/refcount.c
index 9e6b36ac8079..af5b796ec9ec 100644
--- a/fs/xfs/scrub/refcount.c
+++ b/fs/xfs/scrub/refcount.c
@@ -334,21 +334,19 @@ xchk_refcountbt_rec(
struct xfs_refcount_irec irec;
xfs_agblock_t *cow_blocks = bs->private;
struct xfs_perag *pag = bs->cur->bc_ag.pag;
- bool has_cowflag;
xfs_refcount_btrec_to_irec(rec, &irec);
/* Only CoW records can have refcount == 1. */
- has_cowflag = (irec.rc_startblock & XFS_REFC_COW_START);
- if ((irec.rc_refcount == 1 && !has_cowflag) ||
- (irec.rc_refcount != 1 && has_cowflag))
+ if (irec.rc_domain == XFS_REFC_DOMAIN_SHARED && irec.rc_refcount == 1)
xchk_btree_set_corrupt(bs->sc, bs->cur, 0);
- if (has_cowflag)
+ if (irec.rc_domain == XFS_REFC_DOMAIN_COW) {
+ if (irec.rc_refcount != 1)
+ xchk_btree_set_corrupt(bs->sc, bs->cur, 0);
(*cow_blocks) += irec.rc_blockcount;
+ }
/* Check the extent. */
- irec.rc_startblock &= ~XFS_REFC_COW_START;
-
if (!xfs_verify_agbext(pag, irec.rc_startblock, irec.rc_blockcount))
xchk_btree_set_corrupt(bs->sc, bs->cur, 0);
@@ -419,7 +417,6 @@ xchk_xref_is_cow_staging(
xfs_extlen_t len)
{
struct xfs_refcount_irec rc;
- bool has_cowflag;
int has_refcount;
int error;
@@ -427,8 +424,8 @@ xchk_xref_is_cow_staging(
return;
/* Find the CoW staging extent. */
- error = xfs_refcount_lookup_le(sc->sa.refc_cur,
- agbno + XFS_REFC_COW_START, &has_refcount);
+ error = xfs_refcount_lookup_le(sc->sa.refc_cur, XFS_REFC_DOMAIN_COW,
+ agbno, &has_refcount);
if (!xchk_should_check_xref(sc, &error, &sc->sa.refc_cur))
return;
if (!has_refcount) {
@@ -445,8 +442,7 @@ xchk_xref_is_cow_staging(
}
/* CoW flag must be set, refcount must be 1. */
- has_cowflag = (rc.rc_startblock & XFS_REFC_COW_START);
- if (!has_cowflag || rc.rc_refcount != 1)
+ if (rc.rc_domain != XFS_REFC_DOMAIN_COW || rc.rc_refcount != 1)
xchk_btree_xref_set_corrupt(sc, sc->sa.refc_cur, 0);
/* Must be at least as long as what was passed in */
@@ -470,7 +466,8 @@ xchk_xref_is_not_shared(
if (!sc->sa.refc_cur || xchk_skip_xref(sc->sm))
return;
- error = xfs_refcount_has_record(sc->sa.refc_cur, agbno, len, &shared);
+ error = xfs_refcount_has_record(sc->sa.refc_cur, XFS_REFC_DOMAIN_SHARED,
+ agbno, len, &shared);
if (!xchk_should_check_xref(sc, &error, &sc->sa.refc_cur))
return;
if (shared)