xfs: avoid buffer deadlocks when walking fs inodes
authorDarrick J. Wong <djwong@kernel.org>
Fri, 6 Aug 2021 18:05:43 +0000 (11:05 -0700)
committerDarrick J. Wong <djwong@kernel.org>
Mon, 9 Aug 2021 18:13:16 +0000 (11:13 -0700)
When we're servicing an INUMBERS or BULKSTAT request or running
quotacheck, grab an empty transaction so that we can use its inherent
recursive buffer locking abilities to detect inode btree cycles without
hitting ABBA buffer deadlocks.  This patch requires the deferred inode
inactivation patchset because xfs_irele cannot directly call
xfs_inactive when the iwalk itself has an (empty) transaction.

Found by fuzzing an inode btree pointer to introduce a cycle into the
tree (xfs/365).

Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Dave Chinner <dchinner@redhat.com>
Reviewed-by: Christoph Hellwig <hch@lst.de>
fs/xfs/xfs_itable.c
fs/xfs/xfs_iwalk.c

index f331975a16dea7dc5abee683d553c4ced8364ca6..84c17a9f98698ee529106c1e5ef43f1992223237 100644 (file)
@@ -19,6 +19,7 @@
 #include "xfs_error.h"
 #include "xfs_icache.h"
 #include "xfs_health.h"
+#include "xfs_trans.h"
 
 /*
  * Bulk Stat
@@ -163,6 +164,7 @@ xfs_bulkstat_one(
                .formatter      = formatter,
                .breq           = breq,
        };
+       struct xfs_trans        *tp;
        int                     error;
 
        if (breq->mnt_userns != &init_user_ns) {
@@ -178,9 +180,18 @@ xfs_bulkstat_one(
        if (!bc.buf)
                return -ENOMEM;
 
-       error = xfs_bulkstat_one_int(breq->mp, breq->mnt_userns, NULL,
-                                    breq->startino, &bc);
+       /*
+        * Grab an empty transaction so that we can use its recursive buffer
+        * locking abilities to detect cycles in the inobt without deadlocking.
+        */
+       error = xfs_trans_alloc_empty(breq->mp, &tp);
+       if (error)
+               goto out;
 
+       error = xfs_bulkstat_one_int(breq->mp, breq->mnt_userns, tp,
+                       breq->startino, &bc);
+       xfs_trans_cancel(tp);
+out:
        kmem_free(bc.buf);
 
        /*
@@ -244,6 +255,7 @@ xfs_bulkstat(
                .formatter      = formatter,
                .breq           = breq,
        };
+       struct xfs_trans        *tp;
        int                     error;
 
        if (breq->mnt_userns != &init_user_ns) {
@@ -259,9 +271,18 @@ xfs_bulkstat(
        if (!bc.buf)
                return -ENOMEM;
 
-       error = xfs_iwalk(breq->mp, NULL, breq->startino, breq->flags,
-                       xfs_bulkstat_iwalk, breq->icount, &bc);
+       /*
+        * Grab an empty transaction so that we can use its recursive buffer
+        * locking abilities to detect cycles in the inobt without deadlocking.
+        */
+       error = xfs_trans_alloc_empty(breq->mp, &tp);
+       if (error)
+               goto out;
 
+       error = xfs_iwalk(breq->mp, tp, breq->startino, breq->flags,
+                       xfs_bulkstat_iwalk, breq->icount, &bc);
+       xfs_trans_cancel(tp);
+out:
        kmem_free(bc.buf);
 
        /*
@@ -374,13 +395,24 @@ xfs_inumbers(
                .formatter      = formatter,
                .breq           = breq,
        };
+       struct xfs_trans        *tp;
        int                     error = 0;
 
        if (xfs_bulkstat_already_done(breq->mp, breq->startino))
                return 0;
 
-       error = xfs_inobt_walk(breq->mp, NULL, breq->startino, breq->flags,
+       /*
+        * Grab an empty transaction so that we can use its recursive buffer
+        * locking abilities to detect cycles in the inobt without deadlocking.
+        */
+       error = xfs_trans_alloc_empty(breq->mp, &tp);
+       if (error)
+               goto out;
+
+       error = xfs_inobt_walk(breq->mp, tp, breq->startino, breq->flags,
                        xfs_inumbers_walk, breq->icount, &ic);
+       xfs_trans_cancel(tp);
+out:
 
        /*
         * We found some inode groups, so clear the error status and return
index 917d51eefee30ab9e84e52b1ee139df962df826b..7558486f49371c861e3d38a0d803ef2537fa4e06 100644 (file)
@@ -83,6 +83,9 @@ struct xfs_iwalk_ag {
 
        /* Skip empty inobt records? */
        unsigned int                    skip_empty:1;
+
+       /* Drop the (hopefully empty) transaction when calling iwalk_fn. */
+       unsigned int                    drop_trans:1;
 };
 
 /*
@@ -352,7 +355,6 @@ xfs_iwalk_run_callbacks(
        int                             *has_more)
 {
        struct xfs_mount                *mp = iwag->mp;
-       struct xfs_trans                *tp = iwag->tp;
        struct xfs_inobt_rec_incore     *irec;
        xfs_agino_t                     next_agino;
        int                             error;
@@ -362,10 +364,15 @@ xfs_iwalk_run_callbacks(
        ASSERT(iwag->nr_recs > 0);
 
        /* Delete cursor but remember the last record we cached... */
-       xfs_iwalk_del_inobt(tp, curpp, agi_bpp, 0);
+       xfs_iwalk_del_inobt(iwag->tp, curpp, agi_bpp, 0);
        irec = &iwag->recs[iwag->nr_recs - 1];
        ASSERT(next_agino >= irec->ir_startino + XFS_INODES_PER_CHUNK);
 
+       if (iwag->drop_trans) {
+               xfs_trans_cancel(iwag->tp);
+               iwag->tp = NULL;
+       }
+
        error = xfs_iwalk_ag_recs(iwag);
        if (error)
                return error;
@@ -376,8 +383,15 @@ xfs_iwalk_run_callbacks(
        if (!has_more)
                return 0;
 
+       if (iwag->drop_trans) {
+               error = xfs_trans_alloc_empty(mp, &iwag->tp);
+               if (error)
+                       return error;
+       }
+
        /* ...and recreate the cursor just past where we left off. */
-       error = xfs_inobt_cur(mp, tp, iwag->pag, XFS_BTNUM_INO, curpp, agi_bpp);
+       error = xfs_inobt_cur(mp, iwag->tp, iwag->pag, XFS_BTNUM_INO, curpp,
+                       agi_bpp);
        if (error)
                return error;
 
@@ -390,7 +404,6 @@ xfs_iwalk_ag(
        struct xfs_iwalk_ag             *iwag)
 {
        struct xfs_mount                *mp = iwag->mp;
-       struct xfs_trans                *tp = iwag->tp;
        struct xfs_perag                *pag = iwag->pag;
        struct xfs_buf                  *agi_bp = NULL;
        struct xfs_btree_cur            *cur = NULL;
@@ -469,7 +482,7 @@ xfs_iwalk_ag(
        error = xfs_iwalk_run_callbacks(iwag, &cur, &agi_bp, &has_more);
 
 out:
-       xfs_iwalk_del_inobt(tp, &cur, &agi_bp, error);
+       xfs_iwalk_del_inobt(iwag->tp, &cur, &agi_bp, error);
        return error;
 }
 
@@ -599,8 +612,18 @@ xfs_iwalk_ag_work(
        error = xfs_iwalk_alloc(iwag);
        if (error)
                goto out;
+       /*
+        * Grab an empty transaction so that we can use its recursive buffer
+        * locking abilities to detect cycles in the inobt without deadlocking.
+        */
+       error = xfs_trans_alloc_empty(mp, &iwag->tp);
+       if (error)
+               goto out;
+       iwag->drop_trans = 1;
 
        error = xfs_iwalk_ag(iwag);
+       if (iwag->tp)
+               xfs_trans_cancel(iwag->tp);
        xfs_iwalk_free(iwag);
 out:
        xfs_perag_put(iwag->pag);