[PATCH RESEND v2 44/51] proc: sysctl: resume readdir after the last name it returned

From: Christian Brauner

Date: Wed Nov 04 2026 - 15:45:25 EST


proc_sys_readdir() counts the entries of a directory and resumes a
listing at the number getdents64() stopped at. The entries are sorted
by name, so one that is registered or unregistered in between shifts
everything behind it by one. Listing /proc/sys/net/ipv4/conf with a
buffer that holds ".", ".." and "all" and registering a device whose
name sorts before "all" makes the next call return "all" a second time.
Unregistering one that sorted before the position makes it skip an
entry instead. So a listing that takes more than one call shows a name
twice, or not at all. Even the 64k buffer of ls needs a second call to
find the end of the directory, so a device that comes up right then
repeats the last entry.

So remember the name of the last entry a call emitted in the file and
resume at the first usable entry after it, like kernfs resumes after
the node it stopped at. The entry itself can't be pinned across calls,
unregistering waits for that. If the position was moved by a seek in
between the listing counts from the start like before, seeking to an
arbitrary position has never been better than that.

That also takes one walk down the tree to resume instead of a walk over
every entry in front of the position with a round trip through
sysctl_lock for each of them.

Fixes: 77b14db502cb ("[PATCH] sysctl: reimplement the sysctl proc support")
Cc: stable@xxxxxxxxxxxxxxx
Signed-off-by: Christian Brauner (Amutable) <brauner@xxxxxxxxxx>
---
fs/proc/proc_sysctl.c | 106 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 103 insertions(+), 3 deletions(-)

diff --git a/fs/proc/proc_sysctl.c b/fs/proc/proc_sysctl.c
index b4a309d49698..ba52f13eb8f3 100644
--- a/fs/proc/proc_sysctl.c
+++ b/fs/proc/proc_sysctl.c
@@ -417,6 +417,54 @@ static void next_entry(struct ctl_table_header **phead, const struct ctl_table *
*pentry = entry;
}

+/*
+ * The first usable entry of @dir after @name, or the first one at all for a
+ * NULL @name. The entries are sorted by name, so that is where a listing
+ * resumes when entries in front of it came or went in between. -ENOENT if
+ * the directory is being unregistered.
+ */
+static int entry_after(struct ctl_dir *dir, const char *name, int namelen,
+ struct ctl_table_header **phead,
+ const struct ctl_table **pentry)
+{
+ struct ctl_table_header *head = NULL;
+ const struct ctl_table *entry = NULL;
+ struct rb_node *node, *after = NULL;
+ struct ctl_node *ctl_node;
+
+ spin_lock(&sysctl_lock);
+ if (unlikely(dir->header.unregistering)) {
+ spin_unlock(&sysctl_lock);
+ return -ENOENT;
+ }
+ if (!name) {
+ after = rb_first(&dir->root);
+ } else for (node = dir->root.rb_node; node;) {
+ const struct ctl_table *table;
+ struct ctl_table_header *h;
+
+ ctl_node = rb_entry(node, struct ctl_node, node);
+ h = ctl_node->header;
+ table = &h->ctl_table[ctl_node - h->node];
+ if (namecmp(table->procname, strlen(table->procname),
+ name, namelen) > 0) {
+ after = node;
+ node = node->rb_left;
+ } else {
+ node = node->rb_right;
+ }
+ }
+ ctl_node = first_usable_entry(after);
+ spin_unlock(&sysctl_lock);
+ if (ctl_node) {
+ head = ctl_node->header;
+ entry = &head->ctl_table[ctl_node - head->node];
+ }
+ *phead = head;
+ *pentry = entry;
+ return 0;
+}
+
/*
* sysctl_perm does NOT grant the superuser all rights automatically, because
* some sysctl variables are readonly even to root.
@@ -675,6 +723,36 @@ 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);
+}
+
static bool proc_sys_fill_cache(struct file *file,
struct dir_context *ctx,
struct ctl_table_header *head,
@@ -756,8 +834,11 @@ static int scan(struct ctl_table_header *head, const struct ctl_table *table,
else
res = proc_sys_fill_cache(file, ctx, head, table);

- if (res)
+ if (res) {
ctx->pos = *pos;
+ sysctl_cursor_set(file, table->procname, strlen(table->procname),
+ ctx->pos);
+ }

return res;
}
@@ -765,6 +846,7 @@ static int scan(struct ctl_table_header *head, const struct ctl_table *table,
static int proc_sys_readdir(struct file *file, struct dir_context *ctx)
{
struct ctl_table_header *head = grab_header(file_inode(file));
+ struct sysctl_cursor *cursor = file->private_data;
struct ctl_table_header *h = NULL;
const struct ctl_table *entry;
struct ctl_dir *ctl_dir;
@@ -778,9 +860,20 @@ static int proc_sys_readdir(struct file *file, struct dir_context *ctx)
if (!dir_emit_dots(file, ctx))
goto out;

- pos = 2;
+ /*
+ * Resume after the last entry the previous call emitted. Only a seek
+ * 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) {
+ entry_after(ctl_dir, cursor->name, cursor->len, &h, &entry);
+ pos = ctx->pos;
+ } else {
+ first_entry(ctl_dir, &h, &entry);
+ pos = 2;
+ }

- for (first_entry(ctl_dir, &h, &entry); h; next_entry(&h, &entry)) {
+ for (; h; next_entry(&h, &entry)) {
if (!scan(h, entry, &pos, file, ctx)) {
sysctl_head_finish(h);
break;
@@ -791,6 +884,12 @@ out:
return 0;
}

+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)
{
@@ -870,6 +969,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,
};

static const struct inode_operations proc_sys_inode_operations = {

--
2.53.0