#include "xfs_platform.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_iwalk.h"
#include "xfs_ialloc.h"
#include "xfs_dir2.h"
#include "xfs_dir2_priv.h"
#include "xfs_ag.h"
#include "xfs_parent.h"
#include "scrub/scrub.h"
#include "scrub/common.h"
#include "scrub/repair.h"
#include "scrub/xfile.h"
#include "scrub/xfarray.h"
#include "scrub/iscan.h"
#include "scrub/orphanage.h"
#include "scrub/nlinks.h"
#include "scrub/trace.h"
#include "scrub/readdir.h"
#include "scrub/tempfile.h"
#include "scrub/listxattr.h"
int
xchk_setup_nlinks(
struct xfs_scrub *sc)
{
struct xchk_nlink_ctrs *xnc;
int error;
xchk_fsgates_enable(sc, XCHK_FSGATES_DIRENTS);
if (xchk_could_repair(sc)) {
error = xrep_setup_nlinks(sc);
if (error)
return error;
}
xnc = kvzalloc_obj(struct xchk_nlink_ctrs, XCHK_GFP_FLAGS);
if (!xnc)
return -ENOMEM;
xnc->xname.name = xnc->namebuf;
xnc->sc = sc;
sc->buf = xnc;
return xchk_setup_fs(sc);
}
static inline void
careful_add(
xfs_nlink_t *nlinkp,
int delta)
{
uint64_t new_value = (uint64_t)(*nlinkp) + delta;
BUILD_BUG_ON(XFS_MAXLINK > U32_MAX);
*nlinkp = min_t(uint64_t, new_value, U32_MAX);
}
STATIC int
xchk_nlinks_update_incore(
struct xchk_nlink_ctrs *xnc,
xfs_ino_t ino,
int parents_delta,
int backrefs_delta,
int children_delta)
{
struct xchk_nlink nl;
int error;
if (!xnc->nlinks)
return 0;
error = xfarray_load_sparse(xnc->nlinks, ino, &nl);
if (error)
return error;
trace_xchk_nlinks_update_incore(xnc->sc->mp, ino, &nl, parents_delta,
backrefs_delta, children_delta);
careful_add(&nl.parents, parents_delta);
careful_add(&nl.backrefs, backrefs_delta);
careful_add(&nl.children, children_delta);
nl.flags |= XCHK_NLINK_WRITTEN;
error = xfarray_store(xnc->nlinks, ino, &nl);
if (error == -EFBIG) {
error = -ECANCELED;
}
return error;
}
STATIC int
xchk_nlinks_live_update(
struct notifier_block *nb,
unsigned long action,
void *data)
{
struct xfs_dir_update_params *p = data;
struct xchk_nlink_ctrs *xnc;
int error;
xnc = container_of(nb, struct xchk_nlink_ctrs, dhook.dirent_hook.nb);
if (xrep_is_tempfile(p->dp))
return NOTIFY_DONE;
trace_xchk_nlinks_live_update(xnc->sc->mp, p->dp, action, p->ip->i_ino,
p->delta, p->name->name, p->name->len);
if (xchk_iscan_want_live_update(&xnc->collect_iscan, p->dp->i_ino)) {
mutex_lock(&xnc->lock);
error = xchk_nlinks_update_incore(xnc, p->ip->i_ino, p->delta,
0, 0);
if (!error && S_ISDIR(VFS_IC(p->ip)->i_mode))
error = xchk_nlinks_update_incore(xnc, p->dp->i_ino, 0,
0, p->delta);
mutex_unlock(&xnc->lock);
if (error)
goto out_abort;
}
if (S_ISDIR(VFS_IC(p->ip)->i_mode) &&
xchk_iscan_want_live_update(&xnc->collect_iscan, p->ip->i_ino)) {
mutex_lock(&xnc->lock);
error = xchk_nlinks_update_incore(xnc, p->dp->i_ino, 0,
p->delta, 0);
mutex_unlock(&xnc->lock);
if (error)
goto out_abort;
}
return NOTIFY_DONE;
out_abort:
xchk_iscan_abort(&xnc->collect_iscan);
return NOTIFY_DONE;
}
STATIC int
xchk_nlinks_collect_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 xchk_nlink_ctrs *xnc = priv;
bool dot = false, dotdot = false;
int error;
if (name->len == 0 || !xfs_dir2_namecheck(name->name, name->len)) {
error = -ECANCELED;
goto out_abort;
}
if (name->len == 1 && name->name[0] == '.')
dot = true;
else if (name->len == 2 && name->name[0] == '.' &&
name->name[1] == '.')
dotdot = true;
if (dot && ino != dp->i_ino) {
error = -ECANCELED;
goto out_abort;
}
if (!xfs_verify_dir_ino(sc->mp, ino)) {
error = -ECANCELED;
goto out_abort;
}
if (xchk_iscan_aborted(&xnc->collect_iscan)) {
error = -ECANCELED;
goto out_incomplete;
}
trace_xchk_nlinks_collect_dirent(sc->mp, dp, ino, name);
mutex_lock(&xnc->lock);
if (dotdot) {
if (xchk_inode_is_dirtree_root(dp))
error = xchk_nlinks_update_incore(xnc, ino, 1, 0, 0);
else if (!xfs_has_parent(sc->mp))
error = xchk_nlinks_update_incore(xnc, ino, 0, 1, 0);
else
error = 0;
if (error)
goto out_unlock;
}
if (!dot && !dotdot) {
error = xchk_nlinks_update_incore(xnc, ino, 1, 0, 0);
if (error)
goto out_unlock;
}
if (!dot && !dotdot && name->type == XFS_DIR3_FT_DIR) {
error = xchk_nlinks_update_incore(xnc, dp->i_ino, 0, 0, 1);
if (error)
goto out_unlock;
}
mutex_unlock(&xnc->lock);
return 0;
out_unlock:
mutex_unlock(&xnc->lock);
out_abort:
xchk_iscan_abort(&xnc->collect_iscan);
out_incomplete:
xchk_set_incomplete(sc);
return error;
}
STATIC int
xchk_nlinks_collect_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 xfs_name xname = {
.name = name,
.len = namelen,
};
struct xchk_nlink_ctrs *xnc = priv;
const struct xfs_parent_rec *pptr_rec = value;
xfs_ino_t parent_ino;
int error;
if (xchk_iscan_aborted(&xnc->collect_iscan)) {
error = -ECANCELED;
goto out_incomplete;
}
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;
trace_xchk_nlinks_collect_pptr(sc->mp, ip, &xname, pptr_rec);
mutex_lock(&xnc->lock);
error = xchk_nlinks_update_incore(xnc, parent_ino, 0, 1, 0);
if (error)
goto out_unlock;
mutex_unlock(&xnc->lock);
return 0;
out_unlock:
mutex_unlock(&xnc->lock);
xchk_iscan_abort(&xnc->collect_iscan);
out_incomplete:
xchk_set_incomplete(sc);
return error;
}
static uint
xchk_nlinks_ilock_dir(
struct xfs_inode *ip)
{
uint lock_mode = XFS_ILOCK_SHARED;
if (xfs_need_iread_extents(&ip->i_df))
lock_mode = XFS_ILOCK_EXCL;
if (xfs_has_parent(ip->i_mount) && xfs_inode_has_attr_fork(ip) &&
xfs_need_iread_extents(&ip->i_af))
lock_mode = XFS_ILOCK_EXCL;
lock_mode |= XFS_IOLOCK_SHARED;
xfs_ilock(ip, lock_mode);
return lock_mode;
}
STATIC int
xchk_nlinks_collect_dir(
struct xchk_nlink_ctrs *xnc,
struct xfs_inode *dp)
{
struct xfs_scrub *sc = xnc->sc;
unsigned int lock_mode;
int error = 0;
if (xrep_is_tempfile(dp))
return 0;
lock_mode = xchk_nlinks_ilock_dir(dp);
if (VFS_I(dp)->i_nlink == 0)
goto out_unlock;
if (xchk_dir_looks_zapped(dp)) {
error = -EBUSY;
goto out_abort;
}
error = xchk_dir_walk(sc, dp, xchk_nlinks_collect_dirent, xnc);
if (error == -ECANCELED) {
error = 0;
goto out_unlock;
}
if (error)
goto out_abort;
if (xfs_has_parent(sc->mp)) {
if (xchk_pptr_looks_zapped(dp)) {
error = -EBUSY;
goto out_unlock;
}
error = xchk_xattr_walk(sc, dp, xchk_nlinks_collect_pptr, NULL,
xnc);
if (error == -ECANCELED) {
error = 0;
goto out_unlock;
}
if (error)
goto out_abort;
}
xchk_iscan_mark_visited(&xnc->collect_iscan, dp);
goto out_unlock;
out_abort:
xchk_set_incomplete(sc);
xchk_iscan_abort(&xnc->collect_iscan);
out_unlock:
xfs_iunlock(dp, lock_mode);
return error;
}
static inline int
xchk_nlinks_collect_metafile(
struct xchk_nlink_ctrs *xnc,
xfs_ino_t ino)
{
if (!xfs_verify_ino(xnc->sc->mp, ino))
return 0;
trace_xchk_nlinks_collect_metafile(xnc->sc->mp, ino);
return xchk_nlinks_update_incore(xnc, ino, 1, 0, 0);
}
STATIC int
xchk_nlinks_collect_metafiles(
struct xchk_nlink_ctrs *xnc)
{
struct xfs_mount *mp = xnc->sc->mp;
int error = -ECANCELED;
if (xchk_iscan_aborted(&xnc->collect_iscan))
goto out_incomplete;
mutex_lock(&xnc->lock);
error = xchk_nlinks_collect_metafile(xnc, mp->m_sb.sb_rbmino);
if (error)
goto out_abort;
error = xchk_nlinks_collect_metafile(xnc, mp->m_sb.sb_rsumino);
if (error)
goto out_abort;
error = xchk_nlinks_collect_metafile(xnc, mp->m_sb.sb_uquotino);
if (error)
goto out_abort;
error = xchk_nlinks_collect_metafile(xnc, mp->m_sb.sb_gquotino);
if (error)
goto out_abort;
error = xchk_nlinks_collect_metafile(xnc, mp->m_sb.sb_pquotino);
if (error)
goto out_abort;
mutex_unlock(&xnc->lock);
return 0;
out_abort:
mutex_unlock(&xnc->lock);
xchk_iscan_abort(&xnc->collect_iscan);
out_incomplete:
xchk_set_incomplete(xnc->sc);
return error;
}
static inline int
xchk_nlinks_collect_file(
struct xchk_nlink_ctrs *xnc,
struct xfs_inode *ip)
{
xfs_ilock(ip, XFS_IOLOCK_SHARED);
xchk_iscan_mark_visited(&xnc->collect_iscan, ip);
xfs_iunlock(ip, XFS_IOLOCK_SHARED);
return 0;
}
STATIC int
xchk_nlinks_collect(
struct xchk_nlink_ctrs *xnc)
{
struct xfs_scrub *sc = xnc->sc;
struct xfs_inode *ip;
int error;
error = xchk_nlinks_collect_metafiles(xnc);
if (error)
return error;
xchk_trans_cancel(sc);
xchk_trans_alloc_empty(sc);
while ((error = xchk_iscan_iter(&xnc->collect_iscan, &ip)) == 1) {
if (S_ISDIR(VFS_I(ip)->i_mode))
error = xchk_nlinks_collect_dir(xnc, ip);
else
error = xchk_nlinks_collect_file(xnc, ip);
xchk_irele(sc, ip);
if (error)
break;
if (xchk_should_terminate(sc, &error))
break;
}
xchk_iscan_iter_finish(&xnc->collect_iscan);
if (error) {
xchk_set_incomplete(sc);
if (error == -EBUSY)
return -ECANCELED;
return error;
}
xchk_trans_cancel(sc);
return xchk_setup_fs(sc);
}
STATIC int
xchk_nlinks_comparison_read(
struct xchk_nlink_ctrs *xnc,
xfs_ino_t ino,
struct xchk_nlink *obs)
{
struct xchk_nlink nl;
int error;
error = xfarray_load_sparse(xnc->nlinks, ino, &nl);
if (error)
return error;
nl.flags |= (XCHK_NLINK_COMPARE_SCANNED | XCHK_NLINK_WRITTEN);
error = xfarray_store(xnc->nlinks, ino, &nl);
if (error == -EFBIG) {
xchk_set_incomplete(xnc->sc);
return -ECANCELED;
}
if (error)
return error;
obs->parents = nl.parents;
obs->backrefs = nl.backrefs;
obs->children = nl.children;
obs->flags = 0;
return 0;
}
STATIC int
xchk_nlinks_compare_inode(
struct xchk_nlink_ctrs *xnc,
struct xfs_inode *ip)
{
struct xchk_nlink obs;
struct xfs_scrub *sc = xnc->sc;
uint64_t total_links;
unsigned int actual_nlink;
int error;
if (xrep_is_tempfile(ip))
return 0;
xfs_ilock(ip, XFS_ILOCK_SHARED);
mutex_lock(&xnc->lock);
if (xchk_iscan_aborted(&xnc->collect_iscan)) {
xchk_set_incomplete(xnc->sc);
error = -ECANCELED;
goto out_scanlock;
}
error = xchk_nlinks_comparison_read(xnc, ip->i_ino, &obs);
if (error)
goto out_scanlock;
if (!xfs_has_ftype(sc->mp) && S_ISDIR(VFS_I(ip)->i_mode))
obs.children = obs.backrefs;
total_links = xchk_nlink_total(ip, &obs);
actual_nlink = VFS_I(ip)->i_nlink;
trace_xchk_nlinks_compare_inode(sc->mp, ip, &obs);
if (total_links > XFS_NLINK_PINNED) {
xchk_ino_set_corrupt(sc, ip->i_ino);
goto out_corrupt;
} else if (total_links > XFS_MAXLINK) {
xchk_ino_set_warning(sc, ip->i_ino);
}
if (total_links != actual_nlink) {
xchk_ino_set_corrupt(sc, ip->i_ino);
goto out_corrupt;
}
if (S_ISDIR(VFS_I(ip)->i_mode) && actual_nlink > 0) {
if (obs.children != obs.backrefs)
xchk_ino_xref_set_corrupt(sc, ip->i_ino);
} else {
if (obs.backrefs != 0) {
xchk_ino_set_corrupt(sc, ip->i_ino);
goto out_corrupt;
}
if (obs.children != 0) {
xchk_ino_set_corrupt(sc, ip->i_ino);
goto out_corrupt;
}
}
if (xchk_inode_is_dirtree_root(ip)) {
if (obs.parents != 1) {
xchk_ino_set_corrupt(sc, ip->i_ino);
goto out_corrupt;
}
} else if (actual_nlink > 0) {
if (obs.parents == 0) {
xchk_ino_set_corrupt(sc, ip->i_ino);
goto out_corrupt;
}
}
out_corrupt:
if (sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
error = -ECANCELED;
out_scanlock:
mutex_unlock(&xnc->lock);
xfs_iunlock(ip, XFS_ILOCK_SHARED);
return error;
}
STATIC int
xchk_nlinks_compare_inum(
struct xchk_nlink_ctrs *xnc,
xfs_ino_t ino)
{
struct xchk_nlink obs;
struct xfs_mount *mp = xnc->sc->mp;
struct xfs_trans *tp = xnc->sc->tp;
struct xfs_buf *agi_bp;
struct xfs_inode *ip;
int error;
error = xchk_iget_agi(xnc->sc, ino, &agi_bp, &ip);
if (!error) {
error = xchk_nlinks_compare_inode(xnc, ip);
xchk_irele(xnc->sc, ip);
return error;
}
if (error == -ENOENT || error == -EINVAL) {
error = 0;
}
if (error)
goto out_agi;
if (agi_bp == NULL) {
ASSERT(agi_bp != NULL);
xchk_set_incomplete(xnc->sc);
return -ECANCELED;
}
if (xchk_iscan_aborted(&xnc->collect_iscan)) {
xchk_set_incomplete(xnc->sc);
error = -ECANCELED;
goto out_agi;
}
mutex_lock(&xnc->lock);
error = xchk_nlinks_comparison_read(xnc, ino, &obs);
if (error)
goto out_scanlock;
trace_xchk_nlinks_check_zero(mp, ino, &obs);
if (xchk_nlink_total(NULL, &obs) != 0) {
xchk_ino_set_corrupt(xnc->sc, ino);
error = -ECANCELED;
}
out_scanlock:
mutex_unlock(&xnc->lock);
out_agi:
if (agi_bp)
xfs_trans_brelse(tp, agi_bp);
return error;
}
static int
xchk_nlinks_compare_iter(
struct xchk_nlink_ctrs *xnc,
struct xfs_inode **ipp)
{
int error;
do {
error = xchk_iscan_iter(&xnc->compare_iscan, ipp);
} while (error == -EBUSY);
return error;
}
STATIC int
xchk_nlinks_compare(
struct xchk_nlink_ctrs *xnc)
{
struct xchk_nlink nl;
struct xfs_scrub *sc = xnc->sc;
struct xfs_inode *ip;
xfarray_idx_t cur = XFARRAY_CURSOR_INIT;
int error;
if (sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
return 0;
xchk_trans_cancel(sc);
xchk_trans_alloc_empty(sc);
xchk_iscan_start(sc, 0, 0, &xnc->compare_iscan);
while ((error = xchk_nlinks_compare_iter(xnc, &ip)) == 1) {
error = xchk_nlinks_compare_inode(xnc, ip);
xchk_iscan_mark_visited(&xnc->compare_iscan, ip);
xchk_irele(sc, ip);
if (error)
break;
if (xchk_should_terminate(sc, &error))
break;
}
xchk_iscan_iter_finish(&xnc->compare_iscan);
xchk_iscan_teardown(&xnc->compare_iscan);
if (error)
return error;
if (sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT)
return 0;
mutex_lock(&xnc->lock);
while ((error = xfarray_iter(xnc->nlinks, &cur, &nl)) == 1) {
xfs_ino_t ino = cur - 1;
if (nl.flags & XCHK_NLINK_COMPARE_SCANNED)
continue;
mutex_unlock(&xnc->lock);
error = xchk_nlinks_compare_inum(xnc, ino);
if (error)
return error;
if (xchk_should_terminate(xnc->sc, &error))
return error;
mutex_lock(&xnc->lock);
}
mutex_unlock(&xnc->lock);
return error;
}
static void
xchk_nlinks_teardown_scan(
void *priv)
{
struct xchk_nlink_ctrs *xnc = priv;
xchk_iscan_abort(&xnc->collect_iscan);
xfs_dir_hook_del(xnc->sc->mp, &xnc->dhook);
if (xnc->nlinks)
xfarray_destroy(xnc->nlinks);
xnc->nlinks = NULL;
xchk_iscan_teardown(&xnc->collect_iscan);
mutex_destroy(&xnc->lock);
xnc->sc = NULL;
}
STATIC int
xchk_nlinks_setup_scan(
struct xfs_scrub *sc,
struct xchk_nlink_ctrs *xnc)
{
struct xfs_mount *mp = sc->mp;
unsigned long long max_inos;
xfs_agnumber_t last_agno = mp->m_sb.sb_agcount - 1;
xfs_agino_t first_agino, last_agino;
int error;
mutex_init(&xnc->lock);
xchk_iscan_start(sc, 30000, 100, &xnc->collect_iscan);
xfs_agino_range(mp, last_agno, &first_agino, &last_agino);
max_inos = XFS_AGINO_TO_INO(mp, last_agno, last_agino) + 1;
error = xfarray_create("file link counts",
min(XFS_MAXINUMBER + 1, max_inos),
sizeof(struct xchk_nlink), &xnc->nlinks);
if (error)
goto out_teardown;
ASSERT(sc->flags & XCHK_FSGATES_DIRENTS);
xfs_dir_hook_setup(&xnc->dhook, xchk_nlinks_live_update);
error = xfs_dir_hook_add(mp, &xnc->dhook);
if (error)
goto out_teardown;
sc->buf_cleanup = xchk_nlinks_teardown_scan;
return 0;
out_teardown:
xchk_nlinks_teardown_scan(xnc);
return error;
}
int
xchk_nlinks(
struct xfs_scrub *sc)
{
struct xchk_nlink_ctrs *xnc = sc->buf;
int error = 0;
error = xchk_nlinks_setup_scan(sc, xnc);
if (error)
return error;
error = xchk_nlinks_collect(xnc);
if (!xchk_xref_process_error(sc, 0, 0, &error))
return error;
if (xchk_iscan_aborted(&xnc->collect_iscan))
xchk_set_incomplete(sc);
if (sc->sm->sm_flags & XFS_SCRUB_OFLAG_INCOMPLETE)
return 0;
error = xchk_nlinks_compare(xnc);
if (!xchk_xref_process_error(sc, 0, 0, &error))
return error;
if (xchk_iscan_aborted(&xnc->collect_iscan))
xchk_set_incomplete(sc);
return 0;
}