Commit 0d29a20f authored by Darrick J. Wong's avatar Darrick J. Wong
Browse files

xfs: scrub parent pointers



Actually check parent pointers now.

Signed-off-by: default avatarDarrick J. Wong <djwong@kernel.org>
Reviewed-by: default avatarChristoph Hellwig <hch@lst.de>
parent b961c8bf
Loading
Loading
Loading
Loading
+371 −0
Original line number Diff line number Diff line
@@ -15,11 +15,15 @@
#include "xfs_icache.h"
#include "xfs_dir2.h"
#include "xfs_dir2_priv.h"
#include "xfs_attr.h"
#include "xfs_parent.h"
#include "scrub/scrub.h"
#include "scrub/common.h"
#include "scrub/readdir.h"
#include "scrub/tempfile.h"
#include "scrub/repair.h"
#include "scrub/listxattr.h"
#include "scrub/trace.h"

/* Set us up to scrub parents. */
int
@@ -197,6 +201,370 @@ xchk_parent_validate(
	return error;
}

/*
 * Checking of Parent Pointers
 * ===========================
 *
 * On filesystems with directory parent pointers, we check the referential
 * integrity by visiting each parent pointer of a child file and checking that
 * the directory referenced by the pointer actually has a dirent pointing
 * forward to the child file.
 */

struct xchk_pptrs {
	struct xfs_scrub	*sc;

	/* How many parent pointers did we find at the end? */
	unsigned long long	pptrs_found;

	/* Parent of this directory. */
	xfs_ino_t		parent_ino;
};

/* Does this parent pointer match the dotdot entry? */
STATIC int
xchk_parent_scan_dotdot(
	struct xfs_scrub		*sc,
	struct xfs_inode		*ip,
	unsigned int			attr_flags,
	const unsigned char		*name,
	unsigned int			namelen,
	const void			*value,
	unsigned int			valuelen,
	void				*priv)
{
	struct xchk_pptrs		*pp = priv;
	xfs_ino_t			parent_ino;
	int				error;

	if (!(attr_flags & XFS_ATTR_PARENT))
		return 0;

	error = xfs_parent_from_attr(sc->mp, attr_flags, name, namelen, value,
			valuelen, &parent_ino, NULL);
	if (error)
		return error;

	if (pp->parent_ino == parent_ino)
		return -ECANCELED;

	return 0;
}

/* Look up the dotdot entry so that we can check it as we walk the pptrs. */
STATIC int
xchk_parent_pptr_and_dotdot(
	struct xchk_pptrs	*pp)
{
	struct xfs_scrub	*sc = pp->sc;
	int			error;

	/* Look up '..' */
	error = xchk_dir_lookup(sc, sc->ip, &xfs_name_dotdot, &pp->parent_ino);
	if (!xchk_fblock_process_error(sc, XFS_DATA_FORK, 0, &error))
		return error;
	if (!xfs_verify_dir_ino(sc->mp, pp->parent_ino)) {
		xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
		return 0;
	}

	/* Is this the root dir?  Then '..' must point to itself. */
	if (sc->ip == sc->mp->m_rootip) {
		if (sc->ip->i_ino != pp->parent_ino)
			xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
		return 0;
	}

	/*
	 * If this is now an unlinked directory, the dotdot value is
	 * meaningless as long as it points to a valid inode.
	 */
	if (VFS_I(sc->ip)->i_nlink == 0)
		return 0;

	if (pp->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
		return 0;

	/* Otherwise, walk the pptrs again, and check. */
	error = xchk_xattr_walk(sc, sc->ip, xchk_parent_scan_dotdot, pp);
	if (error == -ECANCELED) {
		/* Found a parent pointer that matches dotdot. */
		return 0;
	}
	if (!error || error == -EFSCORRUPTED) {
		/* Found a broken parent pointer or no match. */
		xchk_fblock_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return 0;
	}
	return error;
}

/*
 * Try to lock a parent directory for checking dirents.  Returns the inode
 * flags for the locks we now hold, or zero if we failed.
 */
STATIC unsigned int
xchk_parent_lock_dir(
	struct xfs_scrub	*sc,
	struct xfs_inode	*dp)
{
	if (!xfs_ilock_nowait(dp, XFS_IOLOCK_SHARED))
		return 0;

	if (!xfs_ilock_nowait(dp, XFS_ILOCK_SHARED)) {
		xfs_iunlock(dp, XFS_IOLOCK_SHARED);
		return 0;
	}

	if (!xfs_need_iread_extents(&dp->i_df))
		return XFS_IOLOCK_SHARED | XFS_ILOCK_SHARED;

	xfs_iunlock(dp, XFS_ILOCK_SHARED);

	if (!xfs_ilock_nowait(dp, XFS_ILOCK_EXCL)) {
		xfs_iunlock(dp, XFS_IOLOCK_SHARED);
		return 0;
	}

	return XFS_IOLOCK_SHARED | XFS_ILOCK_EXCL;
}

/* Check the forward link (dirent) associated with this parent pointer. */
STATIC int
xchk_parent_dirent(
	struct xchk_pptrs	*pp,
	const struct xfs_name	*xname,
	struct xfs_inode	*dp)
{
	struct xfs_scrub	*sc = pp->sc;
	xfs_ino_t		child_ino;
	int			error;

	/*
	 * Use the name attached to this parent pointer to look up the
	 * directory entry in the alleged parent.
	 */
	error = xchk_dir_lookup(sc, dp, xname, &child_ino);
	if (error == -ENOENT) {
		xchk_fblock_xref_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return 0;
	}
	if (!xchk_fblock_xref_process_error(sc, XFS_ATTR_FORK, 0, &error))
		return error;

	/* Does the inode number match? */
	if (child_ino != sc->ip->i_ino) {
		xchk_fblock_xref_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return 0;
	}

	return 0;
}

/* Try to grab a parent directory. */
STATIC int
xchk_parent_iget(
	struct xchk_pptrs	*pp,
	const struct xfs_parent_rec	*pptr,
	struct xfs_inode	**dpp)
{
	struct xfs_scrub	*sc = pp->sc;
	struct xfs_inode	*ip;
	xfs_ino_t		parent_ino = be64_to_cpu(pptr->p_ino);
	int			error;

	/* Validate inode number. */
	error = xfs_dir_ino_validate(sc->mp, parent_ino);
	if (error) {
		xchk_fblock_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return -ECANCELED;
	}

	error = xchk_iget(sc, parent_ino, &ip);
	if (error == -EINVAL || error == -ENOENT) {
		xchk_fblock_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return -ECANCELED;
	}
	if (!xchk_fblock_xref_process_error(sc, XFS_ATTR_FORK, 0, &error))
		return error;

	/* The parent must be a directory. */
	if (!S_ISDIR(VFS_I(ip)->i_mode)) {
		xchk_fblock_xref_set_corrupt(sc, XFS_ATTR_FORK, 0);
		goto out_rele;
	}

	/* Validate generation number. */
	if (VFS_I(ip)->i_generation != be32_to_cpu(pptr->p_gen)) {
		xchk_fblock_xref_set_corrupt(sc, XFS_ATTR_FORK, 0);
		goto out_rele;
	}

	*dpp = ip;
	return 0;
out_rele:
	xchk_irele(sc, ip);
	return 0;
}

/*
 * Walk an xattr of a file.  If this xattr is a parent pointer, follow it up
 * to a parent directory and check that the parent has a dirent pointing back
 * to us.
 */
STATIC int
xchk_parent_scan_attr(
	struct xfs_scrub	*sc,
	struct xfs_inode	*ip,
	unsigned int		attr_flags,
	const unsigned char	*name,
	unsigned int		namelen,
	const void		*value,
	unsigned int		valuelen,
	void			*priv)
{
	struct xfs_name		xname = {
		.name		= name,
		.len		= namelen,
	};
	struct xchk_pptrs	*pp = priv;
	struct xfs_inode	*dp = NULL;
	const struct xfs_parent_rec *pptr_rec = value;
	xfs_ino_t		parent_ino;
	unsigned int		lockmode;
	int			error;

	if (!(attr_flags & XFS_ATTR_PARENT))
		return 0;

	error = xfs_parent_from_attr(sc->mp, attr_flags, name, namelen, value,
			valuelen, &parent_ino, NULL);
	if (error) {
		xchk_fblock_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return error;
	}

	/* No self-referential parent pointers. */
	if (parent_ino == sc->ip->i_ino) {
		xchk_fblock_set_corrupt(sc, XFS_ATTR_FORK, 0);
		return -ECANCELED;
	}

	pp->pptrs_found++;

	error = xchk_parent_iget(pp, pptr_rec, &dp);
	if (error)
		return error;
	if (!dp)
		return 0;

	/* Try to lock the inode. */
	lockmode = xchk_parent_lock_dir(sc, dp);
	if (!lockmode) {
		xchk_set_incomplete(sc);
		error = -ECANCELED;
		goto out_rele;
	}

	error = xchk_parent_dirent(pp, &xname, dp);
	if (error)
		goto out_unlock;

out_unlock:
	xfs_iunlock(dp, lockmode);
out_rele:
	xchk_irele(sc, dp);
	return error;
}

/*
 * Compare the number of parent pointers to the link count.  For
 * non-directories these should be the same.  For unlinked directories the
 * count should be zero; for linked directories, it should be nonzero.
 */
STATIC int
xchk_parent_count_pptrs(
	struct xchk_pptrs	*pp)
{
	struct xfs_scrub	*sc = pp->sc;

	if (S_ISDIR(VFS_I(sc->ip)->i_mode)) {
		if (sc->ip == sc->mp->m_rootip)
			pp->pptrs_found++;

		if (VFS_I(sc->ip)->i_nlink == 0 && pp->pptrs_found > 0)
			xchk_ino_set_corrupt(sc, sc->ip->i_ino);
		else if (VFS_I(sc->ip)->i_nlink > 0 &&
			 pp->pptrs_found == 0)
			xchk_ino_set_corrupt(sc, sc->ip->i_ino);
	} else {
		if (VFS_I(sc->ip)->i_nlink != pp->pptrs_found)
			xchk_ino_set_corrupt(sc, sc->ip->i_ino);
	}

	return 0;
}

/* Check parent pointers of a file. */
STATIC int
xchk_parent_pptr(
	struct xfs_scrub	*sc)
{
	struct xchk_pptrs	*pp;
	int			error;

	pp = kvzalloc(sizeof(struct xchk_pptrs), XCHK_GFP_FLAGS);
	if (!pp)
		return -ENOMEM;
	pp->sc = sc;

	error = xchk_xattr_walk(sc, sc->ip, xchk_parent_scan_attr, pp);
	if (error == -ECANCELED) {
		error = 0;
		goto out_pp;
	}
	if (error)
		goto out_pp;

	if (pp->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
		goto out_pp;

	/*
	 * For subdirectories, make sure the dotdot entry references the same
	 * inode as the parent pointers.
	 *
	 * If we're scanning a /consistent/ directory, there should only be
	 * one parent pointer, and it should point to the same directory as
	 * the dotdot entry.
	 *
	 * However, a corrupt directory tree might feature a subdirectory with
	 * multiple parents.  The directory loop scanner is responsible for
	 * correcting that kind of problem, so for now we only validate that
	 * the dotdot entry matches /one/ of the parents.
	 */
	if (S_ISDIR(VFS_I(sc->ip)->i_mode)) {
		error = xchk_parent_pptr_and_dotdot(pp);
		if (error)
			goto out_pp;
	}

	if (pp->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
		goto out_pp;

	/*
	 * Complain if the number of parent pointers doesn't match the link
	 * count.  This could be a sign of missing parent pointers (or an
	 * incorrect link count).
	 */
	error = xchk_parent_count_pptrs(pp);
	if (error)
		goto out_pp;

out_pp:
	kvfree(pp);
	return error;
}

/* Scrub a parent pointer. */
int
xchk_parent(
@@ -206,6 +574,9 @@ xchk_parent(
	xfs_ino_t		parent_ino;
	int			error = 0;

	if (xfs_has_parent(mp))
		return xchk_parent_pptr(sc);

	/*
	 * If we're a directory, check that the '..' link points up to
	 * a directory that has one entry pointing to us.