223 lines
5.3 KiB
C
223 lines
5.3 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_inode.h"
|
|
#include "xfs_icache.h"
|
|
#include "xfs_dir2.h"
|
|
#include "xfs_dir2_priv.h"
|
|
#include "scrub/scrub.h"
|
|
#include "scrub/common.h"
|
|
#include "scrub/readdir.h"
|
|
|
|
/* Set us up to scrub parents. */
|
|
int
|
|
xchk_setup_parent(
|
|
struct xfs_scrub *sc)
|
|
{
|
|
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 || !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) {
|
|
xfs_iunlock(sc->ip, XFS_ILOCK_EXCL);
|
|
xfs_ilock(sc->ip, XFS_ILOCK_EXCL);
|
|
error = -EAGAIN;
|
|
goto out_rele;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* 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 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);
|
|
|
|
return error;
|
|
}
|