[PATCH RESEND v2 03/23] proc: share the readdir cursor with sysctl

From: Christian Brauner

Date: Wed Nov 04 2026 - 15:34:05 EST


proc_readdir_de() and proc_sys_readdir() both resume a listing after
the last entry it emitted and each remembers that entry in the open
file with a cursor of its own: the same struct, the same way of
setting it, the same release. Keep one in generic.c and let sysctl
use it.

No functional changes.

Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
---
fs/proc/generic.c | 29 ++++++++++++-----------------
fs/proc/internal.h | 25 +++++++++++++++++++++++++
fs/proc/proc_sysctl.c | 45 +++++----------------------------------------
3 files changed, 42 insertions(+), 57 deletions(-)

diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index e7264f0cf5ab..f6d620a5c3d2 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -301,22 +301,16 @@ struct dentry *proc_lookup(struct inode *dir, struct dentry *dentry,
}

/*
- * Where the next readdir resumes. The entries are counted, so one that is
- * added or removed in front of the position would shift what the next call
- * returns. The name of the last entry emitted doesn't move.
+ * Remember @name as the entry the listing is behind at @pos. If this can't
+ * the next call counts from the start.
*/
-struct proc_dir_cursor {
- loff_t pos; /* the position behind the entry */
- unsigned int len;
- char name[NAME_MAX + 1];
-};
-
-/* If this can't remember the entry the next call counts from the start. */
-static void proc_dir_cursor_set(struct file *file, struct proc_dir_entry *de,
- loff_t pos)
+void proc_dir_cursor_set(struct file *file, const char *name, unsigned int len,
+ loff_t pos)
{
struct proc_dir_cursor *cursor = file->private_data;

+ if (len > NAME_MAX)
+ return;
if (!cursor) {
cursor = kmalloc(sizeof(*cursor), GFP_KERNEL);
if (!cursor)
@@ -324,8 +318,8 @@ static void proc_dir_cursor_set(struct file *file, struct proc_dir_entry *de,
file->private_data = cursor;
}
cursor->pos = pos;
- cursor->len = de->namelen;
- memcpy(cursor->name, de->name, de->namelen);
+ cursor->len = len;
+ memcpy(cursor->name, name, len);
}

int proc_dir_release(struct inode *inode, struct file *file)
@@ -346,7 +340,7 @@ int proc_dir_release(struct inode *inode, struct file *file)
int proc_readdir_de(struct file *file, struct dir_context *ctx,
struct proc_dir_entry *de)
{
- struct proc_dir_cursor *cursor = file->private_data;
+ const struct proc_dir_cursor *cursor;
struct proc_dir_entry *dir = de;
int i;

@@ -354,7 +348,8 @@ int proc_readdir_de(struct file *file, struct dir_context *ctx,
return 0;

read_lock(&proc_subdir_lock);
- if (cursor && cursor->pos == ctx->pos) {
+ cursor = proc_dir_cursor_at(file, ctx->pos);
+ if (cursor) {
/* Resume after the last entry emitted, wherever it is now. */
de = pde_subdir_after(dir, cursor->name, cursor->len);
} else {
@@ -378,7 +373,7 @@ int proc_readdir_de(struct file *file, struct dir_context *ctx,
return 0;
}
ctx->pos++;
- proc_dir_cursor_set(file, de, ctx->pos);
+ proc_dir_cursor_set(file, de->name, de->namelen, ctx->pos);
read_lock(&proc_subdir_lock);
/* Removed in the meantime, go on after its name. */
if (RB_EMPTY_NODE(&de->subdir_node))
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index 268a0523be12..6b61d09ac5e5 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -278,6 +278,31 @@ extern struct dentry *proc_lookup(struct inode *, struct dentry *, unsigned int)
struct dentry *proc_lookup_de(struct inode *, struct dentry *, struct proc_dir_entry *);
extern int proc_readdir(struct file *, struct dir_context *);
int proc_readdir_de(struct file *, struct dir_context *, struct proc_dir_entry *);
+
+/*
+ * Where the next readdir resumes. The entries are counted, so one that is
+ * added or removed in front of the position would shift what the next call
+ * returns. The name of the last entry emitted doesn't move. The directory's
+ * file keeps it, see proc_dir_release().
+ */
+struct proc_dir_cursor {
+ loff_t pos; /* the position behind the entry */
+ unsigned int len;
+ char name[NAME_MAX + 1];
+};
+
+/* The entry to resume after, if the position is still the one behind it. */
+static inline const struct proc_dir_cursor *
+proc_dir_cursor_at(struct file *file, loff_t pos)
+{
+ const struct proc_dir_cursor *cursor = file->private_data;
+
+ if (cursor && cursor->pos == pos)
+ return cursor;
+ return NULL;
+}
+void proc_dir_cursor_set(struct file *file, const char *name, unsigned int len,
+ loff_t pos);
int proc_dir_release(struct inode *, struct file *);

static inline void pde_get(struct proc_dir_entry *pde)
diff --git a/fs/proc/proc_sysctl.c b/fs/proc/proc_sysctl.c
index 6a92c33db9b5..c1e41a3f8f6b 100644
--- a/fs/proc/proc_sysctl.c
+++ b/fs/proc/proc_sysctl.c
@@ -787,36 +787,6 @@ out:
return ret;
}

-/*
- * Where the next readdir resumes. The entries are counted, so one that is
- * registered or unregistered in front of the position would shift what
- * the next call returns. The name of the last entry emitted doesn't move.
- */
-struct sysctl_cursor {
- loff_t pos; /* the position behind the entry */
- int len;
- char name[NAME_MAX + 1];
-};
-
-/* If this can't remember the entry the next call counts from the start. */
-static void sysctl_cursor_set(struct file *file, const char *name, int len,
- loff_t pos)
-{
- struct sysctl_cursor *cursor = file->private_data;
-
- if (len > NAME_MAX)
- return;
- if (!cursor) {
- cursor = kmalloc(sizeof(*cursor), GFP_KERNEL);
- if (!cursor)
- return;
- file->private_data = cursor;
- }
- cursor->pos = pos;
- cursor->len = len;
- memcpy(cursor->name, name, len);
-}
-
/*
* The inode number and type of the entry @d holds, instantiating its dentry
* if there is none. Needs the header pinned, proc_sys_make_inode() checks
@@ -871,7 +841,7 @@ static int proc_sys_fill_dirent(struct file *file, struct sysctl_dirent *d)
static int proc_sys_readdir(struct file *file, struct dir_context *ctx)
{
struct ctl_table_header *head = PROC_I(file_inode(file))->sysctl;
- struct sysctl_cursor *cursor = file->private_data;
+ const struct proc_dir_cursor *cursor;
struct sysctl_dirent d = {};
struct ctl_dir *ctl_dir;
int err;
@@ -888,7 +858,8 @@ static int proc_sys_readdir(struct file *file, struct dir_context *ctx)
* that moved the position away from it counts from the start, like
* seeking to an arbitrary position always did.
*/
- if (cursor && cursor->pos == ctx->pos) {
+ cursor = proc_dir_cursor_at(file, ctx->pos);
+ if (cursor) {
err = sysctl_dir_next(ctl_dir, &d, cursor->name, cursor->len);
} else {
loff_t pos = 2;
@@ -910,19 +881,13 @@ static int proc_sys_readdir(struct file *file, struct dir_context *ctx)
break;
}
ctx->pos++;
- sysctl_cursor_set(file, d.name, d.len, ctx->pos);
+ proc_dir_cursor_set(file, d.name, d.len, ctx->pos);
err = sysctl_dir_next(ctl_dir, &d, d.name, d.len);
}
sysctl_dir_done(&d);
return err;
}

-static int proc_sys_dir_release(struct inode *inode, struct file *file)
-{
- kfree(file->private_data);
- return 0;
-}
-
static int proc_sys_permission(const struct mnt_idmap *idmap,
struct inode *inode, int mask)
{
@@ -1002,7 +967,7 @@ static const struct file_operations proc_sys_dir_file_operations = {
.read = generic_read_dir,
.iterate_shared = proc_sys_readdir,
.llseek = generic_file_llseek,
- .release = proc_sys_dir_release,
+ .release = proc_dir_release,
};

static const struct inode_operations proc_sys_inode_operations = {

--
2.53.0