Linux block layer
 help / color / mirror / Atom feed
* [PATCH v2 0/3] vfs: add inode iteration methods
@ 2026-11-13 11:56 Huang Shijie
  2026-11-13 11:57 ` [PATCH v2 1/3] " Huang Shijie
                   ` (2 more replies)
  0 siblings, 3 replies; 5+ messages in thread
From: Huang Shijie @ 2026-11-13 11:56 UTC (permalink / raw)
  To: axboe, viro, brauner, jack
  Cc: zhongyuan, fangbaoshun, yingzhiwei, agruenba, mic, gnoack, paul,
	jmorris, serge, aleksa, legion, david, sandeen, ebiggers,
	linux-block, linux-kernel, linux-fsdevel, gfs2,
	linux-security-module, sunjunchao, shijie8, Huang Shijie

This series adds two superblock inode iteration helpers:
super_iter_inodes()/super_iter_inodes_unsafe(), and converts the open-coded
sb->s_inodes walks in the VFS to it. The design is based on Dave Chinner's
patch set:
  "vfs: improving inode cache iteration scalability"
  https://lore.kernel.org/all/20241002014017.3801899-1-david@fromorbit.com/

About the two helpers:  
  int super_iter_inodes(struct super_block *sb,
                        struct inode_iter_context *ctx);

  int super_iter_inodes_unsafe(struct super_block *sb,
                               struct inode_iter_context *ctx);

  - super_iter_inodes() runs @ctx->iter_fn() with a referenced inode
    and with sb->s_inode_list_lock dropped, so the callback is free to
    block and to do whatever it wants with the inode except drop the
    reference the iterator holds.  
    It is exported because GFS2 can be built as a module.

  - super_iter_inodes_unsafe() keeps sb->s_inode_list_lock and
    rcu_read_lock() held for the whole walk and takes no inode
    references at all.  Its callback therefore only has an existence
    guarantee: it must not block and must do its own inode state
    checks and its own reference handling.  
    It is internal to the VFS, being declared in fs/internal.h.

  - %INO_ITER_F_REFERENCED additionally restricts the safe iterator to
    inodes that still have a reference.  This preserves the "only
    handle referenced inodes" rule of landlock's sb-delete walk.

  - @ctx->prev is the resume point of the unsafe iterator: a caller
    which has to drop the list lock in the middle of a walk stores the
    inode to continue from, and the next call starts after that inode
    instead of at the head.

   Patch 1 adds the two iterators.

   Patch 2 converts the callers that cannot use referenced inodes
           (dquot removal, inode eviction, nr_blockdev_pages() and the
           generic_shutdown_super() poisoning loop) to
           super_iter_inodes_unsafe().

   Patch 3 converts the remaining callers (sync_bdevs,
           drop_pagecache_sb, gfs2_kill_sb, add_dquot_ref and
           landlock's hook_sb_delete).


BTW:
  This patch set depends a small patch:
  "fs/drop_caches: skip filesystems without fs writeback"
  https://lkml.org/lkml/2026/9/23/675

v1-->v2:
   -split the super_iter_inodes_unsafe from super_iter_inodes.
   -add ->eligible() before calling the __iget().
   -others
   
   v1: https://lore.kernel.org/all/20261015093854.2082600-1-huangsj@hygon.cn/

Huang Shijie (3):
  vfs: add inode iteration methods
  vfs: convert inode iterators to super_iter_inodes_unsafe()
  vfs: convert sb->s_inodes iteration to super_iter_inodes()

 block/bdev.c                   | 111 ++++++++++++------------
 fs/drop_caches.c               |  49 +++++------
 fs/gfs2/ops_fstype.c           |  33 +++----
 fs/inode.c                     |  83 ++++++++++--------
 fs/internal.h                  |  34 ++++++++
 fs/quota/dquot.c               | 149 +++++++++++++++++---------------
 fs/super.c                     |  88 +++++++++++++++++--
 include/linux/fs/super_types.h |  25 ++++++
 security/landlock/fs.c         | 153 +++++++++++++--------------------
 9 files changed, 413 insertions(+), 312 deletions(-)

-- 
2.53.0



^ permalink raw reply	[flat|nested] 5+ messages in thread

* [PATCH v2 1/3] vfs: add inode iteration methods
  2026-11-13 11:56 [PATCH v2 0/3] vfs: add inode iteration methods Huang Shijie
@ 2026-11-13 11:57 ` Huang Shijie
  2026-11-13 12:23   ` Christoph Hellwig
  2026-11-13 11:57 ` [PATCH v2 2/3] vfs: convert inode iterators to super_iter_inodes_unsafe() Huang Shijie
  2026-11-13 11:57 ` [PATCH v2 3/3] vfs: convert sb->s_inodes iteration to super_iter_inodes() Huang Shijie
  2 siblings, 1 reply; 5+ messages in thread
From: Huang Shijie @ 2026-11-13 11:57 UTC (permalink / raw)
  To: axboe, viro, brauner, jack
  Cc: zhongyuan, fangbaoshun, yingzhiwei, agruenba, mic, gnoack, paul,
	jmorris, serge, aleksa, legion, david, sandeen, ebiggers,
	linux-block, linux-kernel, linux-fsdevel, gfs2,
	linux-security-module, sunjunchao, shijie8, Huang Shijie

Add two inode iteration methods:
     super_iter_inodes_unsafe()/super_iter_inodes()

We use them to replace the explicit sb->s_inodes iteration,
with the caller supplying an inode_iter_context which has the
metadata needed in the interation.

The two iteration methods:

-- super_iter_inodes() runs @ctx->iter_fn() with a referenced
   inode and with sb->s_inode_list_lock dropped, so the callback
   is free to block and to do whatever it wants with the inode
   except drop the reference the iterator holds. The iterator
   releases that one reference per inode itself, so the callback
   cannot leave a dangling inode behind. It is exported because
   filesystems built as modules need it.

-- super_iter_inodes_unsafe() keeps sb->s_inode_list_lock and
   rcu_read_lock() held for the whole walk and takes no inode
   references at all. Its callback therefore only has an
   existence guarantee: it must not block and must do its own
   inode state checks and its own reference handling. It is
   internal to the VFS, being declared in fs/internal.h.

Both take a struct inode_iter_context carrying the per-caller
state:

   iter_fn   the callback. Returns INO_ITER_RET_NEXT to go on
             with the next inode, or INO_ITER_RET_ABORT to stop.

   eligible  optional filter, called only by the safe iterator,
             from inside the inode's i_lock and before __iget(),
             so that an inode can be rejected without paying for
             a reference.

   prev      resume point. A caller of the unsafe iterator which
             has to drop the list lock in the middle of a walk
             stores the inode to continue from here, and the next
             call starts after that inode instead of at the head.

   flags     INO_ITER_F_REFERENCED makes the safe iterator skip
             inodes which have no reference, which is what
             hook_sb_delete() filters for today.

   priv      caller private data.

This patch only adds the methods; converting the callers over is
left to the patches which follow.

Signed-off-by: Huang Shijie <huangsj@hygon.cn>
---
 fs/internal.h                  | 34 ++++++++++++++++++
 fs/super.c                     | 65 ++++++++++++++++++++++++++++++++++
 include/linux/fs/super_types.h | 25 +++++++++++++
 3 files changed, 124 insertions(+)

diff --git a/fs/internal.h b/fs/internal.h
index 967c8556c17f..f9b39d05bbee 100644
--- a/fs/internal.h
+++ b/fs/internal.h
@@ -139,6 +139,40 @@ struct super_block *user_get_super(dev_t, bool excl);
 void put_super(struct super_block *sb);
 extern bool mount_capable(struct fs_context *);
 
+/*
+ * Unsafe iteration for internal VFS use only:
+ *   @ctx->iter_fn runs in atomic context with only
+ *   an inode existence guarantee, so it must not block
+ *   and must perform its own inode state checks.
+ *
+ * Return INO_ITER_RET_ABORT if the iteration stops early.
+ * Return INO_ITER_RET_DONE if the iteration runs to the end.
+ */
+static inline int super_iter_inodes_unsafe(struct super_block *sb,
+				struct inode_iter_context *ctx)
+{
+	struct inode *inode;
+	int ret = INO_ITER_RET_NEXT;
+
+	rcu_read_lock();
+	spin_lock(&sb->s_inode_list_lock);
+	inode = list_prepare_entry(ctx->prev, &sb->s_inodes, i_sb_list);
+
+	list_for_each_entry_continue(inode, &sb->s_inodes, i_sb_list) {
+		ret = ctx->iter_fn(inode, ctx);
+		if (ret == INO_ITER_RET_ABORT)
+			break;
+	}
+	spin_unlock(&sb->s_inode_list_lock);
+	rcu_read_unlock();
+
+	/* Mark the end here */
+	if (ret == INO_ITER_RET_NEXT)
+		ret = INO_ITER_RET_DONE;
+
+	return ret;
+}
+
 /*
  * Prepare superblock for changing its read-only state (i.e., either remount
  * read-write superblock read-only or vice versa). After this function returns
diff --git a/fs/super.c b/fs/super.c
index c5a0658f7aaf..32137422df35 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -171,6 +171,71 @@ static void super_wake(struct super_block *sb, unsigned int flag)
 	wake_up_var(&sb->s_flags);
 }
 
+static bool super_iter_iget(struct inode *inode, struct inode_iter_context *ctx)
+{
+	bool ret = false;
+
+	if ((ctx->flags & INO_ITER_F_REFERENCED) && !icount_read_once(inode))
+		return false;
+
+	spin_lock(&inode->i_lock);
+	if (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE))
+		goto out_unlock;
+
+	if (ctx->eligible && !ctx->eligible(inode, ctx))
+		goto out_unlock;
+
+	__iget(inode);
+	ret = true;
+out_unlock:
+	spin_unlock(&inode->i_lock);
+	return ret;
+}
+
+/**
+ * super_iter_inodes - iterate all the cached inodes on a superblock
+ * @sb: superblock to iterate
+ * @ctx: the context which has the metadata
+ *
+ * This function iterates all cached inodes on a superblock that are not in
+ * the process of being initialised or torn down. It will run @ctx->iter_fn()
+ * with a valid, referenced inode, so it is safe for the caller to do
+ * anything it wants with the inode except drop the reference the iterator
+ * holds.
+ *
+ * Return INO_ITER_RET_DONE if the iteration runs to the end.
+ * Return other value if the iteration breaks.
+ */
+int super_iter_inodes(struct super_block *sb, struct inode_iter_context *ctx)
+{
+	struct inode *inode, *old_inode = NULL;
+	int ret;
+
+	spin_lock(&sb->s_inode_list_lock);
+	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
+		if (!super_iter_iget(inode, ctx))
+			continue;
+
+		spin_unlock(&sb->s_inode_list_lock);
+		iput(old_inode);
+
+		ret = ctx->iter_fn(inode, ctx);
+
+		old_inode = inode;
+		if (ret != INO_ITER_RET_NEXT)
+			goto out_loop;
+
+		cond_resched();
+		spin_lock(&sb->s_inode_list_lock);
+	}
+	spin_unlock(&sb->s_inode_list_lock);
+	ret = INO_ITER_RET_DONE;
+out_loop:
+	iput(old_inode);
+	return ret;
+}
+EXPORT_SYMBOL_GPL(super_iter_inodes);
+
 /*
  * One thing we have to be careful of with a per-sb shrinker is that we don't
  * drop the last active reference to the superblock from within the shrinker.
diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
index 72f4288e16c5..da15c360def3 100644
--- a/include/linux/fs/super_types.h
+++ b/include/linux/fs/super_types.h
@@ -80,6 +80,31 @@ enum freeze_holder {
 	FREEZE_EXCL		= (1U << 3),
 };
 
+/* Inode iteration callback return values */
+#define INO_ITER_RET_DONE		0
+#define INO_ITER_RET_ABORT		1
+#define INO_ITER_RET_NEXT		2
+
+/* Inode iteration control flags */
+/* Only iterate inodes that still have a reference. */
+#define INO_ITER_F_REFERENCED		(1U << 0)
+
+struct inode_iter_context {
+	/* the hook for the iteration */
+	int (*iter_fn)(struct inode *inode, struct inode_iter_context *ctx);
+
+	/* the hook for the eligibility check, called before __iget() */
+	bool (*eligible)(struct inode *inode, struct inode_iter_context *ctx);
+
+	/* Save the temporary inode in a loop break */
+	struct inode *prev;
+
+	int flags;
+	void *priv;
+};
+
+int super_iter_inodes(struct super_block *sb, struct inode_iter_context *ctx);
+
 struct super_operations {
 	struct inode *(*alloc_inode)(struct super_block *sb);
 	void (*destroy_inode)(struct inode *inode);
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH v2 2/3] vfs: convert inode iterators to super_iter_inodes_unsafe()
  2026-11-13 11:56 [PATCH v2 0/3] vfs: add inode iteration methods Huang Shijie
  2026-11-13 11:57 ` [PATCH v2 1/3] " Huang Shijie
@ 2026-11-13 11:57 ` Huang Shijie
  2026-11-13 11:57 ` [PATCH v2 3/3] vfs: convert sb->s_inodes iteration to super_iter_inodes() Huang Shijie
  2 siblings, 0 replies; 5+ messages in thread
From: Huang Shijie @ 2026-11-13 11:57 UTC (permalink / raw)
  To: axboe, viro, brauner, jack
  Cc: zhongyuan, fangbaoshun, yingzhiwei, agruenba, mic, gnoack, paul,
	jmorris, serge, aleksa, legion, david, sandeen, ebiggers,
	linux-block, linux-kernel, linux-fsdevel, gfs2,
	linux-security-module, sunjunchao, shijie8, Huang Shijie

Convert VFS internal superblock inode iterators that cannot use
referenced inodes to super_iter_inodes_unsafe():
  - Dquot removal
  - inode eviction
  - The nr_blockdev_pages()
  - generic_shutdown_super()

Signed-off-by: Huang Shijie <huangsj@hygon.cn>
---
 block/bdev.c     | 19 +++++++----
 fs/inode.c       | 83 ++++++++++++++++++++++++++++--------------------
 fs/quota/dquot.c | 73 ++++++++++++++++++++++++++----------------
 fs/super.c       | 23 ++++++++------
 4 files changed, 121 insertions(+), 77 deletions(-)

diff --git a/block/bdev.c b/block/bdev.c
index f20f91b3176e..3d8dd36b76df 100644
--- a/block/bdev.c
+++ b/block/bdev.c
@@ -558,16 +558,23 @@ void bdev_drop(struct block_device *bdev)
 	iput(BD_INODE(bdev));
 }
 
+static int bdev_pages_count(struct inode *inode, struct inode_iter_context *ctx)
+{
+	long *pages = ctx->priv;
+
+	*pages += inode->i_mapping->nrpages;
+	return INO_ITER_RET_NEXT;
+}
+
 long nr_blockdev_pages(void)
 {
-	struct inode *inode;
 	long ret = 0;
+	struct inode_iter_context ctx = {
+		.iter_fn = bdev_pages_count,
+		.priv = &ret,
+	};
 
-	spin_lock(&blockdev_superblock->s_inode_list_lock);
-	list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list)
-		ret += inode->i_mapping->nrpages;
-	spin_unlock(&blockdev_superblock->s_inode_list_lock);
-
+	super_iter_inodes_unsafe(blockdev_superblock, &ctx);
 	return ret;
 }
 
diff --git a/fs/inode.c b/fs/inode.c
index f63535ee3629..f4a2a13a30b2 100644
--- a/fs/inode.c
+++ b/fs/inode.c
@@ -865,6 +865,42 @@ static void dispose_list(struct list_head *head)
 	}
 }
 
+static int evict_inode_fn(struct inode *inode, struct inode_iter_context *ctx)
+{
+	struct list_head *dispose = ctx->priv;
+
+	if (ctx->prev) {
+		list_add(&ctx->prev->i_lru, dispose);
+		ctx->prev = NULL;
+	}
+
+	if (icount_read_once(inode))
+		return INO_ITER_RET_NEXT;
+
+	spin_lock(&inode->i_lock);
+	if (icount_read(inode) ||
+	    (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE))) {
+		spin_unlock(&inode->i_lock);
+		return INO_ITER_RET_NEXT;
+	}
+
+	inode_state_set(inode, I_FREEING);
+	inode_lru_list_del(inode);
+	spin_unlock(&inode->i_lock);
+
+	/*
+	 * Keep this inode out of dispose so it stays on s_inodes while the
+	 * list lock is dropped. I_FREEING prevents new references and leaves
+	 * eviction to us, so the next walk can resume from it.
+	 */
+	if (need_resched()) {
+		ctx->prev = inode;
+		return INO_ITER_RET_ABORT;
+	}
+	list_add(&inode->i_lru, dispose);
+	return INO_ITER_RET_NEXT;
+}
+
 /**
  * evict_inodes	- evict all evictable inodes for a superblock
  * @sb:		superblock to operate on
@@ -876,44 +912,23 @@ static void dispose_list(struct list_head *head)
  */
 void evict_inodes(struct super_block *sb)
 {
-	struct inode *inode;
+	int ret;
 	LIST_HEAD(dispose);
+	struct inode_iter_context ctx = {
+		.iter_fn = evict_inode_fn,
+		.priv = &dispose,
+	};
 
-	spin_lock(&sb->s_inode_list_lock);
-	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-		if (icount_read_once(inode))
-			continue;
-
-		spin_lock(&inode->i_lock);
-		if (icount_read(inode)) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-		if (inode_state_read(inode) & (I_NEW | I_FREEING | I_WILL_FREE)) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-
-		inode_state_set(inode, I_FREEING);
-		inode_lru_list_del(inode);
-		spin_unlock(&inode->i_lock);
+	do {
+		ret = super_iter_inodes_unsafe(sb, &ctx);
 
-		/*
-		 * Keep this inode out of dispose so it stays on s_inodes while
-		 * the list lock is dropped. I_FREEING prevents new references
-		 * and leaves eviction to us, so we can resume the walk from it.
-		 */
-		if (need_resched()) {
-			spin_unlock(&sb->s_inode_list_lock);
-			cond_resched();
-			dispose_list(&dispose);
-			spin_lock(&sb->s_inode_list_lock);
-		}
-		list_add(&inode->i_lru, &dispose);
-	}
-	spin_unlock(&sb->s_inode_list_lock);
+		/* Handle the loop end carefully */
+		if (ret == INO_ITER_RET_DONE && ctx.prev)
+			list_add(&ctx.prev->i_lru, &dispose);
 
-	dispose_list(&dispose);
+		cond_resched();
+		dispose_list(&dispose);
+	} while (ret != INO_ITER_RET_DONE);
 }
 EXPORT_SYMBOL_GPL(evict_inodes);
 
diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c
index a3d503591871..1b374a0c347a 100644
--- a/fs/quota/dquot.c
+++ b/fs/quota/dquot.c
@@ -83,6 +83,8 @@
 
 #include <linux/uaccess.h>
 
+#include "../internal.h"
+
 /*
  * There are five quota SMP locks:
  * * dq_list_lock protects all lists with quotas and quota formats.
@@ -165,6 +167,11 @@ static struct kmem_cache *dquot_cachep;
 /* workqueue for work quota_release_work*/
 static struct workqueue_struct *quota_unbound_wq;
 
+struct dquot_ref_data {
+	int type;
+	int reserved;
+};
+
 void register_quota_format(struct quota_format_type *fmt)
 {
 	spin_lock(&dq_list_lock);
@@ -1117,41 +1124,51 @@ static int add_dquot_ref(struct super_block *sb, int type)
 	return err;
 }
 
-static void remove_dquot_ref(struct super_block *sb, int type)
+static int remove_dquot_ref_fn(struct inode *inode,
+				struct inode_iter_context *ctx)
 {
-	struct inode *inode;
-#ifdef CONFIG_QUOTA_DEBUG
-	int reserved = 0;
-#endif
+	struct dquot_ref_data *ref = ctx->priv;
 
-	spin_lock(&sb->s_inode_list_lock);
-	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-		/*
-		 *  We have to scan also I_NEW inodes because they can already
-		 *  have quota pointer initialized. Luckily, we need to touch
-		 *  only quota pointers and these have separate locking
-		 *  (dq_data_lock).
-		 */
-		spin_lock(&dq_data_lock);
-		if (!IS_NOQUOTA(inode)) {
-			struct dquot __rcu **dquots = i_dquot(inode);
-			struct dquot *dquot = srcu_dereference_check(
-				dquots[type], &dquot_srcu,
-				lockdep_is_held(&dq_data_lock));
+	spin_lock(&dq_data_lock);
+	if (!IS_NOQUOTA(inode)) {
+		struct dquot __rcu **dquots = i_dquot(inode);
+		struct dquot *dquot = srcu_dereference_check(
+			dquots[ref->type], &dquot_srcu,
+			lockdep_is_held(&dq_data_lock));
 
 #ifdef CONFIG_QUOTA_DEBUG
-			if (unlikely(inode_get_rsv_space(inode) > 0))
-				reserved = 1;
+		if (unlikely(inode_get_rsv_space(inode) > 0))
+			ref->reserved++;
 #endif
-			rcu_assign_pointer(dquots[type], NULL);
-			if (dquot)
-				dqput(dquot);
-		}
-		spin_unlock(&dq_data_lock);
+		rcu_assign_pointer(dquots[ref->type], NULL);
+		if (dquot)
+			dqput(dquot);
 	}
-	spin_unlock(&sb->s_inode_list_lock);
+	spin_unlock(&dq_data_lock);
+	return INO_ITER_RET_NEXT;
+}
+
+static void remove_dquot_ref(struct super_block *sb, int type)
+{
+	struct dquot_ref_data ref = {
+		.type = type,
+	};
+	struct inode_iter_context ctx = {
+		.iter_fn = remove_dquot_ref_fn,
+		.priv = &ref,
+	};
+
+	/*
+	 * We have to scan I_NEW inodes because they can already
+	 * have quota pointer initialized. Luckily, we need to touch
+	 * only quota pointers and these have separate locking
+	 * (dq_data_lock) so the existence guarantee that
+	 * super_iter_inodes_unsafe() provides inodes passed to
+	 * remove_dquot_ref_fn() is sufficient for this operation.
+	 */
+	super_iter_inodes_unsafe(sb, &ctx);
 #ifdef CONFIG_QUOTA_DEBUG
-	if (reserved) {
+	if (ref.reserved) {
 		printk(KERN_WARNING "VFS (%s): Writes happened after quota"
 			" was disabled thus quota information is probably "
 			"inconsistent. Please run quotacheck(8).\n", sb->s_id);
diff --git a/fs/super.c b/fs/super.c
index 32137422df35..f2a93bdc4a81 100644
--- a/fs/super.c
+++ b/fs/super.c
@@ -743,6 +743,15 @@ void retire_super(struct super_block *sb)
 }
 EXPORT_SYMBOL(retire_super);
 
+static int inode_poison_fn(struct inode *inode, struct inode_iter_context *ctx)
+{
+	inode->i_op = VFS_PTR_POISON;
+	inode->i_sb = VFS_PTR_POISON;
+	inode->i_mapping = VFS_PTR_POISON;
+
+	return INO_ITER_RET_NEXT;
+}
+
 /**
  *	generic_shutdown_super	-	common helper for ->kill_sb()
  *	@sb: superblock to kill
@@ -796,20 +805,16 @@ void generic_shutdown_super(struct super_block *sb)
 		if (CHECK_DATA_CORRUPTION(!list_empty(&sb->s_inodes), NULL,
 				"VFS: Busy inodes after unmount of %s (%s)",
 				sb->s_id, sb->s_type->name)) {
+			struct inode_iter_context ctx = {
+				.iter_fn = inode_poison_fn,
+			};
+
 			/*
 			 * Adding a proper bailout path here would be hard, but
 			 * we can at least make it more likely that a later
 			 * iput_final() or such crashes cleanly.
 			 */
-			struct inode *inode;
-
-			spin_lock(&sb->s_inode_list_lock);
-			list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-				inode->i_op = VFS_PTR_POISON;
-				inode->i_sb = VFS_PTR_POISON;
-				inode->i_mapping = VFS_PTR_POISON;
-			}
-			spin_unlock(&sb->s_inode_list_lock);
+			super_iter_inodes_unsafe(sb, &ctx);
 		}
 	}
 	/*
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* [PATCH v2 3/3] vfs: convert sb->s_inodes iteration to super_iter_inodes()
  2026-11-13 11:56 [PATCH v2 0/3] vfs: add inode iteration methods Huang Shijie
  2026-11-13 11:57 ` [PATCH v2 1/3] " Huang Shijie
  2026-11-13 11:57 ` [PATCH v2 2/3] vfs: convert inode iterators to super_iter_inodes_unsafe() Huang Shijie
@ 2026-11-13 11:57 ` Huang Shijie
  2 siblings, 0 replies; 5+ messages in thread
From: Huang Shijie @ 2026-11-13 11:57 UTC (permalink / raw)
  To: axboe, viro, brauner, jack
  Cc: zhongyuan, fangbaoshun, yingzhiwei, agruenba, mic, gnoack, paul,
	jmorris, serge, aleksa, legion, david, sandeen, ebiggers,
	linux-block, linux-kernel, linux-fsdevel, gfs2,
	linux-security-module, sunjunchao, shijie8, Huang Shijie

Convert all the remaining superblock inode iterators to use
super_iter_inodes(). These are mostly straight forward conversions
for the iterations that use referenced inodes:
 - sync_bdevs
 - drop_pagecache_sb
 - gfs2_kill_sb
 - add_dquot_ref
 - hook_sb_delete

Signed-off-by: Huang Shijie <huangsj@hygon.cn>
---
 block/bdev.c           |  92 +++++++++++--------------
 fs/drop_caches.c       |  49 ++++++-------
 fs/gfs2/ops_fstype.c   |  33 +++------
 fs/quota/dquot.c       |  76 +++++++++-----------
 security/landlock/fs.c | 153 +++++++++++++++++------------------------
 5 files changed, 168 insertions(+), 235 deletions(-)

diff --git a/block/bdev.c b/block/bdev.c
index 3d8dd36b76df..8ef6cdad0cec 100644
--- a/block/bdev.c
+++ b/block/bdev.c
@@ -1343,66 +1343,54 @@ void bdev_mark_dead(struct block_device *bdev, bool surprise)
  */
 EXPORT_SYMBOL_GPL(bdev_mark_dead);
 
-void sync_bdevs(bool wait)
+static bool sync_bdev_eligible(struct inode *inode,
+				struct inode_iter_context *ctx)
 {
-	struct inode *inode, *old_inode = NULL;
+	return inode->i_mapping->nrpages != 0;
+}
 
-	spin_lock(&blockdev_superblock->s_inode_list_lock);
-	list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list) {
-		struct address_space *mapping = inode->i_mapping;
-		struct block_device *bdev;
+static int sync_bdev_fn(struct inode *inode, struct inode_iter_context *ctx)
+{
+	bool wait = *(bool *)ctx->priv;
+	struct block_device *bdev;
 
-		spin_lock(&inode->i_lock);
-		if (inode_state_read(inode) & (I_FREEING | I_WILL_FREE | I_NEW) ||
-		    mapping->nrpages == 0) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-		__iget(inode);
-		spin_unlock(&inode->i_lock);
-		spin_unlock(&blockdev_superblock->s_inode_list_lock);
-		/*
-		 * We hold a reference to 'inode' so it couldn't have been
-		 * removed from s_inodes list while we dropped the
-		 * s_inode_list_lock  We cannot iput the inode now as we can
-		 * be holding the last reference and we cannot iput it under
-		 * s_inode_list_lock. So we keep the reference and iput it
-		 * later.
-		 */
-		iput(old_inode);
-		old_inode = inode;
-		bdev = I_BDEV(inode);
+	bdev = I_BDEV(inode);
+	/*
+	 * The inode does not keep the gendisk of a partition alive, the
+	 * device does.
+	 */
+	if (!kobject_get_unless_zero(&bdev->bd_device.kobj))
+		return INO_ITER_RET_NEXT;
 
+	mutex_lock(&bdev->bd_disk->open_mutex);
+	if (!atomic_read(&bdev->bd_openers)) {
+		; /* skip */
+	} else if (wait) {
 		/*
-		 * The inode does not keep the gendisk of a partition alive, the
-		 * device does.
+		 * We keep the error status of individual mapping so
+		 * that applications can catch the writeback error using
+		 * fsync(2). See filemap_fdatawait_keep_errors() for
+		 * details.
 		 */
-		if (!kobject_get_unless_zero(&bdev->bd_device.kobj)) {
-			spin_lock(&blockdev_superblock->s_inode_list_lock);
-			continue;
-		}
+		filemap_fdatawait_keep_errors(inode->i_mapping);
+	} else {
+		filemap_fdatawrite(inode->i_mapping);
+	}
+	mutex_unlock(&bdev->bd_disk->open_mutex);
+	put_device(&bdev->bd_device);
 
-		mutex_lock(&bdev->bd_disk->open_mutex);
-		if (!atomic_read(&bdev->bd_openers)) {
-			; /* skip */
-		} else if (wait) {
-			/*
-			 * We keep the error status of individual mapping so
-			 * that applications can catch the writeback error using
-			 * fsync(2). See filemap_fdatawait_keep_errors() for
-			 * details.
-			 */
-			filemap_fdatawait_keep_errors(inode->i_mapping);
-		} else {
-			filemap_fdatawrite(inode->i_mapping);
-		}
-		mutex_unlock(&bdev->bd_disk->open_mutex);
-		put_device(&bdev->bd_device);
+	return INO_ITER_RET_NEXT;
+}
 
-		spin_lock(&blockdev_superblock->s_inode_list_lock);
-	}
-	spin_unlock(&blockdev_superblock->s_inode_list_lock);
-	iput(old_inode);
+void sync_bdevs(bool wait)
+{
+	struct inode_iter_context ctx = {
+		.iter_fn = sync_bdev_fn,
+		.eligible = sync_bdev_eligible,
+		.priv = &wait,
+	};
+
+	super_iter_inodes(blockdev_superblock, &ctx);
 }
 
 /*
diff --git a/fs/drop_caches.c b/fs/drop_caches.c
index 28c5fb9c7357..8b286a595d12 100644
--- a/fs/drop_caches.c
+++ b/fs/drop_caches.c
@@ -17,39 +17,34 @@
 /* A global variable is a bit ugly, but it keeps the code simple */
 static int sysctl_drop_caches;
 
+static bool invalidate_inode_eligible(struct inode *inode,
+			struct inode_iter_context *ctx)
+{
+	/*
+	 * We skip inodes without pages but we deliberately won't
+	 * in case we need to reschedule to avoid softlockups.
+	 */
+	return !mapping_empty(inode->i_mapping) || need_resched();
+}
+
+static int invalidate_inode_fn(struct inode *inode,
+			struct inode_iter_context *ctx)
+{
+	invalidate_mapping_pages(inode->i_mapping, 0, -1);
+	return INO_ITER_RET_NEXT;
+}
+
 static void drop_pagecache_sb(struct super_block *sb, void *unused)
 {
-	struct inode *inode, *toput_inode = NULL;
+	struct inode_iter_context ctx = {
+		.iter_fn = invalidate_inode_fn,
+		.eligible = invalidate_inode_eligible,
+	};
 
 	if (sb->s_bdi == &noop_backing_dev_info)
 		return;
 
-	spin_lock(&sb->s_inode_list_lock);
-	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-		spin_lock(&inode->i_lock);
-		/*
-		 * We must skip inodes in unusual state. We may also skip
-		 * inodes without pages but we deliberately won't in case
-		 * we need to reschedule to avoid softlockups.
-		 */
-		if ((inode_state_read(inode) & (I_FREEING | I_WILL_FREE | I_NEW)) ||
-		    (mapping_empty(inode->i_mapping) && !need_resched())) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-		__iget(inode);
-		spin_unlock(&inode->i_lock);
-		spin_unlock(&sb->s_inode_list_lock);
-
-		invalidate_mapping_pages(inode->i_mapping, 0, -1);
-		iput(toput_inode);
-		toput_inode = inode;
-
-		cond_resched();
-		spin_lock(&sb->s_inode_list_lock);
-	}
-	spin_unlock(&sb->s_inode_list_lock);
-	iput(toput_inode);
+	super_iter_inodes(sb, &ctx);
 }
 
 static int drop_caches_sysctl_handler(const struct ctl_table *table, int write,
diff --git a/fs/gfs2/ops_fstype.c b/fs/gfs2/ops_fstype.c
index 6ec47376aecf..e27b0bdf1dbf 100644
--- a/fs/gfs2/ops_fstype.c
+++ b/fs/gfs2/ops_fstype.c
@@ -1717,6 +1717,13 @@ static int gfs2_meta_init_fs_context(struct fs_context *fc)
 	return 0;
 }
 
+/* Nothing to do because we just want to bounce the inode through iput() */
+static int gfs2_evict_inode_fn(struct inode *inode,
+			struct inode_iter_context *ctx)
+{
+	return INO_ITER_RET_NEXT;
+}
+
 /**
  * gfs2_evict_inodes - evict inodes cooperatively
  * @sb: the superblock
@@ -1739,31 +1746,13 @@ static int gfs2_meta_init_fs_context(struct fs_context *fc)
  */
 static void gfs2_evict_inodes(struct super_block *sb)
 {
-	struct inode *inode, *toput_inode = NULL;
 	struct gfs2_sbd *sdp = sb->s_fs_info;
+	struct inode_iter_context ctx = {
+		.iter_fn = gfs2_evict_inode_fn,
+	};
 
 	set_bit(SDF_EVICTING, &sdp->sd_flags);
-
-	spin_lock(&sb->s_inode_list_lock);
-	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-		spin_lock(&inode->i_lock);
-		if ((inode_state_read(inode) & (I_FREEING | I_WILL_FREE | I_NEW)) &&
-		    !need_resched()) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-		__iget(inode);
-		spin_unlock(&inode->i_lock);
-		spin_unlock(&sb->s_inode_list_lock);
-
-		iput(toput_inode);
-		toput_inode = inode;
-
-		cond_resched();
-		spin_lock(&sb->s_inode_list_lock);
-	}
-	spin_unlock(&sb->s_inode_list_lock);
-	iput(toput_inode);
+	super_iter_inodes(sb, &ctx);
 }
 
 static void gfs2_kill_sb(struct super_block *sb)
diff --git a/fs/quota/dquot.c b/fs/quota/dquot.c
index 1b374a0c347a..25ea8f176363 100644
--- a/fs/quota/dquot.c
+++ b/fs/quota/dquot.c
@@ -1066,56 +1066,46 @@ static int dqinit_needed(struct inode *inode, int type)
 	return 0;
 }
 
-/* This routine is guarded by s_umount semaphore */
-static int add_dquot_ref(struct super_block *sb, int type)
+static bool add_dquot_ref_eligible(struct inode *inode,
+				struct inode_iter_context *ctx)
 {
-	struct inode *inode, *old_inode = NULL;
-#ifdef CONFIG_QUOTA_DEBUG
-	int reserved = 0;
-#endif
-	int err = 0;
+	struct dquot_ref_data *ref = ctx->priv;
 
-	spin_lock(&sb->s_inode_list_lock);
-	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-		spin_lock(&inode->i_lock);
-		if ((inode_state_read(inode) & (I_FREEING | I_WILL_FREE | I_NEW)) ||
-		    !atomic_read(&inode->i_writecount) ||
-		    !dqinit_needed(inode, type)) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-		__iget(inode);
-		spin_unlock(&inode->i_lock);
-		spin_unlock(&sb->s_inode_list_lock);
+	return atomic_read(&inode->i_writecount) &&
+	       dqinit_needed(inode, ref->type);
+}
+
+static int add_dquot_ref_fn(struct inode *inode, struct inode_iter_context *ctx)
+{
+	struct dquot_ref_data *ref = ctx->priv;
+	int ret;
 
 #ifdef CONFIG_QUOTA_DEBUG
-		if (unlikely(inode_get_rsv_space(inode) > 0))
-			reserved = 1;
+	if (unlikely(inode_get_rsv_space(inode) > 0))
+		ref->reserved++;
 #endif
-		iput(old_inode);
-		err = __dquot_initialize(inode, type);
-		if (err) {
-			iput(inode);
-			goto out;
-		}
+	ret = __dquot_initialize(inode, ref->type);
+	if (ret)
+		return ret;
+	return INO_ITER_RET_NEXT;
+}
 
-		/*
-		 * We hold a reference to 'inode' so it couldn't have been
-		 * removed from s_inodes list while we dropped the
-		 * s_inode_list_lock. We cannot iput the inode now as we can be
-		 * holding the last reference and we cannot iput it under
-		 * s_inode_list_lock. So we keep the reference and iput it
-		 * later.
-		 */
-		old_inode = inode;
-		cond_resched();
-		spin_lock(&sb->s_inode_list_lock);
-	}
-	spin_unlock(&sb->s_inode_list_lock);
-	iput(old_inode);
-out:
+/* This routine is guarded by s_umount semaphore */
+static int add_dquot_ref(struct super_block *sb, int type)
+{
+	struct dquot_ref_data ref = {
+		.type = type,
+	};
+	struct inode_iter_context ctx = {
+		.iter_fn = add_dquot_ref_fn,
+		.eligible = add_dquot_ref_eligible,
+		.priv = &ref,
+	};
+	int err;
+
+	err = super_iter_inodes(sb, &ctx);
 #ifdef CONFIG_QUOTA_DEBUG
-	if (reserved) {
+	if (ref.reserved) {
 		quota_error(sb, "Writes happened before quota was turned on "
 			"thus quota information is probably inconsistent. "
 			"Please run quotacheck(8)");
diff --git a/security/landlock/fs.c b/security/landlock/fs.c
index 2ab67bcad198..f4385d5606c5 100644
--- a/security/landlock/fs.c
+++ b/security/landlock/fs.c
@@ -1375,110 +1375,81 @@ static void hook_inode_free_security_rcu(void *inode_security)
 
 /* Super-block hooks */
 
-/*
- * Release the inodes used in a security policy.
- *
- * Cf. fsnotify_unmount_inodes() and evict_inodes()
- */
-static void hook_sb_delete(struct super_block *const sb)
+static bool release_inode_eligible(struct inode *inode,
+				struct inode_iter_context *ctx)
 {
-	struct inode *inode, *prev_inode = NULL;
+	bool ret;
 
-	if (!landlock_initialized)
-		return;
+	rcu_read_lock();
+	ret = !!rcu_dereference(landlock_inode(inode)->object);
+	rcu_read_unlock();
 
-	spin_lock(&sb->s_inode_list_lock);
-	list_for_each_entry(inode, &sb->s_inodes, i_sb_list) {
-		struct landlock_object *object;
+	return ret;
+}
 
-		/* Only handles referenced inodes. */
-		if (!icount_read_once(inode))
-			continue;
+static int release_inode_fn(struct inode *inode, struct inode_iter_context *ctx)
+{
+	struct landlock_object *object;
+
+	rcu_read_lock();
+	object = rcu_dereference(landlock_inode(inode)->object);
+	if (!object) {
+		rcu_read_unlock();
+		return INO_ITER_RET_NEXT;
+	}
+
+	/*
+	 * If there is no concurrent release_inode() ongoing, then we
+	 * are in charge of calling iput() on this inode, otherwise we
+	 * will just wait for it to finish.
+	 */
+	spin_lock(&object->lock);
+	if (object->underobj == inode) {
+		object->underobj = NULL;
+		spin_unlock(&object->lock);
+		rcu_read_unlock();
 
 		/*
-		 * Protects against concurrent modification of inode (e.g.
-		 * from get_inode_object()).
+		 * Because object->underobj was not NULL,
+		 * release_inode() and get_inode_object() guarantee
+		 * that it is safe to reset
+		 * landlock_inode(inode)->object while it is not NULL.
+		 * It is therefore not necessary to lock inode->i_lock.
 		 */
-		spin_lock(&inode->i_lock);
+		rcu_assign_pointer(landlock_inode(inode)->object, NULL);
 		/*
-		 * Checks I_FREEING and I_WILL_FREE  to protect against a race
-		 * condition when release_inode() just called iput(), which
-		 * could lead to a NULL dereference of inode->security or a
-		 * second call to iput() for the same Landlock object.  Also
-		 * checks I_NEW because such inode cannot be tied to an object.
+		 * At this point, we own the ihold() reference that was
+		 * originally set up by get_inode_object() and the
+		 * __iget() reference that we just set in this loop
+		 * walk.  Therefore there are at least two references
+		 * on the inode.
 		 */
-		if (inode_state_read(inode) &
-		    (I_FREEING | I_WILL_FREE | I_NEW)) {
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-
-		rcu_read_lock();
-		object = rcu_dereference(landlock_inode(inode)->object);
-		if (!object) {
-			rcu_read_unlock();
-			spin_unlock(&inode->i_lock);
-			continue;
-		}
-		/* Keeps a reference to this inode until the next loop walk. */
-		__iget(inode);
-		spin_unlock(&inode->i_lock);
+		iput_not_last(inode);
+	} else {
+		spin_unlock(&object->lock);
+		rcu_read_unlock();
+	}
+	return INO_ITER_RET_NEXT;
+}
 
-		/*
-		 * If there is no concurrent release_inode() ongoing, then we
-		 * are in charge of calling iput() on this inode, otherwise we
-		 * will just wait for it to finish.
-		 */
-		spin_lock(&object->lock);
-		if (object->underobj == inode) {
-			object->underobj = NULL;
-			spin_unlock(&object->lock);
-			rcu_read_unlock();
+/*
+ * Release the inodes used in a security policy.
+ *
+ * Cf. fsnotify_unmount_inodes() and evict_inodes()
+ */
+static void hook_sb_delete(struct super_block *const sb)
+{
+	struct inode_iter_context ctx = {
+		.iter_fn = release_inode_fn,
+		.eligible = release_inode_eligible,
+		.flags = INO_ITER_F_REFERENCED,
+	};
 
-			/*
-			 * Because object->underobj was not NULL,
-			 * release_inode() and get_inode_object() guarantee
-			 * that it is safe to reset
-			 * landlock_inode(inode)->object while it is not NULL.
-			 * It is therefore not necessary to lock inode->i_lock.
-			 */
-			rcu_assign_pointer(landlock_inode(inode)->object, NULL);
-			/*
-			 * At this point, we own the ihold() reference that was
-			 * originally set up by get_inode_object() and the
-			 * __iget() reference that we just set in this loop
-			 * walk.  Therefore there are at least two references
-			 * on the inode.
-			 */
-			iput_not_last(inode);
-		} else {
-			spin_unlock(&object->lock);
-			rcu_read_unlock();
-		}
+	if (!landlock_initialized)
+		return;
 
-		if (prev_inode) {
-			/*
-			 * At this point, we still own the __iget() reference
-			 * that we just set in this loop walk.  Therefore we
-			 * can drop the list lock and know that the inode won't
-			 * disappear from under us until the next loop walk.
-			 */
-			spin_unlock(&sb->s_inode_list_lock);
-			/*
-			 * We can now actually put the inode reference from the
-			 * previous loop walk, which is not needed anymore.
-			 */
-			iput(prev_inode);
-			cond_resched();
-			spin_lock(&sb->s_inode_list_lock);
-		}
-		prev_inode = inode;
-	}
-	spin_unlock(&sb->s_inode_list_lock);
+	super_iter_inodes(sb, &ctx);
 
-	/* Puts the inode reference from the last loop walk, if any. */
-	if (prev_inode)
-		iput(prev_inode);
 	/* Waits for pending iput() in release_inode(). */
 	wait_var_event(&landlock_superblock(sb)->inode_refs,
 		       !atomic_long_read(&landlock_superblock(sb)->inode_refs));
-- 
2.53.0



^ permalink raw reply related	[flat|nested] 5+ messages in thread

* Re: [PATCH v2 1/3] vfs: add inode iteration methods
  2026-11-13 11:57 ` [PATCH v2 1/3] " Huang Shijie
@ 2026-11-13 12:23   ` Christoph Hellwig
  0 siblings, 0 replies; 5+ messages in thread
From: Christoph Hellwig @ 2026-11-13 12:23 UTC (permalink / raw)
  To: Huang Shijie
  Cc: axboe, viro, brauner, jack, zhongyuan, fangbaoshun, yingzhiwei,
	agruenba, mic, gnoack, paul, jmorris, serge, aleksa, legion,
	david, sandeen, ebiggers, linux-block, linux-kernel,
	linux-fsdevel, gfs2, linux-security-module, sunjunchao, shijie8

On Fri, Nov 13, 2026 at 07:57:00PM +0800, Huang Shijie wrote:
> +/*
> + * Unsafe iteration for internal VFS use only:
> + *   @ctx->iter_fn runs in atomic context with only
> + *   an inode existence guarantee, so it must not block
> + *   and must perform its own inode state checks.
> + *
> + * Return INO_ITER_RET_ABORT if the iteration stops early.
> + * Return INO_ITER_RET_DONE if the iteration runs to the end.
> + */
> +static inline int super_iter_inodes_unsafe(struct super_block *sb,
> +				struct inode_iter_context *ctx)
> +{
> +	struct inode *inode;
> +	int ret = INO_ITER_RET_NEXT;
> +
> +	rcu_read_lock();
> +	spin_lock(&sb->s_inode_list_lock);
> +	inode = list_prepare_entry(ctx->prev, &sb->s_inodes, i_sb_list);
> +
> +	list_for_each_entry_continue(inode, &sb->s_inodes, i_sb_list) {

This is a really odd way to write a list iteration.  I also don't see
how the RCU lock is needed when you already hold the spinlock.

I honestly don't get this "iterator.  It is trivial, the _unsafe is
wrong naming.  What is the point?

> +/**
> + * super_iter_inodes - iterate all the cached inodes on a superblock
> + * @sb: superblock to iterate
> + * @ctx: the context which has the metadata
> + *
> + * This function iterates all cached inodes on a superblock that are not in
> + * the process of being initialised or torn down. It will run @ctx->iter_fn()
> + * with a valid, referenced inode, so it is safe for the caller to do
> + * anything it wants with the inode except drop the reference the iterator
> + * holds.
> + *
> + * Return INO_ITER_RET_DONE if the iteration runs to the end.
> + * Return other value if the iteration breaks.
> + */
> +int super_iter_inodes(struct super_block *sb, struct inode_iter_context *ctx)

This one makes a lot more sense, but the callback pattern is a bit
annoying as it requires to stuff all information into the context.
Im much of the core kernel we're slowly switching to an iterator
model that allows the caller to keep the state.  In this case something
like

	struct inode_iter_contex ctx = INODE_ITER_CTX_INIT;

	while ((inode = sb_inode_iter()))) {
		/* do stuff */
	}

which should be easily possibly with this model.

You probably also want to split the core iterator and switching each
user into separate patches.

^ permalink raw reply	[flat|nested] 5+ messages in thread

end of thread, other threads:[~2026-11-13 12:23 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-11-13 11:56 [PATCH v2 0/3] vfs: add inode iteration methods Huang Shijie
2026-11-13 11:57 ` [PATCH v2 1/3] " Huang Shijie
2026-11-13 12:23   ` Christoph Hellwig
2026-11-13 11:57 ` [PATCH v2 2/3] vfs: convert inode iterators to super_iter_inodes_unsafe() Huang Shijie
2026-11-13 11:57 ` [PATCH v2 3/3] vfs: convert sb->s_inodes iteration to super_iter_inodes() Huang Shijie

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox