summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDarrick J. Wong <djwong@kernel.org>2022-07-14 11:06:20 -0700
committerDarrick J. Wong <djwong@kernel.org>2022-10-14 14:16:43 -0700
commit11a7ac8ab58347241fb9d1172d234328b766fa21 (patch)
tree32f6769187e5dd79fc123f9caa3e9a8eef81e13a
parent84a85984e84779c4c8f50298d7c7b2f65500f5ea (diff)
xfs: track file link count updates during live nlinks fsck
Create the necessary hooks in the file create/unlink/rename code so that our live nlink scrub code can stay up to date with the rest of the filesystem. This will be the means to keep our shadow link count information up to date while the scan runs in real time. Signed-off-by: Darrick J. Wong <djwong@kernel.org>
-rw-r--r--fs/xfs/scrub/common.c3
-rw-r--r--fs/xfs/scrub/nlinks.c120
-rw-r--r--fs/xfs/scrub/nlinks.h6
-rw-r--r--fs/xfs/scrub/scrub.c3
-rw-r--r--fs/xfs/scrub/scrub.h4
-rw-r--r--fs/xfs/scrub/trace.h43
-rw-r--r--fs/xfs/xfs_inode.c210
-rw-r--r--fs/xfs/xfs_inode.h35
-rw-r--r--fs/xfs/xfs_mount.h2
-rw-r--r--fs/xfs/xfs_super.c2
-rw-r--r--fs/xfs/xfs_symlink.c1
11 files changed, 426 insertions, 3 deletions
diff --git a/fs/xfs/scrub/common.c b/fs/xfs/scrub/common.c
index dd29d1474142..5e16517d6540 100644
--- a/fs/xfs/scrub/common.c
+++ b/fs/xfs/scrub/common.c
@@ -1273,5 +1273,8 @@ xchk_fshooks_enable(
if (scrub_fshooks & XCHK_FSHOOKS_QUOTA)
xfs_dqtrx_hook_enable();
+ if (scrub_fshooks & XCHK_FSHOOKS_NLINKS)
+ xfs_nlink_hook_enable();
+
sc->flags |= scrub_fshooks;
}
diff --git a/fs/xfs/scrub/nlinks.c b/fs/xfs/scrub/nlinks.c
index 0394db00d525..9b15687386d8 100644
--- a/fs/xfs/scrub/nlinks.c
+++ b/fs/xfs/scrub/nlinks.c
@@ -42,8 +42,7 @@ int
xchk_setup_nlinks(
struct xfs_scrub *sc)
{
- /* Not ready for general consumption yet. */
- return -EOPNOTSUPP;
+ xchk_fshooks_enable(sc, XCHK_FSHOOKS_NLINKS);
sc->buf = kzalloc(sizeof(struct xchk_nlink_ctrs), XCHK_GFP_FLAGS);
if (!sc->buf)
@@ -62,6 +61,21 @@ xchk_setup_nlinks(
* 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.
+ *
+ * Because we are scanning a live filesystem, it's possible that another thread
+ * will try to update the link counts for an inode that we've already scanned.
+ * This will cause our counts to be incorrect. Therefore, we hook all inode
+ * link count updates when the change is made to the incore inode. By
+ * shadowing transaction updates in this manner, live nlink check can ensure by
+ * locking the inode 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 jump labels and srcu notifier hooks to minimize the
+ * overhead when live nlinks is /not/ running. Locking order for nlink
+ * observations is inode ILOCK -> iscan_lock/xchk_nlink_ctrs lock.
*/
/* Update incore link count information. Caller must hold the nlinks lock. */
@@ -103,6 +117,91 @@ xchk_nlinks_update_incore(
return error;
}
+/*
+ * Apply a link count change from the regular filesystem into our shadow link
+ * count structure.
+ */
+STATIC int
+xchk_nlinks_live_update(
+ struct xfs_hook *delta_hook,
+ unsigned long action,
+ void *data)
+{
+ struct xfs_nlink_delta_params *p = data;
+ struct xchk_nlink_ctrs *xnc;
+ const struct xfs_inode *scan_dir = p->dp;
+ int error;
+
+ xnc = container_of(delta_hook, struct xchk_nlink_ctrs, hooks.delta_hook);
+
+ /*
+ * Back links between a parent directory and a child subdirectory are
+ * accounted to the incore data when the child is scanned, so we only
+ * want live backref updates if the child has been scanned. For all
+ * other links (forward and dot) we accept the live update for the
+ * parent directory.
+ */
+ if (action == XFS_BACKREF_NLINK_DELTA)
+ scan_dir = p->ip;
+
+ /* Ignore the live update if the directory hasn't been scanned yet. */
+ if (!xchk_iscan_want_live_update(&xnc->collect_iscan, scan_dir->i_ino))
+ 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);
+
+ mutex_lock(&xnc->lock);
+
+ if (action == XFS_DIRENT_NLINK_DELTA) {
+ const struct inode *inode = &p->ip->i_vnode;
+
+ /*
+ * This is an update of a forward link from dp to ino.
+ * Increment the number of parents linking into ino. If the
+ * forward link is to a subdirectory, increment the number of
+ * child links of dp.
+ */
+ error = xchk_nlinks_update_incore(xnc, p->ip->i_ino, p->delta,
+ 0, 0);
+ if (error)
+ goto out_abort;
+
+ if (S_ISDIR(inode->i_mode)) {
+ error = xchk_nlinks_update_incore(xnc, p->dp->i_ino, 0,
+ 0, p->delta);
+ if (error)
+ goto out_abort;
+ }
+ } else if (action == XFS_SELF_NLINK_DELTA) {
+ /*
+ * This is an update to the dot entry. Increment the number of
+ * child links of dp.
+ */
+ error = xchk_nlinks_update_incore(xnc, p->dp->i_ino, 0, 0,
+ p->delta);
+ if (error)
+ goto out_abort;
+ } else if (action == XFS_BACKREF_NLINK_DELTA) {
+ /*
+ * This is an update to the dotdot entry. Increment the number
+ * of backrefs pointing back to dp (from ip).
+ */
+ error = xchk_nlinks_update_incore(xnc, p->dp->i_ino, 0,
+ p->delta, 0);
+ if (error)
+ goto out_abort;
+ }
+
+ mutex_unlock(&xnc->lock);
+ return NOTIFY_DONE;
+
+out_abort:
+ xchk_iscan_abort(&xnc->collect_iscan);
+ mutex_unlock(&xnc->lock);
+ return NOTIFY_DONE;
+}
+
/* Bump the observed link count for the inode referenced by this entry. */
STATIC int
xchk_nlinks_collect_dirent(
@@ -710,6 +809,11 @@ xchk_nlinks_teardown_scan(
{
struct xchk_nlink_ctrs *xnc = priv;
+ /* Discourage any hook functions that might be running. */
+ xchk_iscan_abort(&xnc->collect_iscan);
+
+ xfs_nlink_hook_del(xnc->sc->mp, &xnc->hooks);
+
xfarray_destroy(xnc->nlinks);
xnc->nlinks = NULL;
@@ -755,6 +859,18 @@ xchk_nlinks_setup_scan(
if (error)
goto out_teardown;
+ /*
+ * Hook into the bumplink/droplink 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.
+ */
+ ASSERT(sc->flags & XCHK_FSHOOKS_NLINKS);
+ xfs_hook_setup(&xnc->hooks.delta_hook, xchk_nlinks_live_update);
+ error = xfs_nlink_hook_add(mp, &xnc->hooks);
+ if (error)
+ goto out_teardown;
+
/* Use deferred cleanup to pass the inode link count data to repair. */
sc->buf_cleanup = xchk_nlinks_teardown_scan;
return 0;
diff --git a/fs/xfs/scrub/nlinks.h b/fs/xfs/scrub/nlinks.h
index 30fa7dd93029..69cf556b15a3 100644
--- a/fs/xfs/scrub/nlinks.h
+++ b/fs/xfs/scrub/nlinks.h
@@ -22,6 +22,12 @@ struct xchk_nlink_ctrs {
*/
struct xchk_iscan collect_iscan;
struct xchk_iscan compare_iscan;
+
+ /*
+ * Hook into bumplink/droplink so that we can receive live updates
+ * from other writer threads.
+ */
+ struct xfs_nlink_hook hooks;
};
/*
diff --git a/fs/xfs/scrub/scrub.c b/fs/xfs/scrub/scrub.c
index 8fdd38dbb9f4..7e06aa98ca82 100644
--- a/fs/xfs/scrub/scrub.c
+++ b/fs/xfs/scrub/scrub.c
@@ -161,6 +161,9 @@ xchk_fshooks_disable(
if (sc->flags & XCHK_FSHOOKS_QUOTA)
xfs_dqtrx_hook_disable();
+ if (sc->flags & XCHK_FSHOOKS_NLINKS)
+ xfs_nlink_hook_disable();
+
sc->flags &= ~XCHK_FSHOOKS_ALL;
}
diff --git a/fs/xfs/scrub/scrub.h b/fs/xfs/scrub/scrub.h
index d39b2b95352a..da9da6245475 100644
--- a/fs/xfs/scrub/scrub.h
+++ b/fs/xfs/scrub/scrub.h
@@ -122,11 +122,13 @@ struct xfs_scrub {
#define XCHK_FSHOOKS_DRAIN (1 << 2) /* defer ops draining enabled */
#define XCHK_NEED_DRAIN (1 << 3) /* scrub needs to use intent drain */
#define XCHK_FSHOOKS_QUOTA (1 << 4) /* quota live update enabled */
+#define XCHK_FSHOOKS_NLINKS (1 << 5) /* link count live update enabled */
#define XREP_RESET_PERAG_RESV (1 << 30) /* must reset AG space reservation */
#define XREP_ALREADY_FIXED (1 << 31) /* checking our repair work */
#define XCHK_FSHOOKS_ALL (XCHK_FSHOOKS_DRAIN | \
- XCHK_FSHOOKS_QUOTA)
+ XCHK_FSHOOKS_QUOTA | \
+ XCHK_FSHOOKS_NLINKS)
/* Metadata scrubbers */
int xchk_tester(struct xfs_scrub *sc);
diff --git a/fs/xfs/scrub/trace.h b/fs/xfs/scrub/trace.h
index 433baa72472a..2457c2de930c 100644
--- a/fs/xfs/scrub/trace.h
+++ b/fs/xfs/scrub/trace.h
@@ -112,6 +112,7 @@ TRACE_DEFINE_ENUM(XFS_SCRUB_TYPE_NLINKS);
{ XCHK_FSHOOKS_DRAIN, "fshooks_drain" }, \
{ XCHK_NEED_DRAIN, "need_drain" }, \
{ XCHK_FSHOOKS_QUOTA, "fshooks_quota" }, \
+ { XCHK_FSHOOKS_NLINKS, "fshooks_nlinks" }, \
{ XREP_RESET_PERAG_RESV, "reset_perag_resv" }, \
{ XREP_ALREADY_FIXED, "already_fixed" }
@@ -1140,6 +1141,48 @@ TRACE_EVENT(xchk_nlinks_collect_metafile,
__entry->ino)
);
+TRACE_DEFINE_ENUM(XFS_DIRENT_NLINK_DELTA);
+TRACE_DEFINE_ENUM(XFS_BACKREF_NLINK_DELTA);
+TRACE_DEFINE_ENUM(XFS_SELF_NLINK_DELTA);
+
+#define XFS_NLINK_DELTA_STRINGS \
+ { XFS_DIRENT_NLINK_DELTA, "->" }, \
+ { XFS_BACKREF_NLINK_DELTA, "<-" }, \
+ { XFS_SELF_NLINK_DELTA, "<>" }
+
+TRACE_EVENT(xchk_nlinks_live_update,
+ TP_PROTO(struct xfs_mount *mp, const struct xfs_inode *dp,
+ int action, xfs_ino_t ino, int delta,
+ const char *name, unsigned int namelen),
+ TP_ARGS(mp, dp, action, ino, delta, name, namelen),
+ TP_STRUCT__entry(
+ __field(dev_t, dev)
+ __field(xfs_ino_t, dir)
+ __field(int, action)
+ __field(xfs_ino_t, ino)
+ __field(int, delta)
+ __field(unsigned int, namelen)
+ __dynamic_array(char, name, namelen)
+ ),
+ TP_fast_assign(
+ __entry->dev = mp->m_super->s_dev;
+ __entry->dir = dp ? dp->i_ino : NULLFSINO;
+ __entry->action = action;
+ __entry->ino = ino;
+ __entry->delta = delta;
+ __entry->namelen = namelen;
+ memcpy(__get_str(name), name, namelen);
+ ),
+ TP_printk("dev %d:%d dir 0x%llx %s ino 0x%llx nlink_delta %d name '%.*s'",
+ MAJOR(__entry->dev), MINOR(__entry->dev),
+ __entry->dir,
+ __print_symbolic(__entry->action, XFS_NLINK_DELTA_STRINGS),
+ __entry->ino,
+ __entry->delta,
+ __entry->namelen,
+ __get_str(name))
+);
+
TRACE_EVENT(xchk_nlinks_check_zero,
TP_PROTO(struct xfs_mount *mp, xfs_ino_t ino,
const struct xchk_nlink *live),
diff --git a/fs/xfs/xfs_inode.c b/fs/xfs/xfs_inode.c
index 675bba583d28..859660c02a4f 100644
--- a/fs/xfs/xfs_inode.c
+++ b/fs/xfs/xfs_inode.c
@@ -939,6 +939,117 @@ xfs_bumplink(
xfs_trans_log_inode(tp, ip, XFS_ILOG_CORE);
}
+#ifdef CONFIG_XFS_LIVE_HOOKS
+/*
+ * Use a static key here to reduce the overhead of link count live updates. If
+ * the compiler supports jump labels, the static branch will be replaced by a
+ * nop sled when there are no hook users. Online fsck is currently the only
+ * caller, so this is a reasonable tradeoff.
+ *
+ * Note: Patching the kernel code requires taking the cpu hotplug lock. Other
+ * parts of the kernel allocate memory with that lock held, which means that
+ * XFS callers cannot hold any locks that might be used by memory reclaim or
+ * writeback when calling the static_branch_{inc,dec} functions.
+ */
+DEFINE_STATIC_XFS_HOOK_SWITCH(xfs_nlinks_hooks_switch);
+
+void
+xfs_nlink_hook_disable(void)
+{
+ xfs_hooks_switch_off(&xfs_nlinks_hooks_switch);
+}
+
+void
+xfs_nlink_hook_enable(void)
+{
+ xfs_hooks_switch_on(&xfs_nlinks_hooks_switch);
+}
+
+/* Call hooks for a link count update relating to a dot dirent update. */
+static inline void
+xfs_nlink_self_delta(
+ struct xfs_inode *dp,
+ int delta)
+{
+ if (xfs_hooks_switched_on(&xfs_nlinks_hooks_switch)) {
+ struct xfs_nlink_delta_params p = {
+ .dp = dp,
+ .ip = dp,
+ .delta = delta,
+ .name = &xfs_name_dot,
+ };
+ struct xfs_mount *mp = dp->i_mount;
+
+ xfs_hooks_call(&mp->m_nlink_delta_hooks, XFS_SELF_NLINK_DELTA,
+ &p);
+ }
+}
+
+/* Call hooks for a link count update relating to a dotdot dirent update. */
+static inline void
+xfs_nlink_backref_delta(
+ struct xfs_inode *dp,
+ struct xfs_inode *ip,
+ int delta)
+{
+ if (xfs_hooks_switched_on(&xfs_nlinks_hooks_switch)) {
+ struct xfs_nlink_delta_params p = {
+ .dp = dp,
+ .ip = ip,
+ .delta = delta,
+ .name = &xfs_name_dotdot,
+ };
+ struct xfs_mount *mp = ip->i_mount;
+
+ xfs_hooks_call(&mp->m_nlink_delta_hooks, XFS_BACKREF_NLINK_DELTA,
+ &p);
+ }
+}
+
+/* Call hooks for a link count update relating to a dirent update. */
+void
+xfs_nlink_dirent_delta(
+ struct xfs_inode *dp,
+ struct xfs_inode *ip,
+ int delta,
+ struct xfs_name *name)
+{
+ if (xfs_hooks_switched_on(&xfs_nlinks_hooks_switch)) {
+ struct xfs_nlink_delta_params p = {
+ .dp = dp,
+ .ip = ip,
+ .delta = delta,
+ .name = name,
+ };
+ struct xfs_mount *mp = ip->i_mount;
+
+ xfs_hooks_call(&mp->m_nlink_delta_hooks, XFS_DIRENT_NLINK_DELTA,
+ &p);
+ }
+}
+
+/* Call the specified function during a link count update. */
+int
+xfs_nlink_hook_add(
+ struct xfs_mount *mp,
+ struct xfs_nlink_hook *hook)
+{
+ return xfs_hooks_add(&mp->m_nlink_delta_hooks, &hook->delta_hook);
+}
+
+/* Stop calling the specified function during a link count update. */
+void
+xfs_nlink_hook_del(
+ struct xfs_mount *mp,
+ struct xfs_nlink_hook *hook)
+{
+ xfs_hooks_del(&mp->m_nlink_delta_hooks, &hook->delta_hook);
+}
+#else
+# define xfs_nlink_self_delta(dp, delta) ((void)0)
+# define xfs_nlink_backref_delta(dp, ip, delta) ((void)0)
+#endif /* CONFIG_XFS_LIVE_HOOKS */
+
int
xfs_create(
struct user_namespace *mnt_userns,
@@ -1048,6 +1159,16 @@ xfs_create(
}
/*
+ * Create ip with a reference from dp, and add '.' and '..' references
+ * if it's a directory.
+ */
+ xfs_nlink_dirent_delta(dp, ip, 1, name);
+ if (is_dir) {
+ xfs_nlink_self_delta(ip, 1);
+ xfs_nlink_backref_delta(dp, ip, 1);
+ }
+
+ /*
* If this is a synchronous mount, make sure that the
* create transaction goes to disk before returning to
* the user.
@@ -1259,6 +1380,7 @@ xfs_link(
xfs_trans_log_inode(tp, tdp, XFS_ILOG_CORE);
xfs_bumplink(tp, sip);
+ xfs_nlink_dirent_delta(tdp, sip, 1, target_name);
/*
* If this is a synchronous mount, make sure that the
@@ -2488,6 +2610,16 @@ xfs_remove(
}
/*
+ * Drop the link from dp to ip, and if ip was a directory, remove the
+ * '.' and '..' references since we freed the directory.
+ */
+ xfs_nlink_dirent_delta(dp, ip, -1, name);
+ if (S_ISDIR(VFS_I(ip)->i_mode)) {
+ xfs_nlink_backref_delta(dp, ip, -1);
+ xfs_nlink_self_delta(ip, -1);
+ }
+
+ /*
* If this is a synchronous mount, make sure that the
* remove transaction goes to disk before returning to
* the user.
@@ -2561,6 +2693,75 @@ xfs_sort_for_rename(
}
}
+#ifdef CONFIG_XFS_LIVE_HOOKS
+static inline void
+xfs_rename_call_nlink_hooks(
+ struct xfs_inode *src_dp,
+ struct xfs_name *src_name,
+ struct xfs_inode *src_ip,
+ struct xfs_inode *target_dp,
+ struct xfs_name *target_name,
+ struct xfs_inode *target_ip,
+ struct xfs_inode *wip,
+ unsigned int flags)
+{
+ /* If we added a whiteout, add the reference from src_dp. */
+ if (wip)
+ xfs_nlink_dirent_delta(src_dp, wip, 1, src_name);
+
+ /* Move the src_ip forward link from src_dp to target_dp. */
+ xfs_nlink_dirent_delta(src_dp, src_ip, -1, src_name);
+ xfs_nlink_dirent_delta(target_dp, src_ip, 1, target_name);
+
+ /*
+ * If src_ip is a dir, move its '..' back link from src_dp to
+ * target_dp.
+ */
+ if (S_ISDIR(VFS_I(src_ip)->i_mode)) {
+ xfs_nlink_backref_delta(src_dp, src_ip, -1);
+ xfs_nlink_backref_delta(target_dp, src_ip, 1);
+ }
+
+ if (!target_ip)
+ return;
+
+ if (flags & RENAME_EXCHANGE) {
+ /* Move the target_ip forward link from target_dp to src_dp. */
+ xfs_nlink_dirent_delta(target_dp, target_ip, -1, target_name);
+ xfs_nlink_dirent_delta(src_dp, target_ip, 1, target_name);
+
+ /*
+ * If target_ip is a dir, move its '..' back link from
+ * target_dp to src_dp.
+ */
+ if (S_ISDIR(VFS_I(target_ip)->i_mode)) {
+ xfs_nlink_backref_delta(target_dp, target_ip, -1);
+ xfs_nlink_backref_delta(src_dp, target_ip, 1);
+ }
+
+ return;
+ }
+
+ /* Drop target_ip's forward link from target_dp. */
+ xfs_nlink_dirent_delta(target_dp, target_ip, -1, target_name);
+
+ if (!S_ISDIR(VFS_I(target_ip)->i_mode))
+ return;
+
+ /*
+ * If target_ip was a dir, drop the '.' and '..' references since that
+ * was the last reference.
+ */
+ ASSERT(VFS_I(target_ip)->i_nlink == 0);
+ xfs_nlink_self_delta(target_ip, -1);
+ xfs_nlink_backref_delta(target_dp, target_ip, -1);
+}
+#else
+# define xfs_rename_call_nlink_hooks(src_dp, src_name, src_ip, target_dp, \
+ target_name, target_ip, wip, flags) \
+ ((void)0)
+#endif /* CONFIG_XFS_LIVE_HOOKS */
+
static int
xfs_finish_rename(
struct xfs_trans *tp)
@@ -2677,6 +2878,11 @@ xfs_cross_rename(
}
xfs_trans_ichgtime(tp, dp1, XFS_ICHGTIME_MOD | XFS_ICHGTIME_CHG);
xfs_trans_log_inode(tp, dp1, XFS_ILOG_CORE);
+
+ if (xfs_hooks_switched_on(&xfs_nlinks_hooks_switch))
+ xfs_rename_call_nlink_hooks(dp1, name1, ip1, dp2, name2, ip2,
+ NULL, RENAME_EXCHANGE);
+
return xfs_finish_rename(tp);
out_trans_abort:
@@ -3060,6 +3266,10 @@ retry:
if (new_parent)
xfs_trans_log_inode(tp, target_dp, XFS_ILOG_CORE);
+ if (xfs_hooks_switched_on(&xfs_nlinks_hooks_switch))
+ xfs_rename_call_nlink_hooks(src_dp, src_name, src_ip,
+ target_dp, target_name, target_ip, wip, flags);
+
error = xfs_finish_rename(tp);
if (wip)
xfs_irele(wip);
diff --git a/fs/xfs/xfs_inode.h b/fs/xfs/xfs_inode.h
index 57c459f8e669..926e4dd566d0 100644
--- a/fs/xfs/xfs_inode.h
+++ b/fs/xfs/xfs_inode.h
@@ -578,4 +578,39 @@ void xfs_iunlock2_io_mmap(struct xfs_inode *ip1, struct xfs_inode *ip2);
void xfs_inode_count_blocks(struct xfs_trans *tp, struct xfs_inode *ip,
xfs_filblks_t *dblocks, xfs_filblks_t *rblocks);
+/*
+ * Parameters for tracking bumplink and droplink operations. The hook
+ * function arg parameter is one of these.
+ */
+enum xfs_nlink_delta_type {
+ XFS_DIRENT_NLINK_DELTA, /* parent pointing to child */
+ XFS_BACKREF_NLINK_DELTA, /* dotdot entries */
+ XFS_SELF_NLINK_DELTA, /* dot entries */
+};
+
+struct xfs_nlink_delta_params {
+ const struct xfs_inode *dp;
+ const struct xfs_inode *ip;
+ const struct xfs_name *name;
+ int delta;
+};
+
+#ifdef CONFIG_XFS_LIVE_HOOKS
+void xfs_nlink_dirent_delta(struct xfs_inode *dp, struct xfs_inode *ip,
+ int delta, struct xfs_name *name);
+
+struct xfs_nlink_hook {
+ struct xfs_hook delta_hook;
+};
+
+void xfs_nlink_hook_disable(void);
+void xfs_nlink_hook_enable(void);
+
+int xfs_nlink_hook_add(struct xfs_mount *mp, struct xfs_nlink_hook *hook);
+void xfs_nlink_hook_del(struct xfs_mount *mp, struct xfs_nlink_hook *hook);
+
+#else
+# define xfs_nlink_dirent_delta(dp, ip, delta, name) ((void)0)
+#endif /* CONFIG_XFS_LIVE_HOOKS */
+
#endif /* __XFS_INODE_H__ */
diff --git a/fs/xfs/xfs_mount.h b/fs/xfs/xfs_mount.h
index b099af731535..a9c61bf04841 100644
--- a/fs/xfs/xfs_mount.h
+++ b/fs/xfs/xfs_mount.h
@@ -369,6 +369,8 @@ typedef struct xfs_mount {
unsigned int *m_errortag;
struct xfs_kobj m_errortag_kobj;
#endif
+ /* Hook to feed file link count updates to an active online repair. */
+ struct xfs_hooks m_nlink_delta_hooks;
} xfs_mount_t;
#define M_IGEO(mp) (&(mp)->m_ino_geo)
diff --git a/fs/xfs/xfs_super.c b/fs/xfs/xfs_super.c
index 9ac59814bbb6..2c23209bcdf7 100644
--- a/fs/xfs/xfs_super.c
+++ b/fs/xfs/xfs_super.c
@@ -1938,6 +1938,8 @@ static int xfs_init_fs_context(
mp->m_logbsize = -1;
mp->m_allocsize_log = 16; /* 64k */
+ xfs_hooks_init(&mp->m_nlink_delta_hooks);
+
/*
* Copy binary VFS mount flags we are interested in.
*/
diff --git a/fs/xfs/xfs_symlink.c b/fs/xfs/xfs_symlink.c
index 8389f3ef88ef..8241c0fcd0ba 100644
--- a/fs/xfs/xfs_symlink.c
+++ b/fs/xfs/xfs_symlink.c
@@ -319,6 +319,7 @@ xfs_symlink(
goto out_trans_cancel;
xfs_trans_ichgtime(tp, dp, XFS_ICHGTIME_MOD | XFS_ICHGTIME_CHG);
xfs_trans_log_inode(tp, dp, XFS_ILOG_CORE);
+ xfs_nlink_dirent_delta(dp, ip, 1, link_name);
/*
* If this is a synchronous mount, make sure that the