linux/fs/xfs/scrub/findparent.c

// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Copyright (c) 2020-2024 Oracle.  All Rights Reserved.
 * Author: Darrick J. Wong <[email protected]>
 */
#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_defer.h"
#include "xfs_bit.h"
#include "xfs_log_format.h"
#include "xfs_trans.h"
#include "xfs_sb.h"
#include "xfs_inode.h"
#include "xfs_icache.h"
#include "xfs_da_format.h"
#include "xfs_da_btree.h"
#include "xfs_dir2.h"
#include "xfs_bmap_btree.h"
#include "xfs_dir2_priv.h"
#include "xfs_trans_space.h"
#include "xfs_health.h"
#include "xfs_exchmaps.h"
#include "xfs_parent.h"
#include "scrub/xfs_scrub.h"
#include "scrub/scrub.h"
#include "scrub/common.h"
#include "scrub/trace.h"
#include "scrub/repair.h"
#include "scrub/iscan.h"
#include "scrub/findparent.h"
#include "scrub/readdir.h"
#include "scrub/tempfile.h"
#include "scrub/listxattr.h"

/*
 * Finding the Parent of a Directory
 * =================================
 *
 * Directories have parent pointers, in the sense that each directory contains
 * a dotdot entry that points to the single allowed parent.  The brute force
 * way to find the parent of a given directory is to scan every directory in
 * the filesystem looking for a child dirent that references this directory.
 *
 * This module wraps the process of scanning the directory tree.  It requires
 * that @sc->ip is the directory whose parent we want to find, and that the
 * caller hold only the IOLOCK on that directory.  The scan itself needs to
 * take the ILOCK of each directory visited.
 *
 * Because we cannot hold @sc->ip's ILOCK during a scan of the whole fs, it is
 * necessary to use dirent hook to update the parent scan results.  Callers
 * must not read the scan results without re-taking @sc->ip's ILOCK.
 *
 * There are a few shortcuts that we can take to avoid scanning the entire
 * filesystem, such as noticing directory tree roots and querying the dentry
 * cache for parent information.
 */

struct xrep_findparent_info {
	/* The directory currently being scanned. */
	struct xfs_inode	*dp;

	/*
	 * Scrub context.  We're looking for a @dp containing a directory
	 * entry pointing to sc->ip->i_ino.
	 */
	struct xfs_scrub	*sc;

	/* Optional scan information for a xrep_findparent_scan call. */
	struct xrep_parent_scan_info *parent_scan;

	/*
	 * Parent that we've found for sc->ip.  If we're scanning the entire
	 * directory tree, we need this to ensure that we only find /one/
	 * parent directory.
	 */
	xfs_ino_t		found_parent;

	/*
	 * This is set to true if @found_parent was not observed directly from
	 * the directory scan but by noticing a change in dotdot entries after
	 * cycling the sc->ip IOLOCK.
	 */
	bool			parent_tentative;
};

/*
 * If this directory entry points to the scrub target inode, then the directory
 * we're scanning is the parent of the scrub target inode.
 */
STATIC int
xrep_findparent_dirent(
	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 xrep_findparent_info	*fpi = priv;
	int				error = 0;

	if (xchk_should_terminate(fpi->sc, &error))
		return error;

	if (ino != fpi->sc->ip->i_ino)
		return 0;

	/* Ignore garbage directory entry names. */
	if (name->len == 0 || !xfs_dir2_namecheck(name->name, name->len))
		return -EFSCORRUPTED;

	/*
	 * Ignore dotdot and dot entries -- we're looking for parent -> child
	 * links only.
	 */
	if (name->name[0] == '.' && (name->len == 1 ||
				     (name->len == 2 && name->name[1] == '.')))
		return 0;

	/* Uhoh, more than one parent for a dir? */
	if (fpi->found_parent != NULLFSINO &&
	    !(fpi->parent_tentative && fpi->found_parent == fpi->dp->i_ino)) {
		trace_xrep_findparent_dirent(fpi->sc->ip, 0);
		return -EFSCORRUPTED;
	}

	/* We found a potential parent; remember this. */
	trace_xrep_findparent_dirent(fpi->sc->ip, fpi->dp->i_ino);
	fpi->found_parent = fpi->dp->i_ino;
	fpi->parent_tentative = false;

	if (fpi->parent_scan)
		xrep_findparent_scan_found(fpi->parent_scan, fpi->dp->i_ino);

	return 0;
}

/*
 * If this is a directory, walk the dirents looking for any that point to the
 * scrub target inode.
 */
STATIC int
xrep_findparent_walk_directory(
	struct xrep_findparent_info	*fpi)
{
	struct xfs_scrub		*sc = fpi->sc;
	struct xfs_inode		*dp = fpi->dp;
	unsigned int			lock_mode;
	int				error = 0;

	/*
	 * The inode being scanned cannot be its own parent, nor can any
	 * temporary directory we created to stage this repair.
	 */
	if (dp == sc->ip || dp == sc->tempip)
		return 0;

	/*
	 * Similarly, temporary files created to stage a repair cannot be the
	 * parent of this inode.
	 */
	if (xrep_is_tempfile(dp))
		return 0;

	/*
	 * Scan the directory to see if there it contains an entry pointing to
	 * the directory that we are repairing.
	 */
	lock_mode = xfs_ilock_data_map_shared(dp);

	/*
	 * If this directory is known to be sick, we cannot scan it reliably
	 * and must abort.
	 */
	if (xfs_inode_has_sickness(dp, XFS_SICK_INO_CORE |
				       XFS_SICK_INO_BMBTD |
				       XFS_SICK_INO_DIR)) {
		error = -EFSCORRUPTED;
		goto out_unlock;
	}

	/*
	 * We cannot complete our parent pointer scan if a directory looks as
	 * though it has been zapped by the inode record repair code.
	 */
	if (xchk_dir_looks_zapped(dp)) {
		error = -EBUSY;
		goto out_unlock;
	}

	error = xchk_dir_walk(sc, dp, xrep_findparent_dirent, fpi);
	if (error)
		goto out_unlock;

out_unlock:
	xfs_iunlock(dp, lock_mode);
	return error;
}

/*
 * Update this directory's dotdot pointer based on ongoing dirent updates.
 */
STATIC int
xrep_findparent_live_update(
	struct notifier_block		*nb,
	unsigned long			action,
	void				*data)
{
	struct xfs_dir_update_params	*p = data;
	struct xrep_parent_scan_info	*pscan;
	struct xfs_scrub		*sc;

	pscan = container_of(nb, struct xrep_parent_scan_info,
			dhook.dirent_hook.nb);
	sc = pscan->sc;

	/*
	 * If @p->ip is the subdirectory that we're interested in and we've
	 * already scanned @p->dp, update the dotdot target inumber to the
	 * parent inode.
	 */
	if (p->ip->i_ino == sc->ip->i_ino &&
	    xchk_iscan_want_live_update(&pscan->iscan, p->dp->i_ino)) {
		if (p->delta > 0) {
			xrep_findparent_scan_found(pscan, p->dp->i_ino);
		} else {
			xrep_findparent_scan_found(pscan, NULLFSINO);
		}
	}

	return NOTIFY_DONE;
}

/*
 * Set up a scan to find the parent of a directory.  The provided dirent hook
 * will be called when there is a dotdot update for the inode being repaired.
 */
int
__xrep_findparent_scan_start(
	struct xfs_scrub		*sc,
	struct xrep_parent_scan_info	*pscan,
	notifier_fn_t			custom_fn)
{
	int				error;

	if (!(sc->flags & XCHK_FSGATES_DIRENTS)) {
		ASSERT(sc->flags & XCHK_FSGATES_DIRENTS);
		return -EINVAL;
	}

	pscan->sc = sc;
	pscan->parent_ino = NULLFSINO;

	mutex_init(&pscan->lock);

	xchk_iscan_start(sc, 30000, 100, &pscan->iscan);

	/*
	 * Hook into the dirent update code.  The hook only operates on inodes
	 * that were already scanned, and the scanner thread takes each inode's
	 * ILOCK, which means that any in-progress inode updates will finish
	 * before we can scan the inode.
	 */
	if (custom_fn)
		xfs_dir_hook_setup(&pscan->dhook, custom_fn);
	else
		xfs_dir_hook_setup(&pscan->dhook, xrep_findparent_live_update);
	error = xfs_dir_hook_add(sc->mp, &pscan->dhook);
	if (error)
		goto out_iscan;

	return 0;
out_iscan:
	xchk_iscan_teardown(&pscan->iscan);
	mutex_destroy(&pscan->lock);
	return error;
}

/*
 * Scan the entire filesystem looking for a parent inode for the inode being
 * scrubbed.  @sc->ip must not be the root of a directory tree.  Callers must
 * not hold a dirty transaction or any lock that would interfere with taking
 * an ILOCK.
 *
 * Returns 0 with @pscan->parent_ino set to the parent that we found.
 * Returns 0 with @pscan->parent_ino set to NULLFSINO if we found no parents.
 * Returns the usual negative errno if something else happened.
 */
int
xrep_findparent_scan(
	struct xrep_parent_scan_info	*pscan)
{
	struct xrep_findparent_info	fpi = {
		.sc			= pscan->sc,
		.found_parent		= NULLFSINO,
		.parent_scan		= pscan,
	};
	struct xfs_scrub		*sc = pscan->sc;
	int				ret;

	ASSERT(S_ISDIR(VFS_IC(sc->ip)->i_mode));

	while ((ret = xchk_iscan_iter(&pscan->iscan, &fpi.dp)) == 1) {
		if (S_ISDIR(VFS_I(fpi.dp)->i_mode))
			ret = xrep_findparent_walk_directory(&fpi);
		else
			ret = 0;
		xchk_iscan_mark_visited(&pscan->iscan, fpi.dp);
		xchk_irele(sc, fpi.dp);
		if (ret)
			break;

		if (xchk_should_terminate(sc, &ret))
			break;
	}
	xchk_iscan_iter_finish(&pscan->iscan);

	return ret;
}

/* Tear down a parent scan. */
void
xrep_findparent_scan_teardown(
	struct xrep_parent_scan_info	*pscan)
{
	xfs_dir_hook_del(pscan->sc->mp, &pscan->dhook);
	xchk_iscan_teardown(&pscan->iscan);
	mutex_destroy(&pscan->lock);
}

/* Finish a parent scan early. */
void
xrep_findparent_scan_finish_early(
	struct xrep_parent_scan_info	*pscan,
	xfs_ino_t			ino)
{
	xrep_findparent_scan_found(pscan, ino);
	xchk_iscan_finish_early(&pscan->iscan);
}

/*
 * Confirm that the directory @parent_ino actually contains a directory entry
 * pointing to the child @sc->ip->ino.  This function returns one of several
 * ways:
 *
 * Returns 0 with @parent_ino unchanged if the parent was confirmed.
 * Returns 0 with @parent_ino set to NULLFSINO if the parent was not valid.
 * Returns the usual negative errno if something else happened.
 */
int
xrep_findparent_confirm(
	struct xfs_scrub	*sc,
	xfs_ino_t		*parent_ino)
{
	struct xrep_findparent_info fpi = {
		.sc		= sc,
		.found_parent	= NULLFSINO,
	};
	int			error;

	/*
	 * The root directory always points to itself.  Unlinked dirs can point
	 * anywhere, so we point them at the root dir too.
	 */
	if (sc->ip == sc->mp->m_rootip || VFS_I(sc->ip)->i_nlink == 0) {
		*parent_ino = sc->mp->m_sb.sb_rootino;
		return 0;
	}

	/* Reject garbage parent inode numbers and self-referential parents. */
	if (*parent_ino == NULLFSINO)
	       return 0;
	if (!xfs_verify_dir_ino(sc->mp, *parent_ino) ||
	    *parent_ino == sc->ip->i_ino) {
		*parent_ino = NULLFSINO;
		return 0;
	}

	error = xchk_iget(sc, *parent_ino, &fpi.dp);
	if (error)
		return error;

	if (!S_ISDIR(VFS_I(fpi.dp)->i_mode)) {
		*parent_ino = NULLFSINO;
		goto out_rele;
	}

	error = xrep_findparent_walk_directory(&fpi);
	if (error)
		goto out_rele;

	*parent_ino = fpi.found_parent;
out_rele:
	xchk_irele(sc, fpi.dp);
	return error;
}

/*
 * If we're the root of a directory tree, we are our own parent.  If we're an
 * unlinked directory, the parent /won't/ have a link to us.  Set the parent
 * directory to the root for both cases.  Returns NULLFSINO if we don't know
 * what to do.
 */
xfs_ino_t
xrep_findparent_self_reference(
	struct xfs_scrub	*sc)
{
	if (sc->ip->i_ino == sc->mp->m_sb.sb_rootino)
		return sc->mp->m_sb.sb_rootino;

	if (VFS_I(sc->ip)->i_nlink == 0)
		return sc->mp->m_sb.sb_rootino;

	return NULLFSINO;
}

/* Check the dentry cache to see if knows of a parent for the scrub target. */
xfs_ino_t
xrep_findparent_from_dcache(
	struct xfs_scrub	*sc)
{
	struct inode		*pip = NULL;
	struct dentry		*dentry, *parent;
	xfs_ino_t		ret = NULLFSINO;

	dentry = d_find_alias(VFS_I(sc->ip));
	if (!dentry)
		goto out;

	parent = dget_parent(dentry);
	if (!parent)
		goto out_dput;

	ASSERT(parent->d_sb == sc->ip->i_mount->m_super);

	pip = igrab(d_inode(parent));
	dput(parent);

	if (S_ISDIR(pip->i_mode)) {
		trace_xrep_findparent_from_dcache(sc->ip, XFS_I(pip)->i_ino);
		ret = XFS_I(pip)->i_ino;
	}

	xchk_irele(sc, XFS_I(pip));

out_dput:
	dput(dentry);
out:
	return ret;
}