Skip to content

Commit

Permalink
xfs: Don't allow logging of XFS_ISTALE inodes
Browse files Browse the repository at this point in the history
[ Upstream commit 96355d5 ]

In tracking down a problem in this patchset, I discovered we are
reclaiming dirty stale inodes. This wasn't discovered until inodes
were always attached to the cluster buffer and then the rcu callback
that freed inodes was assert failing because the inode still had an
active pointer to the cluster buffer after it had been reclaimed.

Debugging the issue indicated that this was a pre-existing issue
resulting from the way the inodes are handled in xfs_inactive_ifree.
When we free a cluster buffer from xfs_ifree_cluster, all the inodes
in cache are marked XFS_ISTALE. Those that are clean have nothing
else done to them and so eventually get cleaned up by background
reclaim. i.e. it is assumed we'll never dirty/relog an inode marked
XFS_ISTALE.

On journal commit dirty stale inodes as are handled by both
buffer and inode log items to run though xfs_istale_done() and
removed from the AIL (buffer log item commit) or the log item will
simply unpin it because the buffer log item will clean it. What happens
to any specific inode is entirely dependent on which log item wins
the commit race, but the result is the same - stale inodes are
clean, not attached to the cluster buffer, and not in the AIL. Hence
inode reclaim can just free these inodes without further care.

However, if the stale inode is relogged, it gets dirtied again and
relogged into the CIL. Most of the time this isn't an issue, because
relogging simply changes the inode's location in the current
checkpoint. Problems arise, however, when the CIL checkpoints
between two transactions in the xfs_inactive_ifree() deferops
processing. This results in the XFS_ISTALE inode being redirtied
and inserted into the CIL without any of the other stale cluster
buffer infrastructure being in place.

Hence on journal commit, it simply gets unpinned, so it remains
dirty in memory. Everything in inode writeback avoids XFS_ISTALE
inodes so it can't be written back, and it is not tracked in the AIL
so there's not even a trigger to attempt to clean the inode. Hence
the inode just sits dirty in memory until inode reclaim comes along,
sees that it is XFS_ISTALE, and goes to reclaim it. This reclaiming
of a dirty inode caused use after free, list corruptions and other
nasty issues later in this patchset.

Hence this patch addresses a violation of the "never log XFS_ISTALE
inodes" caused by the deferops processing rolling a transaction
and relogging a stale inode in xfs_inactive_free. It also adds a
bunch of asserts to catch this problem in debug kernels so that
we don't reintroduce this problem in future.

Reproducer for this issue was generic/558 on a v4 filesystem.

Signed-off-by: Dave Chinner <dchinner@redhat.com>
Reviewed-by: Brian Foster <bfoster@redhat.com>
Reviewed-by: Darrick J. Wong <darrick.wong@oracle.com>
Signed-off-by: Darrick J. Wong <darrick.wong@oracle.com>
Signed-off-by: Sasha Levin <sashal@kernel.org>
  • Loading branch information
Dave Chinner authored and gregkh committed Sep 3, 2020
1 parent 2b5055b commit bce0f66
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 4 deletions.
2 changes: 2 additions & 0 deletions fs/xfs/libxfs/xfs_trans_inode.c
Expand Up @@ -36,6 +36,7 @@ xfs_trans_ijoin(

ASSERT(iip->ili_lock_flags == 0);
iip->ili_lock_flags = lock_flags;
ASSERT(!xfs_iflags_test(ip, XFS_ISTALE));

/*
* Get a log_item_desc to point at the new item.
Expand Down Expand Up @@ -89,6 +90,7 @@ xfs_trans_log_inode(

ASSERT(ip->i_itemp != NULL);
ASSERT(xfs_isilocked(ip, XFS_ILOCK_EXCL));
ASSERT(!xfs_iflags_test(ip, XFS_ISTALE));

/*
* Don't bother with i_lock for the I_DIRTY_TIME check here, as races
Expand Down
3 changes: 2 additions & 1 deletion fs/xfs/xfs_icache.c
Expand Up @@ -1141,7 +1141,7 @@ xfs_reclaim_inode(
goto out_ifunlock;
xfs_iunpin_wait(ip);
}
if (xfs_iflags_test(ip, XFS_ISTALE) || xfs_inode_clean(ip)) {
if (xfs_inode_clean(ip)) {
xfs_ifunlock(ip);
goto reclaim;
}
Expand Down Expand Up @@ -1228,6 +1228,7 @@ xfs_reclaim_inode(
xfs_ilock(ip, XFS_ILOCK_EXCL);
xfs_qm_dqdetach(ip);
xfs_iunlock(ip, XFS_ILOCK_EXCL);
ASSERT(xfs_inode_clean(ip));

__xfs_inode_free(ip);
return error;
Expand Down
25 changes: 22 additions & 3 deletions fs/xfs/xfs_inode.c
Expand Up @@ -1740,10 +1740,31 @@ xfs_inactive_ifree(
return error;
}

/*
* We do not hold the inode locked across the entire rolling transaction
* here. We only need to hold it for the first transaction that
* xfs_ifree() builds, which may mark the inode XFS_ISTALE if the
* underlying cluster buffer is freed. Relogging an XFS_ISTALE inode
* here breaks the relationship between cluster buffer invalidation and
* stale inode invalidation on cluster buffer item journal commit
* completion, and can result in leaving dirty stale inodes hanging
* around in memory.
*
* We have no need for serialising this inode operation against other
* operations - we freed the inode and hence reallocation is required
* and that will serialise on reallocating the space the deferops need
* to free. Hence we can unlock the inode on the first commit of
* the transaction rather than roll it right through the deferops. This
* avoids relogging the XFS_ISTALE inode.
*
* We check that xfs_ifree() hasn't grown an internal transaction roll
* by asserting that the inode is still locked when it returns.
*/
xfs_ilock(ip, XFS_ILOCK_EXCL);
xfs_trans_ijoin(tp, ip, 0);
xfs_trans_ijoin(tp, ip, XFS_ILOCK_EXCL);

error = xfs_ifree(tp, ip);
ASSERT(xfs_isilocked(ip, XFS_ILOCK_EXCL));
if (error) {
/*
* If we fail to free the inode, shut down. The cancel
Expand All @@ -1756,7 +1777,6 @@ xfs_inactive_ifree(
xfs_force_shutdown(mp, SHUTDOWN_META_IO_ERROR);
}
xfs_trans_cancel(tp);
xfs_iunlock(ip, XFS_ILOCK_EXCL);
return error;
}

Expand All @@ -1774,7 +1794,6 @@ xfs_inactive_ifree(
xfs_notice(mp, "%s: xfs_trans_commit returned error %d",
__func__, error);

xfs_iunlock(ip, XFS_ILOCK_EXCL);
return 0;
}

Expand Down

0 comments on commit bce0f66

Please sign in to comment.