80d3d33cdf
kmemleak reported that we don't free the parent pointer names here if we
found corruption.
Fixes: 0d29a20fbd
("xfs: scrub parent pointers")
Signed-off-by: Darrick J. Wong <djwong@kernel.org>
Reviewed-by: Christoph Hellwig <hch@lst.de>
Signed-off-by: Chandan Babu R <chandanbabu@kernel.org>
938 lines
22 KiB
C
938 lines
22 KiB
C
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
/*
|
|
* Copyright (C) 2017-2023 Oracle. All Rights Reserved.
|
|
* Author: Darrick J. Wong <djwong@kernel.org>
|
|
*/
|
|
#include "xfs.h"
|
|
#include "xfs_fs.h"
|
|
#include "xfs_shared.h"
|
|
#include "xfs_format.h"
|
|
#include "xfs_trans_resv.h"
|
|
#include "xfs_mount.h"
|
|
#include "xfs_log_format.h"
|
|
#include "xfs_trans.h"
|
|
#include "xfs_inode.h"
|
|
#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/xfile.h"
|
|
#include "scrub/xfarray.h"
|
|
#include "scrub/xfblob.h"
|
|
#include "scrub/trace.h"
|
|
|
|
/* Set us up to scrub parents. */
|
|
int
|
|
xchk_setup_parent(
|
|
struct xfs_scrub *sc)
|
|
{
|
|
int error;
|
|
|
|
if (xchk_could_repair(sc)) {
|
|
error = xrep_setup_parent(sc);
|
|
if (error)
|
|
return error;
|
|
}
|
|
|
|
return xchk_setup_inode_contents(sc, 0);
|
|
}
|
|
|
|
/* Parent pointers */
|
|
|
|
/* Look for an entry in a parent pointing to this inode. */
|
|
|
|
struct xchk_parent_ctx {
|
|
struct xfs_scrub *sc;
|
|
xfs_nlink_t nlink;
|
|
};
|
|
|
|
/* Look for a single entry in a directory pointing to an inode. */
|
|
STATIC int
|
|
xchk_parent_actor(
|
|
struct xfs_scrub *sc,
|
|
struct xfs_inode *dp,
|
|
xfs_dir2_dataptr_t dapos,
|
|
const struct xfs_name *name,
|
|
xfs_ino_t ino,
|
|
void *priv)
|
|
{
|
|
struct xchk_parent_ctx *spc = priv;
|
|
int error = 0;
|
|
|
|
/* Does this name make sense? */
|
|
if (!xfs_dir2_namecheck(name->name, name->len))
|
|
error = -EFSCORRUPTED;
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_DATA_FORK, 0, &error))
|
|
return error;
|
|
|
|
if (sc->ip->i_ino == ino)
|
|
spc->nlink++;
|
|
|
|
if (xchk_should_terminate(spc->sc, &error))
|
|
return error;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* 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_ilock_dir(
|
|
struct xfs_inode *dp)
|
|
{
|
|
if (!xfs_ilock_nowait(dp, XFS_ILOCK_SHARED))
|
|
return 0;
|
|
|
|
if (!xfs_need_iread_extents(&dp->i_df))
|
|
return XFS_ILOCK_SHARED;
|
|
|
|
xfs_iunlock(dp, XFS_ILOCK_SHARED);
|
|
|
|
if (!xfs_ilock_nowait(dp, XFS_ILOCK_EXCL))
|
|
return 0;
|
|
|
|
return XFS_ILOCK_EXCL;
|
|
}
|
|
|
|
/*
|
|
* Given the inode number of the alleged parent of the inode being scrubbed,
|
|
* try to validate that the parent has exactly one directory entry pointing
|
|
* back to the inode being scrubbed. Returns -EAGAIN if we need to revalidate
|
|
* the dotdot entry.
|
|
*/
|
|
STATIC int
|
|
xchk_parent_validate(
|
|
struct xfs_scrub *sc,
|
|
xfs_ino_t parent_ino)
|
|
{
|
|
struct xchk_parent_ctx spc = {
|
|
.sc = sc,
|
|
.nlink = 0,
|
|
};
|
|
struct xfs_mount *mp = sc->mp;
|
|
struct xfs_inode *dp = NULL;
|
|
xfs_nlink_t expected_nlink;
|
|
unsigned int lock_mode;
|
|
int error = 0;
|
|
|
|
/* Is this the root dir? Then '..' must point to itself. */
|
|
if (sc->ip == mp->m_rootip) {
|
|
if (sc->ip->i_ino != mp->m_sb.sb_rootino ||
|
|
sc->ip->i_ino != parent_ino)
|
|
xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
|
|
return 0;
|
|
}
|
|
|
|
/* '..' must not point to ourselves. */
|
|
if (sc->ip->i_ino == parent_ino) {
|
|
xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* If we're an unlinked directory, the parent /won't/ have a link
|
|
* to us. Otherwise, it should have one link.
|
|
*/
|
|
expected_nlink = VFS_I(sc->ip)->i_nlink == 0 ? 0 : 1;
|
|
|
|
/*
|
|
* Grab the parent directory inode. This must be released before we
|
|
* cancel the scrub transaction.
|
|
*
|
|
* If _iget returns -EINVAL or -ENOENT then the parent inode number is
|
|
* garbage and the directory is corrupt. If the _iget returns
|
|
* -EFSCORRUPTED or -EFSBADCRC then the parent is corrupt which is a
|
|
* cross referencing error. Any other error is an operational error.
|
|
*/
|
|
error = xchk_iget(sc, parent_ino, &dp);
|
|
if (error == -EINVAL || error == -ENOENT) {
|
|
error = -EFSCORRUPTED;
|
|
xchk_fblock_process_error(sc, XFS_DATA_FORK, 0, &error);
|
|
return error;
|
|
}
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_DATA_FORK, 0, &error))
|
|
return error;
|
|
if (dp == sc->ip || xrep_is_tempfile(dp) ||
|
|
!S_ISDIR(VFS_I(dp)->i_mode)) {
|
|
xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
|
|
goto out_rele;
|
|
}
|
|
|
|
lock_mode = xchk_parent_ilock_dir(dp);
|
|
if (!lock_mode) {
|
|
xchk_iunlock(sc, XFS_ILOCK_EXCL);
|
|
xchk_ilock(sc, XFS_ILOCK_EXCL);
|
|
error = -EAGAIN;
|
|
goto out_rele;
|
|
}
|
|
|
|
/*
|
|
* We cannot yet validate this parent pointer if the directory looks as
|
|
* though it has been zapped by the inode record repair code.
|
|
*/
|
|
if (xchk_dir_looks_zapped(dp)) {
|
|
error = -EBUSY;
|
|
xchk_set_incomplete(sc);
|
|
goto out_unlock;
|
|
}
|
|
|
|
/* Look for a directory entry in the parent pointing to the child. */
|
|
error = xchk_dir_walk(sc, dp, xchk_parent_actor, &spc);
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_DATA_FORK, 0, &error))
|
|
goto out_unlock;
|
|
|
|
/*
|
|
* Ensure that the parent has as many links to the child as the child
|
|
* thinks it has to the parent.
|
|
*/
|
|
if (spc.nlink != expected_nlink)
|
|
xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
|
|
|
|
out_unlock:
|
|
xfs_iunlock(dp, lock_mode);
|
|
out_rele:
|
|
xchk_irele(sc, dp);
|
|
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.
|
|
*/
|
|
|
|
/* Deferred parent pointer entry that we saved for later. */
|
|
struct xchk_pptr {
|
|
/* Cookie for retrieval of the pptr name. */
|
|
xfblob_cookie name_cookie;
|
|
|
|
/* Parent pointer record. */
|
|
struct xfs_parent_rec pptr_rec;
|
|
|
|
/* Length of the pptr name. */
|
|
uint8_t namelen;
|
|
};
|
|
|
|
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;
|
|
|
|
/* Fixed-size array of xchk_pptr structures. */
|
|
struct xfarray *pptr_entries;
|
|
|
|
/* Blobs containing parent pointer names. */
|
|
struct xfblob *pptr_names;
|
|
|
|
/* Scratch buffer for scanning pptr xattrs */
|
|
struct xfs_da_args pptr_args;
|
|
|
|
/* If we've cycled the ILOCK, we must revalidate all deferred pptrs. */
|
|
bool need_revalidate;
|
|
|
|
/* Name buffer */
|
|
struct xfs_name xname;
|
|
char namebuf[MAXNAMELEN];
|
|
};
|
|
|
|
/* 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, NULL, 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) {
|
|
struct xchk_pptr save_pp = {
|
|
.pptr_rec = *pptr_rec, /* struct copy */
|
|
.namelen = namelen,
|
|
};
|
|
|
|
/* Couldn't lock the inode, so save the pptr for later. */
|
|
trace_xchk_parent_defer(sc->ip, &xname, dp->i_ino);
|
|
|
|
error = xfblob_storename(pp->pptr_names, &save_pp.name_cookie,
|
|
&xname);
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_ATTR_FORK, 0,
|
|
&error))
|
|
goto out_rele;
|
|
|
|
error = xfarray_append(pp->pptr_entries, &save_pp);
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_ATTR_FORK, 0,
|
|
&error))
|
|
goto out_rele;
|
|
|
|
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;
|
|
}
|
|
|
|
/*
|
|
* Revalidate a parent pointer that we collected in the past but couldn't check
|
|
* because of lock contention. Returns 0 if the parent pointer is still valid,
|
|
* -ENOENT if it has gone away on us, or a negative errno.
|
|
*/
|
|
STATIC int
|
|
xchk_parent_revalidate_pptr(
|
|
struct xchk_pptrs *pp,
|
|
const struct xfs_name *xname,
|
|
struct xfs_parent_rec *pptr)
|
|
{
|
|
struct xfs_scrub *sc = pp->sc;
|
|
int error;
|
|
|
|
error = xfs_parent_lookup(sc->tp, sc->ip, xname, pptr, &pp->pptr_args);
|
|
if (error == -ENOATTR) {
|
|
/* Parent pointer went away, nothing to revalidate. */
|
|
return -ENOENT;
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* Check a parent pointer the slow way, which means we cycle locks a bunch
|
|
* and put up with revalidation until we get it done.
|
|
*/
|
|
STATIC int
|
|
xchk_parent_slow_pptr(
|
|
struct xchk_pptrs *pp,
|
|
const struct xfs_name *xname,
|
|
struct xfs_parent_rec *pptr)
|
|
{
|
|
struct xfs_scrub *sc = pp->sc;
|
|
struct xfs_inode *dp = NULL;
|
|
unsigned int lockmode;
|
|
int error;
|
|
|
|
/* Check that the deferred parent pointer still exists. */
|
|
if (pp->need_revalidate) {
|
|
error = xchk_parent_revalidate_pptr(pp, xname, pptr);
|
|
if (error == -ENOENT)
|
|
return 0;
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_ATTR_FORK, 0,
|
|
&error))
|
|
return error;
|
|
}
|
|
|
|
error = xchk_parent_iget(pp, pptr, &dp);
|
|
if (error)
|
|
return error;
|
|
if (!dp)
|
|
return 0;
|
|
|
|
/*
|
|
* If we can grab both IOLOCK and ILOCK of the alleged parent, we
|
|
* can proceed with the validation.
|
|
*/
|
|
lockmode = xchk_parent_lock_dir(sc, dp);
|
|
if (lockmode) {
|
|
trace_xchk_parent_slowpath(sc->ip, xname, dp->i_ino);
|
|
goto check_dirent;
|
|
}
|
|
|
|
/*
|
|
* We couldn't lock the parent dir. Drop all the locks and try to
|
|
* get them again, one at a time.
|
|
*/
|
|
xchk_iunlock(sc, sc->ilock_flags);
|
|
pp->need_revalidate = true;
|
|
|
|
trace_xchk_parent_ultraslowpath(sc->ip, xname, dp->i_ino);
|
|
|
|
error = xchk_dir_trylock_for_pptrs(sc, dp, &lockmode);
|
|
if (error)
|
|
goto out_rele;
|
|
|
|
/* Revalidate the parent pointer now that we cycled locks. */
|
|
error = xchk_parent_revalidate_pptr(pp, xname, pptr);
|
|
if (error == -ENOENT) {
|
|
error = 0;
|
|
goto out_unlock;
|
|
}
|
|
if (!xchk_fblock_xref_process_error(sc, XFS_ATTR_FORK, 0, &error))
|
|
goto out_unlock;
|
|
|
|
check_dirent:
|
|
error = xchk_parent_dirent(pp, xname, dp);
|
|
out_unlock:
|
|
xfs_iunlock(dp, lockmode);
|
|
out_rele:
|
|
xchk_irele(sc, dp);
|
|
return error;
|
|
}
|
|
|
|
/* Check all the parent pointers that we deferred the first time around. */
|
|
STATIC int
|
|
xchk_parent_finish_slow_pptrs(
|
|
struct xchk_pptrs *pp)
|
|
{
|
|
xfarray_idx_t array_cur;
|
|
int error;
|
|
|
|
foreach_xfarray_idx(pp->pptr_entries, array_cur) {
|
|
struct xchk_pptr pptr;
|
|
|
|
if (pp->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
|
|
return 0;
|
|
|
|
error = xfarray_load(pp->pptr_entries, array_cur, &pptr);
|
|
if (error)
|
|
return error;
|
|
|
|
error = xfblob_loadname(pp->pptr_names, pptr.name_cookie,
|
|
&pp->xname, pptr.namelen);
|
|
if (error)
|
|
return error;
|
|
|
|
error = xchk_parent_slow_pptr(pp, &pp->xname, &pptr.pptr_rec);
|
|
if (error)
|
|
return error;
|
|
}
|
|
|
|
/* Empty out both xfiles now that we've checked everything. */
|
|
xfarray_truncate(pp->pptr_entries);
|
|
xfblob_truncate(pp->pptr_names);
|
|
return 0;
|
|
}
|
|
|
|
/* Count the number of parent pointers. */
|
|
STATIC int
|
|
xchk_parent_count_pptr(
|
|
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;
|
|
int error;
|
|
|
|
if (!(attr_flags & XFS_ATTR_PARENT))
|
|
return 0;
|
|
|
|
error = xfs_parent_from_attr(sc->mp, attr_flags, name, namelen, value,
|
|
valuelen, NULL, NULL);
|
|
if (error)
|
|
return error;
|
|
|
|
pp->pptrs_found++;
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* 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;
|
|
int error;
|
|
|
|
/*
|
|
* If we cycled the ILOCK while cross-checking parent pointers with
|
|
* dirents, then we need to recalculate the number of parent pointers.
|
|
*/
|
|
if (pp->need_revalidate) {
|
|
pp->pptrs_found = 0;
|
|
error = xchk_xattr_walk(sc, sc->ip, xchk_parent_count_pptr,
|
|
NULL, pp);
|
|
if (error == -EFSCORRUPTED) {
|
|
/* Found a bad parent pointer */
|
|
xchk_fblock_set_corrupt(sc, XFS_ATTR_FORK, 0);
|
|
return 0;
|
|
}
|
|
if (error)
|
|
return error;
|
|
}
|
|
|
|
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;
|
|
char *descr;
|
|
int error;
|
|
|
|
pp = kvzalloc(sizeof(struct xchk_pptrs), XCHK_GFP_FLAGS);
|
|
if (!pp)
|
|
return -ENOMEM;
|
|
pp->sc = sc;
|
|
pp->xname.name = pp->namebuf;
|
|
|
|
/*
|
|
* Set up some staging memory for parent pointers that we can't check
|
|
* due to locking contention.
|
|
*/
|
|
descr = xchk_xfile_ino_descr(sc, "slow parent pointer entries");
|
|
error = xfarray_create(descr, 0, sizeof(struct xchk_pptr),
|
|
&pp->pptr_entries);
|
|
kfree(descr);
|
|
if (error)
|
|
goto out_pp;
|
|
|
|
descr = xchk_xfile_ino_descr(sc, "slow parent pointer names");
|
|
error = xfblob_create(descr, &pp->pptr_names);
|
|
kfree(descr);
|
|
if (error)
|
|
goto out_entries;
|
|
|
|
error = xchk_xattr_walk(sc, sc->ip, xchk_parent_scan_attr, NULL, pp);
|
|
if (error == -ECANCELED) {
|
|
error = 0;
|
|
goto out_names;
|
|
}
|
|
if (error)
|
|
goto out_names;
|
|
|
|
error = xchk_parent_finish_slow_pptrs(pp);
|
|
if (error == -ETIMEDOUT) {
|
|
/* Couldn't grab a lock, scrub was marked incomplete */
|
|
error = 0;
|
|
goto out_names;
|
|
}
|
|
if (error)
|
|
goto out_names;
|
|
|
|
if (pp->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
|
|
goto out_names;
|
|
|
|
/*
|
|
* 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_names;
|
|
}
|
|
|
|
if (pp->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
|
|
goto out_names;
|
|
|
|
/*
|
|
* 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_names;
|
|
|
|
out_names:
|
|
xfblob_destroy(pp->pptr_names);
|
|
out_entries:
|
|
xfarray_destroy(pp->pptr_entries);
|
|
out_pp:
|
|
kvfree(pp);
|
|
return error;
|
|
}
|
|
|
|
/* Scrub a parent pointer. */
|
|
int
|
|
xchk_parent(
|
|
struct xfs_scrub *sc)
|
|
{
|
|
struct xfs_mount *mp = sc->mp;
|
|
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.
|
|
*/
|
|
if (!S_ISDIR(VFS_I(sc->ip)->i_mode))
|
|
return -ENOENT;
|
|
|
|
/* We're not a special inode, are we? */
|
|
if (!xfs_verify_dir_ino(mp, sc->ip->i_ino)) {
|
|
xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
|
|
return 0;
|
|
}
|
|
|
|
do {
|
|
if (xchk_should_terminate(sc, &error))
|
|
break;
|
|
|
|
/* Look up '..' */
|
|
error = xchk_dir_lookup(sc, sc->ip, &xfs_name_dotdot,
|
|
&parent_ino);
|
|
if (!xchk_fblock_process_error(sc, XFS_DATA_FORK, 0, &error))
|
|
return error;
|
|
if (!xfs_verify_dir_ino(mp, parent_ino)) {
|
|
xchk_fblock_set_corrupt(sc, XFS_DATA_FORK, 0);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Check that the dotdot entry points to a parent directory
|
|
* containing a dirent pointing to this subdirectory.
|
|
*/
|
|
error = xchk_parent_validate(sc, parent_ino);
|
|
} while (error == -EAGAIN);
|
|
if (error == -EBUSY) {
|
|
/*
|
|
* We could not scan a directory, so we marked the check
|
|
* incomplete. No further error return is necessary.
|
|
*/
|
|
return 0;
|
|
}
|
|
|
|
return error;
|
|
}
|
|
|
|
/*
|
|
* Decide if this file's extended attributes (and therefore its parent
|
|
* pointers) have been zapped to satisfy the inode and ifork verifiers.
|
|
* Checking and repairing should be postponed until the extended attribute
|
|
* structure is fixed.
|
|
*/
|
|
bool
|
|
xchk_pptr_looks_zapped(
|
|
struct xfs_inode *ip)
|
|
{
|
|
struct xfs_mount *mp = ip->i_mount;
|
|
struct inode *inode = VFS_I(ip);
|
|
|
|
ASSERT(xfs_has_parent(mp));
|
|
|
|
/*
|
|
* Temporary files that cannot be linked into the directory tree do not
|
|
* have attr forks because they cannot ever have parents.
|
|
*/
|
|
if (inode->i_nlink == 0 && !(inode->i_state & I_LINKABLE))
|
|
return false;
|
|
|
|
/*
|
|
* Directory tree roots do not have parents, so the expected outcome
|
|
* of a parent pointer scan is always the empty set. It's safe to scan
|
|
* them even if the attr fork was zapped.
|
|
*/
|
|
if (ip == mp->m_rootip)
|
|
return false;
|
|
|
|
/*
|
|
* Metadata inodes are all rooted in the superblock and do not have
|
|
* any parents. Hence the attr fork will not be initialized, but
|
|
* there are no parent pointers that might have been zapped.
|
|
*/
|
|
if (xfs_is_metadata_inode(ip))
|
|
return false;
|
|
|
|
/*
|
|
* Linked and linkable non-rootdir files should always have an
|
|
* attribute fork because that is where parent pointers are
|
|
* stored. If the fork is absent, something is amiss.
|
|
*/
|
|
if (!xfs_inode_has_attr_fork(ip))
|
|
return true;
|
|
|
|
/* Repair zapped this file's attr fork a short time ago */
|
|
if (xfs_ifork_zapped(ip, XFS_ATTR_FORK))
|
|
return true;
|
|
|
|
/*
|
|
* If the dinode repair found a bad attr fork, it will reset the fork
|
|
* to extents format with zero records and wait for the bmapbta
|
|
* scrubber to reconstruct the block mappings. The extended attribute
|
|
* structure always contain some content when parent pointers are
|
|
* enabled, so this is a clear sign of a zapped attr fork.
|
|
*/
|
|
return ip->i_af.if_format == XFS_DINODE_FMT_EXTENTS &&
|
|
ip->i_af.if_nextents == 0;
|
|
}
|