xfs: fix race in inode cluster freeing failing to stale inodes
authorDave Chinner <david@fromorbit.com>
Thu, 3 Jun 2010 06:22:29 +0000 (16:22 +1000)
committerDave Chinner <david@fromorbit.com>
Thu, 3 Jun 2010 06:22:29 +0000 (16:22 +1000)
When an inode cluster is freed, it needs to mark all inodes in memory as
XFS_ISTALE before marking the buffer as stale. This is eeded because the inodes
have a different life cycle to the buffer, and once the buffer is torn down
during transaction completion, we must ensure none of the inodes get written
back (which is what XFS_ISTALE does).

Unfortunately, xfs_ifree_cluster() has some bugs that lead to inodes not being
marked with XFS_ISTALE. This shows up when xfs_iflush() is called on these
inodes either during inode reclaim or tail pushing on the AIL.  The buffer is
read back, but no longer contains inodes and so triggers assert failures and
shutdowns. This was reproducable with at run.dbench10 invocation from xfstests.

There are two main causes of xfs_ifree_cluster() failing. The first is simple -
it checks in-memory inodes it finds in the per-ag icache to see if they are
clean without holding the flush lock. if they are clean it skips them
completely. However, If an inode is flushed delwri, it will
appear clean, but is not guaranteed to be written back until the flush lock has
been dropped. Hence we may have raced on the clean check and the inode may
actually be dirty. Hence always mark inodes found in memory stale before we
check properly if they are clean.

The second is more complex, and makes the first problem easier to hit.
Basically the in-memory inode scan is done with full knowledge it can be racing
with inode flushing and AIl tail pushing, which means that inodes that it can't
get the flush lock on might not be attached to the buffer after then in-memory
inode scan due to IO completion occurring. This is actually documented in the
code as "needs better interlocking". i.e. this is a zero-day bug.

Effectively, the in-memory scan must be done while the inode buffer is locked
and Io cannot be issued on it while we do the in-memory inode scan. This
ensures that inodes we couldn't get the flush lock on are guaranteed to be
attached to the cluster buffer, so we can then catch all in-memory inodes and
mark them stale.

Now that the inode cluster buffer is locked before the in-memory scan is done,
there is no need for the two-phase update of the in-memory inodes, so simplify
the code into two loops and remove the allocation of the temporary buffer used
to hold locked inodes across the phases.

Signed-off-by: Dave Chinner <dchinner@redhat.com>
Reviewed-by: Christoph Hellwig <hch@lst.de>
fs/xfs/xfs_inode.c

index 6ba7765..d53c39d 100644 (file)
@@ -1940,10 +1940,10 @@ xfs_ifree_cluster(
        int                     blks_per_cluster;
        int                     nbufs;
        int                     ninodes;
-       int                     i, j, found, pre_flushed;
+       int                     i, j;
        xfs_daddr_t             blkno;
        xfs_buf_t               *bp;
-       xfs_inode_t             *ip, **ip_found;
+       xfs_inode_t             *ip;
        xfs_inode_log_item_t    *iip;
        xfs_log_item_t          *lip;
        struct xfs_perag        *pag;
@@ -1960,114 +1960,97 @@ xfs_ifree_cluster(
                nbufs = XFS_IALLOC_BLOCKS(mp) / blks_per_cluster;
        }
 
-       ip_found = kmem_alloc(ninodes * sizeof(xfs_inode_t *), KM_NOFS);
-
        for (j = 0; j < nbufs; j++, inum += ninodes) {
+               int     found = 0;
+
                blkno = XFS_AGB_TO_DADDR(mp, XFS_INO_TO_AGNO(mp, inum),
                                         XFS_INO_TO_AGBNO(mp, inum));
 
+               /*
+                * We obtain and lock the backing buffer first in the process
+                * here, as we have to ensure that any dirty inode that we
+                * can't get the flush lock on is attached to the buffer.
+                * If we scan the in-memory inodes first, then buffer IO can
+                * complete before we get a lock on it, and hence we may fail
+                * to mark all the active inodes on the buffer stale.
+                */
+               bp = xfs_trans_get_buf(tp, mp->m_ddev_targp, blkno,
+                                       mp->m_bsize * blks_per_cluster,
+                                       XBF_LOCK);
+
+               /*
+                * Walk the inodes already attached to the buffer and mark them
+                * stale. These will all have the flush locks held, so an
+                * in-memory inode walk can't lock them.
+                */
+               lip = XFS_BUF_FSPRIVATE(bp, xfs_log_item_t *);
+               while (lip) {
+                       if (lip->li_type == XFS_LI_INODE) {
+                               iip = (xfs_inode_log_item_t *)lip;
+                               ASSERT(iip->ili_logged == 1);
+                               lip->li_cb = (void(*)(xfs_buf_t*,xfs_log_item_t*)) xfs_istale_done;
+                               xfs_trans_ail_copy_lsn(mp->m_ail,
+                                                       &iip->ili_flush_lsn,
+                                                       &iip->ili_item.li_lsn);
+                               xfs_iflags_set(iip->ili_inode, XFS_ISTALE);
+                               found++;
+                       }
+                       lip = lip->li_bio_list;
+               }
 
                /*
-                * Look for each inode in memory and attempt to lock it,
-                * we can be racing with flush and tail pushing here.
-                * any inode we get the locks on, add to an array of
-                * inode items to process later.
+                * For each inode in memory attempt to add it to the inode
+                * buffer and set it up for being staled on buffer IO
+                * completion.  This is safe as we've locked out tail pushing
+                * and flushing by locking the buffer.
                 *
-                * The get the buffer lock, we could beat a flush
-                * or tail pushing thread to the lock here, in which
-                * case they will go looking for the inode buffer
-                * and fail, we need some other form of interlock
-                * here.
+                * We have already marked every inode that was part of a
+                * transaction stale above, which means there is no point in
+                * even trying to lock them.
                 */
-               found = 0;
                for (i = 0; i < ninodes; i++) {
                        read_lock(&pag->pag_ici_lock);
                        ip = radix_tree_lookup(&pag->pag_ici_root,
                                        XFS_INO_TO_AGINO(mp, (inum + i)));
 
-                       /* Inode not in memory or we found it already,
-                        * nothing to do
-                        */
+                       /* Inode not in memory or stale, nothing to do */
                        if (!ip || xfs_iflags_test(ip, XFS_ISTALE)) {
                                read_unlock(&pag->pag_ici_lock);
                                continue;
                        }
 
-                       if (xfs_inode_clean(ip)) {
-                               read_unlock(&pag->pag_ici_lock);
-                               continue;
-                       }
-
-                       /* If we can get the locks then add it to the
-                        * list, otherwise by the time we get the bp lock
-                        * below it will already be attached to the
-                        * inode buffer.
-                        */
-
-                       /* This inode will already be locked - by us, lets
-                        * keep it that way.
-                        */
-
-                       if (ip == free_ip) {
-                               if (xfs_iflock_nowait(ip)) {
-                                       xfs_iflags_set(ip, XFS_ISTALE);
-                                       if (xfs_inode_clean(ip)) {
-                                               xfs_ifunlock(ip);
-                                       } else {
-                                               ip_found[found++] = ip;
-                                       }
-                               }
+                       /* don't try to lock/unlock the current inode */
+                       if (ip != free_ip &&
+                           !xfs_ilock_nowait(ip, XFS_ILOCK_EXCL)) {
                                read_unlock(&pag->pag_ici_lock);
                                continue;
                        }
+                       read_unlock(&pag->pag_ici_lock);
 
-                       if (xfs_ilock_nowait(ip, XFS_ILOCK_EXCL)) {
-                               if (xfs_iflock_nowait(ip)) {
-                                       xfs_iflags_set(ip, XFS_ISTALE);
-
-                                       if (xfs_inode_clean(ip)) {
-                                               xfs_ifunlock(ip);
-                                               xfs_iunlock(ip, XFS_ILOCK_EXCL);
-                                       } else {
-                                               ip_found[found++] = ip;
-                                       }
-                               } else {
+                       if (!xfs_iflock_nowait(ip)) {
+                               if (ip != free_ip)
                                        xfs_iunlock(ip, XFS_ILOCK_EXCL);
-                               }
+                               continue;
                        }
-                       read_unlock(&pag->pag_ici_lock);
-               }
 
-               bp = xfs_trans_get_buf(tp, mp->m_ddev_targp, blkno, 
-                                       mp->m_bsize * blks_per_cluster,
-                                       XBF_LOCK);
-
-               pre_flushed = 0;
-               lip = XFS_BUF_FSPRIVATE(bp, xfs_log_item_t *);
-               while (lip) {
-                       if (lip->li_type == XFS_LI_INODE) {
-                               iip = (xfs_inode_log_item_t *)lip;
-                               ASSERT(iip->ili_logged == 1);
-                               lip->li_cb = (void(*)(xfs_buf_t*,xfs_log_item_t*)) xfs_istale_done;
-                               xfs_trans_ail_copy_lsn(mp->m_ail,
-                                                       &iip->ili_flush_lsn,
-                                                       &iip->ili_item.li_lsn);
-                               xfs_iflags_set(iip->ili_inode, XFS_ISTALE);
-                               pre_flushed++;
+                       xfs_iflags_set(ip, XFS_ISTALE);
+                       if (xfs_inode_clean(ip)) {
+                               ASSERT(ip != free_ip);
+                               xfs_ifunlock(ip);
+                               xfs_iunlock(ip, XFS_ILOCK_EXCL);
+                               continue;
                        }
-                       lip = lip->li_bio_list;
-               }
 
-               for (i = 0; i < found; i++) {
-                       ip = ip_found[i];
                        iip = ip->i_itemp;
-
                        if (!iip) {
+                               /* inode with unlogged changes only */
+                               ASSERT(ip != free_ip);
                                ip->i_update_core = 0;
                                xfs_ifunlock(ip);
                                xfs_iunlock(ip, XFS_ILOCK_EXCL);
                                continue;
                        }
+                       found++;
 
                        iip->ili_last_fields = iip->ili_format.ilf_fields;
                        iip->ili_format.ilf_fields = 0;
@@ -2078,17 +2061,16 @@ xfs_ifree_cluster(
                        xfs_buf_attach_iodone(bp,
                                (void(*)(xfs_buf_t*,xfs_log_item_t*))
                                xfs_istale_done, (xfs_log_item_t *)iip);
-                       if (ip != free_ip) {
+
+                       if (ip != free_ip)
                                xfs_iunlock(ip, XFS_ILOCK_EXCL);
-                       }
                }
 
-               if (found || pre_flushed)
+               if (found)
                        xfs_trans_stale_inode_buf(tp, bp);
                xfs_trans_binval(tp, bp);
        }
 
-       kmem_free(ip_found);
        xfs_perag_put(pag);
 }