diff --git a/fs/proc/proc_sysctl.c b/fs/proc/proc_sysctl.c
index 6c0859c66563..38b61887b59c 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 @@ static __poll_t proc_sys_poll(struct file *filp, poll_table *wait)
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,@@ -753,8 +831,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;
}@@ -762,6 +843,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;@@ -775,9 +857,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;@@ -788,6 +881,12 @@ static int proc_sys_readdir(struct file *file, struct dir_context *ctx)
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)
{@@ -867,6 +966,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