// SPDX-License-Identifier: GPL-2.0-or-later /* * Copyright (c) 2020-2024 Oracle. All Rights Reserved. * Author: Darrick J. Wong */ #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_quota.h" #include "xfs_qm.h" #include "xfs_icache.h" #include "xfs_bmap_util.h" #include "xfs_ialloc.h" #include "xfs_ag.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/quota.h" #include "scrub/quotacheck.h" #include "scrub/trace.h" /* * Live Quotacheck * =============== * * Quota counters are "summary" metadata, in the sense that they are computed * as the summation of the block usage counts for every file on the filesystem. * Therefore, we compute the correct icount, bcount, and rtbcount values by * creating a shadow quota counter structure and walking every inode. */ /* Track the quota deltas for a dquot in a transaction. */ struct xqcheck_dqtrx { xfs_dqtype_t q_type; xfs_dqid_t q_id; int64_t icount_delta; int64_t bcount_delta; int64_t delbcnt_delta; int64_t rtbcount_delta; int64_t delrtb_delta; }; #define XQCHECK_MAX_NR_DQTRXS (XFS_QM_TRANS_DQTYPES * XFS_QM_TRANS_MAXDQS) /* * Track the quota deltas for all dquots attached to a transaction if the * quota deltas are being applied to an inode that we already scanned. */ struct xqcheck_dqacct { struct rhash_head hash; uintptr_t tx_id; struct xqcheck_dqtrx dqtrx[XQCHECK_MAX_NR_DQTRXS]; unsigned int refcount; }; /* Free a shadow dquot accounting structure. */ static void xqcheck_dqacct_free( void *ptr, void *arg) { struct xqcheck_dqacct *dqa = ptr; kfree(dqa); } /* Set us up to scrub quota counters. */ int xchk_setup_quotacheck( struct xfs_scrub *sc) { if (!XFS_IS_QUOTA_ON(sc->mp)) return -ENOENT; xchk_fsgates_enable(sc, XCHK_FSGATES_QUOTA); sc->buf = kzalloc(sizeof(struct xqcheck), XCHK_GFP_FLAGS); if (!sc->buf) return -ENOMEM; return xchk_setup_fs(sc); } /* * Part 1: Collecting dquot resource usage counts. For each xfs_dquot attached * to each inode, we create a shadow dquot, and compute the inode count and add * the data/rt block usage from what we see. * * To avoid false corruption reports in part 2, any failure in this part must * set the INCOMPLETE flag even when a negative errno is returned. This care * must be taken with certain errno values (i.e. EFSBADCRC, EFSCORRUPTED, * ECANCELED) that are absorbed into a scrub state flag update by * xchk_*_process_error. Scrub and repair share the same incore data * structures, so the INCOMPLETE flag is critical to prevent a repair based on * insufficient information. * * Because we are scanning a live filesystem, it's possible that another thread * will try to update the quota counters for an inode that we've already * scanned. This will cause our counts to be incorrect. Therefore, we hook * the live transaction code in two places: (1) when the callers update the * per-transaction dqtrx structure to log quota counter updates; and (2) when * transaction commit actually logs those updates to the incore dquot. By * shadowing transaction updates in this manner, live quotacheck can ensure * by locking the dquot and the shadow structure that its own copies are not * out of date. Because the hook code runs in a different process context from * the scrub code and the scrub state flags are not accessed atomically, * failures in the hook code must abort the iscan and the scrubber must notice * the aborted scan and set the incomplete flag. * * Note that we use srcu notifier hooks to minimize the overhead when live * quotacheck is /not/ running. */ /* Update an incore dquot counter information from a live update. */ static int xqcheck_update_incore_counts( struct xqcheck *xqc, struct xfarray *counts, xfs_dqid_t id, int64_t inodes, int64_t nblks, int64_t rtblks) { struct xqcheck_dquot xcdq; int error; error = xfarray_load_sparse(counts, id, &xcdq); if (error) return error; xcdq.flags |= XQCHECK_DQUOT_WRITTEN; xcdq.icount += inodes; xcdq.bcount += nblks; xcdq.rtbcount += rtblks; error = xfarray_store(counts, id, &xcdq); if (error == -EFBIG) { /* * EFBIG means we tried to store data at too high a byte offset * in the sparse array. IOWs, we cannot complete the check and * must notify userspace that the check was incomplete. */ error = -ECANCELED; } return error; } /* Decide if this is the shadow dquot accounting structure for a transaction. */ static int xqcheck_dqacct_obj_cmpfn( struct rhashtable_compare_arg *arg, const void *obj) { const uintptr_t *tx_idp = arg->key; const struct xqcheck_dqacct *dqa = obj; if (dqa->tx_id != *tx_idp) return 1; return 0; } static const struct rhashtable_params xqcheck_dqacct_hash_params = { .min_size = 32, .key_len = sizeof(uintptr_t), .key_offset = offsetof(struct xqcheck_dqacct, tx_id), .head_offset = offsetof(struct xqcheck_dqacct, hash), .automatic_shrinking = true, .obj_cmpfn = xqcheck_dqacct_obj_cmpfn, }; /* Find a shadow dqtrx slot for the given dquot. */ STATIC struct xqcheck_dqtrx * xqcheck_get_dqtrx( struct xqcheck_dqacct *dqa, xfs_dqtype_t q_type, xfs_dqid_t q_id) { int i; for (i = 0; i < XQCHECK_MAX_NR_DQTRXS; i++) { if (dqa->dqtrx[i].q_type == 0 || (dqa->dqtrx[i].q_type == q_type && dqa->dqtrx[i].q_id == q_id)) return &dqa->dqtrx[i]; } return NULL; } /* * Create and fill out a quota delta tracking structure to shadow the updates * going on in the regular quota code. */ static int xqcheck_mod_live_ino_dqtrx( struct notifier_block *nb, unsigned long action, void *data) { struct xfs_mod_ino_dqtrx_params *p = data; struct xqcheck *xqc; struct xqcheck_dqacct *dqa; struct xqcheck_dqtrx *dqtrx; int error; xqc = container_of(nb, struct xqcheck, qhook.mod_hook.nb); /* Skip quota reservation fields. */ switch (action) { case XFS_TRANS_DQ_BCOUNT: case XFS_TRANS_DQ_DELBCOUNT: case XFS_TRANS_DQ_ICOUNT: case XFS_TRANS_DQ_RTBCOUNT: case XFS_TRANS_DQ_DELRTBCOUNT: break; default: return NOTIFY_DONE; } /* Ignore dqtrx updates for quota types we don't care about. */ switch (p->q_type) { case XFS_DQTYPE_USER: if (!xqc->ucounts) return NOTIFY_DONE; break; case XFS_DQTYPE_GROUP: if (!xqc->gcounts) return NOTIFY_DONE; break; case XFS_DQTYPE_PROJ: if (!xqc->pcounts) return NOTIFY_DONE; break; default: return NOTIFY_DONE; } /* Skip inodes that haven't been scanned yet. */ if (!xchk_iscan_want_live_update(&xqc->iscan, p->ino)) return NOTIFY_DONE; /* Make a shadow quota accounting tracker for this transaction. */ mutex_lock(&xqc->lock); dqa = rhashtable_lookup_fast(&xqc->shadow_dquot_acct, &p->tx_id, xqcheck_dqacct_hash_params); if (!dqa) { dqa = kzalloc(sizeof(struct xqcheck_dqacct), XCHK_GFP_FLAGS); if (!dqa) goto out_abort; dqa->tx_id = p->tx_id; error = rhashtable_insert_fast(&xqc->shadow_dquot_acct, &dqa->hash, xqcheck_dqacct_hash_params); if (error) goto out_abort; } /* Find the shadow dqtrx (or an empty slot) here. */ dqtrx = xqcheck_get_dqtrx(dqa, p->q_type, p->q_id); if (!dqtrx) goto out_abort; if (dqtrx->q_type == 0) { dqtrx->q_type = p->q_type; dqtrx->q_id = p->q_id; dqa->refcount++; } /* Update counter */ switch (action) { case XFS_TRANS_DQ_BCOUNT: dqtrx->bcount_delta += p->delta; break; case XFS_TRANS_DQ_DELBCOUNT: dqtrx->delbcnt_delta += p->delta; break; case XFS_TRANS_DQ_ICOUNT: dqtrx->icount_delta += p->delta; break; case XFS_TRANS_DQ_RTBCOUNT: dqtrx->rtbcount_delta += p->delta; break; case XFS_TRANS_DQ_DELRTBCOUNT: dqtrx->delrtb_delta += p->delta; break; } mutex_unlock(&xqc->lock); return NOTIFY_DONE; out_abort: xchk_iscan_abort(&xqc->iscan); mutex_unlock(&xqc->lock); return NOTIFY_DONE; } /* * Apply the transaction quota deltas to our shadow quota accounting info when * the regular quota code are doing the same. */ static int xqcheck_apply_live_dqtrx( struct notifier_block *nb, unsigned long action, void *data) { struct xfs_apply_dqtrx_params *p = data; struct xqcheck *xqc; struct xqcheck_dqacct *dqa; struct xqcheck_dqtrx *dqtrx; struct xfarray *counts; int error; xqc = container_of(nb, struct xqcheck, qhook.apply_hook.nb); /* Map the dquot type to an incore counter object. */ switch (p->q_type) { case XFS_DQTYPE_USER: counts = xqc->ucounts; break; case XFS_DQTYPE_GROUP: counts = xqc->gcounts; break; case XFS_DQTYPE_PROJ: counts = xqc->pcounts; break; default: return NOTIFY_DONE; } if (xchk_iscan_aborted(&xqc->iscan) || counts == NULL) return NOTIFY_DONE; /* * Find the shadow dqtrx for this transaction and dquot, if any deltas * need to be applied here. If not, we're finished early. */ mutex_lock(&xqc->lock); dqa = rhashtable_lookup_fast(&xqc->shadow_dquot_acct, &p->tx_id, xqcheck_dqacct_hash_params); if (!dqa) goto out_unlock; dqtrx = xqcheck_get_dqtrx(dqa, p->q_type, p->q_id); if (!dqtrx || dqtrx->q_type == 0) goto out_unlock; /* Update our shadow dquot if we're committing. */ if (action == XFS_APPLY_DQTRX_COMMIT) { error = xqcheck_update_incore_counts(xqc, counts, p->q_id, dqtrx->icount_delta, dqtrx->bcount_delta + dqtrx->delbcnt_delta, dqtrx->rtbcount_delta + dqtrx->delrtb_delta); if (error) goto out_abort; } /* Free the shadow accounting structure if that was the last user. */ dqa->refcount--; if (dqa->refcount == 0) { error = rhashtable_remove_fast(&xqc->shadow_dquot_acct, &dqa->hash, xqcheck_dqacct_hash_params); if (error) goto out_abort; xqcheck_dqacct_free(dqa, NULL); } mutex_unlock(&xqc->lock); return NOTIFY_DONE; out_abort: xchk_iscan_abort(&xqc->iscan); out_unlock: mutex_unlock(&xqc->lock); return NOTIFY_DONE; } /* Record this inode's quota usage in our shadow quota counter data. */ STATIC int xqcheck_collect_inode( struct xqcheck *xqc, struct xfs_inode *ip) { struct xfs_trans *tp = xqc->sc->tp; xfs_filblks_t nblks, rtblks; uint ilock_flags = 0; xfs_dqid_t id; bool isreg = S_ISREG(VFS_I(ip)->i_mode); int error = 0; if (xfs_is_quota_inode(&tp->t_mountp->m_sb, ip->i_ino)) { /* * Quota files are never counted towards quota, so we do not * need to take the lock. */ xchk_iscan_mark_visited(&xqc->iscan, ip); return 0; } /* Figure out the data / rt device block counts. */ xfs_ilock(ip, XFS_IOLOCK_SHARED); if (isreg) xfs_ilock(ip, XFS_MMAPLOCK_SHARED); if (XFS_IS_REALTIME_INODE(ip)) { /* * Read in the data fork for rt files so that _count_blocks * can count the number of blocks allocated from the rt volume. * Inodes do not track that separately. */ ilock_flags = xfs_ilock_data_map_shared(ip); error = xfs_iread_extents(tp, ip, XFS_DATA_FORK); if (error) goto out_abort; } else { ilock_flags = XFS_ILOCK_SHARED; xfs_ilock(ip, XFS_ILOCK_SHARED); } xfs_inode_count_blocks(tp, ip, &nblks, &rtblks); if (xchk_iscan_aborted(&xqc->iscan)) { error = -ECANCELED; goto out_incomplete; } /* Update the shadow dquot counters. */ mutex_lock(&xqc->lock); if (xqc->ucounts) { id = xfs_qm_id_for_quotatype(ip, XFS_DQTYPE_USER); error = xqcheck_update_incore_counts(xqc, xqc->ucounts, id, 1, nblks, rtblks); if (error) goto out_mutex; } if (xqc->gcounts) { id = xfs_qm_id_for_quotatype(ip, XFS_DQTYPE_GROUP); error = xqcheck_update_incore_counts(xqc, xqc->gcounts, id, 1, nblks, rtblks); if (error) goto out_mutex; } if (xqc->pcounts) { id = xfs_qm_id_for_quotatype(ip, XFS_DQTYPE_PROJ); error = xqcheck_update_incore_counts(xqc, xqc->pcounts, id, 1, nblks, rtblks); if (error) goto out_mutex; } mutex_unlock(&xqc->lock); xchk_iscan_mark_visited(&xqc->iscan, ip); goto out_ilock; out_mutex: mutex_unlock(&xqc->lock); out_abort: xchk_iscan_abort(&xqc->iscan); out_incomplete: xchk_set_incomplete(xqc->sc); out_ilock: xfs_iunlock(ip, ilock_flags); if (isreg) xfs_iunlock(ip, XFS_MMAPLOCK_SHARED); xfs_iunlock(ip, XFS_IOLOCK_SHARED); return error; } /* Walk all the allocated inodes and run a quota scan on them. */ STATIC int xqcheck_collect_counts( struct xqcheck *xqc) { struct xfs_scrub *sc = xqc->sc; struct xfs_inode *ip; int error; /* * Set up for a potentially lengthy filesystem scan by reducing our * transaction resource usage for the duration. Specifically: * * Cancel the transaction to release the log grant space while we scan * the filesystem. * * Create a new empty transaction to eliminate the possibility of the * inode scan deadlocking on cyclical metadata. * * We pass the empty transaction to the file scanning function to avoid * repeatedly cycling empty transactions. This can be done without * risk of deadlock between sb_internal and the IOLOCK (we take the * IOLOCK to quiesce the file before scanning) because empty * transactions do not take sb_internal. */ xchk_trans_cancel(sc); error = xchk_trans_alloc_empty(sc); if (error) return error; while ((error = xchk_iscan_iter(&xqc->iscan, &ip)) == 1) { error = xqcheck_collect_inode(xqc, ip); xchk_irele(sc, ip); if (error) break; if (xchk_should_terminate(sc, &error)) break; } xchk_iscan_iter_finish(&xqc->iscan); if (error) { xchk_set_incomplete(sc); /* * If we couldn't grab an inode that was busy with a state * change, change the error code so that we exit to userspace * as quickly as possible. */ if (error == -EBUSY) return -ECANCELED; return error; } /* * Switch out for a real transaction in preparation for building a new * tree. */ xchk_trans_cancel(sc); return xchk_setup_fs(sc); } /* * Part 2: Comparing dquot resource counters. Walk each xfs_dquot, comparing * the resource usage counters against our shadow dquots; and then walk each * shadow dquot (that wasn't covered in the first part), comparing it against * the xfs_dquot. */ /* * Check the dquot data against what we observed. Caller must hold the dquot * lock. */ STATIC int xqcheck_compare_dquot( struct xqcheck *xqc, xfs_dqtype_t dqtype, struct xfs_dquot *dq) { struct xqcheck_dquot xcdq; struct xfarray *counts = xqcheck_counters_for(xqc, dqtype); int error; if (xchk_iscan_aborted(&xqc->iscan)) { xchk_set_incomplete(xqc->sc); return -ECANCELED; } mutex_lock(&xqc->lock); error = xfarray_load_sparse(counts, dq->q_id, &xcdq); if (error) goto out_unlock; if (xcdq.icount != dq->q_ino.count) xchk_qcheck_set_corrupt(xqc->sc, dqtype, dq->q_id); if (xcdq.bcount != dq->q_blk.count) xchk_qcheck_set_corrupt(xqc->sc, dqtype, dq->q_id); if (xcdq.rtbcount != dq->q_rtb.count) xchk_qcheck_set_corrupt(xqc->sc, dqtype, dq->q_id); xcdq.flags |= (XQCHECK_DQUOT_COMPARE_SCANNED | XQCHECK_DQUOT_WRITTEN); error = xfarray_store(counts, dq->q_id, &xcdq); if (error == -EFBIG) { /* * EFBIG means we tried to store data at too high a byte offset * in the sparse array. IOWs, we cannot complete the check and * must notify userspace that the check was incomplete. This * should never happen outside of the collection phase. */ xchk_set_incomplete(xqc->sc); error = -ECANCELED; } mutex_unlock(&xqc->lock); if (error) return error; if (xqc->sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT) return -ECANCELED; return 0; out_unlock: mutex_unlock(&xqc->lock); return error; } /* * Walk all the observed dquots, and make sure there's a matching incore * dquot and that its counts match ours. */ STATIC int xqcheck_walk_observations( struct xqcheck *xqc, xfs_dqtype_t dqtype) { struct xqcheck_dquot xcdq; struct xfs_dquot *dq; struct xfarray *counts = xqcheck_counters_for(xqc, dqtype); xfarray_idx_t cur = XFARRAY_CURSOR_INIT; int error; mutex_lock(&xqc->lock); while ((error = xfarray_iter(counts, &cur, &xcdq)) == 1) { xfs_dqid_t id = cur - 1; if (xcdq.flags & XQCHECK_DQUOT_COMPARE_SCANNED) continue; mutex_unlock(&xqc->lock); error = xfs_qm_dqget(xqc->sc->mp, id, dqtype, false, &dq); if (error == -ENOENT) { xchk_qcheck_set_corrupt(xqc->sc, dqtype, id); return 0; } if (error) return error; error = xqcheck_compare_dquot(xqc, dqtype, dq); xfs_qm_dqput(dq); if (error) return error; if (xchk_should_terminate(xqc->sc, &error)) return error; mutex_lock(&xqc->lock); } mutex_unlock(&xqc->lock); return error; } /* Compare the quota counters we observed against the live dquots. */ STATIC int xqcheck_compare_dqtype( struct xqcheck *xqc, xfs_dqtype_t dqtype) { struct xchk_dqiter cursor = { }; struct xfs_scrub *sc = xqc->sc; struct xfs_dquot *dq; int error; if (sc->sm->sm_flags & XFS_SCRUB_OFLAG_CORRUPT) return 0; /* If the quota CHKD flag is cleared, we need to repair this quota. */ if (!(xfs_quota_chkd_flag(dqtype) & sc->mp->m_qflags)) { xchk_qcheck_set_corrupt(xqc->sc, dqtype, 0); return 0; } /* Compare what we observed against the actual dquots. */ xchk_dqiter_init(&cursor, sc, dqtype); while ((error = xchk_dquot_iter(&cursor, &dq)) == 1) { error = xqcheck_compare_dquot(xqc, dqtype, dq); xfs_qm_dqput(dq); if (error) break; } if (error) return error; /* Walk all the observed dquots and compare to the incore ones. */ return xqcheck_walk_observations(xqc, dqtype); } /* Tear down everything associated with a quotacheck. */ static void xqcheck_teardown_scan( void *priv) { struct xqcheck *xqc = priv; struct xfs_quotainfo *qi = xqc->sc->mp->m_quotainfo; /* Discourage any hook functions that might be running. */ xchk_iscan_abort(&xqc->iscan); /* * As noted above, the apply hook is responsible for cleaning up the * shadow dquot accounting data when a transaction completes. The mod * hook must be removed before the apply hook so that we don't * mistakenly leave an active shadow account for the mod hook to get * its hands on. No hooks should be running after these functions * return. */ xfs_dqtrx_hook_del(qi, &xqc->qhook); if (xqc->shadow_dquot_acct.key_len) { rhashtable_free_and_destroy(&xqc->shadow_dquot_acct, xqcheck_dqacct_free, NULL); xqc->shadow_dquot_acct.key_len = 0; } if (xqc->pcounts) { xfarray_destroy(xqc->pcounts); xqc->pcounts = NULL; } if (xqc->gcounts) { xfarray_destroy(xqc->gcounts); xqc->gcounts = NULL; } if (xqc->ucounts) { xfarray_destroy(xqc->ucounts); xqc->ucounts = NULL; } xchk_iscan_teardown(&xqc->iscan); mutex_destroy(&xqc->lock); xqc->sc = NULL; } /* * Scan all inodes in the entire filesystem to generate quota counter data. * If the scan is successful, the quota data will be left alive for a repair. * If any error occurs, we'll tear everything down. */ STATIC int xqcheck_setup_scan( struct xfs_scrub *sc, struct xqcheck *xqc) { char *descr; struct xfs_quotainfo *qi = sc->mp->m_quotainfo; unsigned long long max_dquots = XFS_DQ_ID_MAX + 1ULL; int error; ASSERT(xqc->sc == NULL); xqc->sc = sc; mutex_init(&xqc->lock); /* Retry iget every tenth of a second for up to 30 seconds. */ xchk_iscan_start(sc, 30000, 100, &xqc->iscan); error = -ENOMEM; if (xfs_this_quota_on(sc->mp, XFS_DQTYPE_USER)) { descr = xchk_xfile_descr(sc, "user dquot records"); error = xfarray_create(descr, max_dquots, sizeof(struct xqcheck_dquot), &xqc->ucounts); kfree(descr); if (error) goto out_teardown; } if (xfs_this_quota_on(sc->mp, XFS_DQTYPE_GROUP)) { descr = xchk_xfile_descr(sc, "group dquot records"); error = xfarray_create(descr, max_dquots, sizeof(struct xqcheck_dquot), &xqc->gcounts); kfree(descr); if (error) goto out_teardown; } if (xfs_this_quota_on(sc->mp, XFS_DQTYPE_PROJ)) { descr = xchk_xfile_descr(sc, "project dquot records"); error = xfarray_create(descr, max_dquots, sizeof(struct xqcheck_dquot), &xqc->pcounts); kfree(descr); if (error) goto out_teardown; } /* * Set up hash table to map transactions to our internal shadow dqtrx * structures. */ error = rhashtable_init(&xqc->shadow_dquot_acct, &xqcheck_dqacct_hash_params); if (error) goto out_teardown; /* * Hook into the quota code. The hook only triggers for 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. * * The apply hook (which removes the shadow dquot accounting struct) * must be installed before the mod hook so that we never fail to catch * the end of a quota update sequence and leave stale shadow data. */ ASSERT(sc->flags & XCHK_FSGATES_QUOTA); xfs_dqtrx_hook_setup(&xqc->qhook, xqcheck_mod_live_ino_dqtrx, xqcheck_apply_live_dqtrx); error = xfs_dqtrx_hook_add(qi, &xqc->qhook); if (error) goto out_teardown; /* Use deferred cleanup to pass the quota count data to repair. */ sc->buf_cleanup = xqcheck_teardown_scan; return 0; out_teardown: xqcheck_teardown_scan(xqc); return error; } /* Scrub all counters for a given quota type. */ int xchk_quotacheck( struct xfs_scrub *sc) { struct xqcheck *xqc = sc->buf; int error = 0; /* Check quota counters on the live filesystem. */ error = xqcheck_setup_scan(sc, xqc); if (error) return error; /* Walk all inodes, picking up quota information. */ error = xqcheck_collect_counts(xqc); if (!xchk_xref_process_error(sc, 0, 0, &error)) return error; /* Fail fast if we're not playing with a full dataset. */ if (xchk_iscan_aborted(&xqc->iscan)) xchk_set_incomplete(sc); if (sc->sm->sm_flags & XFS_SCRUB_OFLAG_INCOMPLETE) return 0; /* Compare quota counters. */ if (xqc->ucounts) { error = xqcheck_compare_dqtype(xqc, XFS_DQTYPE_USER); if (!xchk_xref_process_error(sc, 0, 0, &error)) return error; } if (xqc->gcounts) { error = xqcheck_compare_dqtype(xqc, XFS_DQTYPE_GROUP); if (!xchk_xref_process_error(sc, 0, 0, &error)) return error; } if (xqc->pcounts) { error = xqcheck_compare_dqtype(xqc, XFS_DQTYPE_PROJ); if (!xchk_xref_process_error(sc, 0, 0, &error)) return error; } /* Check one last time for an incomplete dataset. */ if (xchk_iscan_aborted(&xqc->iscan)) xchk_set_incomplete(sc); return 0; }