From: Christian Brauner <brauner@kernel.org>
To: linux-fsdevel@vger.kernel.org
Cc: Linus Torvalds <torvalds@linux-foundation.org>,
Jann Horn <jannh@google.com>, Josef Bacik <josef@toxicpanda.com>,
Chris Mason <mason@kernel.org>, Jeff Layton <jlayton@kernel.org>,
NeilBrown <neil@brown.name>, Eric Dumazet <edumazet@kernel.org>,
Thomas Gleixner <tglx@kernel.org>,
Al Viro <viro@zeniv.linux.org.uk>,
Jonathan Corbet <corbet@lwn.net>, Christoph Hellwig <hch@lst.de>,
Greg Kroah-Hartman <gregkh@suse.de>,
Joel Granados <joel.granados@kernel.org>,
Kees Cook <kees@kernel.org>, Jan Kara <jack@suse.cz>,
Jan Kara <jack@suse.com>,
linux-kernel@vger.kernel.org, linux-nfs@vger.kernel.org,
linux-acpi@vger.kernel.org, linux-kselftest@vger.kernel.org,
linux-s390@vger.kernel.org, linux-scsi@vger.kernel.org,
linux-um@lists.infradead.org, linux-wireless@vger.kernel.org,
linuxppc-dev@lists.ozlabs.org, netdev@vger.kernel.org,
ntfs3@lists.linux.dev, platform-driver-x86@vger.kernel.org,
coreteam@netfilter.org, kexec@lists.infradead.org,
linux-pci@vger.kernel.org, linux-sound@vger.kernel.org,
netfilter-devel@vger.kernel.org,
dri-devel@lists.freedesktop.org,
ibm-acpi-devel@lists.sourceforge.net,
jfs-discussion@lists.sourceforge.net,
linux-alpha@vger.kernel.org,
linux-arm-kernel@lists.infradead.org,
linux-cifs@vger.kernel.org, linux-fbdev@vger.kernel.org,
linux-hwmon@vger.kernel.org, linux-mm@kvack.org,
linux-sh@vger.kernel.org, linux-usb@vger.kernel.org,
samba-technical@lists.samba.org, sparclinux@vger.kernel.org,
linux-m68k@lists.linux-m68k.org, linux-doc@vger.kernel.org,
linux-serial@vger.kernel.org,
linux-atm-general@lists.sourceforge.net,
Takashi Iwai <tiwai@suse.de>,
linux-input@vger.kernel.org, linux-raid@vger.kernel.org,
linux-ext4@vger.kernel.org, linux-modules@vger.kernel.org,
linux-debuggers@vger.kernel.org,
"Christian Brauner (Amutable)" <brauner@kernel.org>,
Chuck Lever <cel@kernel.org>,
stable@vger.kernel.org
Subject: [PATCH RFC 19/38] proc: resume readdir after the last name it returned
Date: Mon, 02 Nov 2026 15:21:34 +0100 [thread overview]
Message-ID: <20261102-work-procfs-bugfixes-v1-19-59422ed3e465@kernel.org> (raw)
In-Reply-To: <20261102-work-procfs-bugfixes-v1-0-59422ed3e465@kernel.org>
proc_readdir_de() 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 added or removed in between shifts everything
behind it by one. Listing /proc/net/dev_snmp6 with a buffer that holds
".", ".." and "lo" and adding a device whose name sorts before "lo"
makes the next call return "lo" a second time. Removing one that sorted
before the position makes the next call skip an entry instead.
Since commit 895b4c0c79b0 ("fs/proc: fix uaf in proc_readdir_de()") an
entry that is removed while it is being emitted also ends the listing
right there. pde_subdir_next() finds nothing after an erased entry, so
proc_readdir_de() reports the end of the directory and the next call
resumes by number, skipping another entry. For /proc itself
proc_root_readdir() takes that as the end of the static entries and
moves on to the pids, so the rest of the static entries is missing.
And if the position is behind the last entry when a call starts
proc_readdir_de() returns 0, so /proc then lists no pids at all.
So remember the name of the last entry a call emitted in the file and
resume at the first entry after it, like kernfs resumes after the node
it stopped at. An entry that was removed while it was emitted is
continued after by its name as well. If the position was moved by a
seek in between the listing counts from the start like before.
Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2")
Fixes: 895b4c0c79b0 ("fs/proc: fix uaf in proc_readdir_de()")
Cc: stable@vger.kernel.org
Signed-off-by: Christian Brauner (Amutable) <brauner@kernel.org>
---
fs/proc/generic.c | 89 ++++++++++++++++++++++++++++++++++++++++++++++--------
fs/proc/internal.h | 1 +
fs/proc/proc_net.c | 1 +
fs/proc/root.c | 1 +
4 files changed, 80 insertions(+), 12 deletions(-)
diff --git a/fs/proc/generic.c b/fs/proc/generic.c
index ef804cd3ec45..47830d6af255 100644
--- a/fs/proc/generic.c
+++ b/fs/proc/generic.c
@@ -87,6 +87,28 @@ static struct proc_dir_entry *pde_subdir_find(struct proc_dir_entry *dir,
return NULL;
}
+/* The first entry of @dir that sorts after @name. */
+static struct proc_dir_entry *pde_subdir_after(struct proc_dir_entry *dir,
+ const char *name,
+ unsigned int len)
+{
+ struct rb_node *node = dir->subdir.rb_node, *after = NULL;
+
+ while (node) {
+ struct proc_dir_entry *de = rb_entry(node,
+ struct proc_dir_entry,
+ subdir_node);
+
+ if (proc_match(name, de, len) < 0) {
+ after = node;
+ node = node->rb_left;
+ } else {
+ node = node->rb_right;
+ }
+ }
+ return rb_entry_safe(after, struct proc_dir_entry, subdir_node);
+}
+
static bool pde_subdir_insert(struct proc_dir_entry *dir,
struct proc_dir_entry *de)
{
@@ -277,6 +299,40 @@ struct dentry *proc_lookup(struct inode *dir, struct dentry *dentry,
return proc_lookup_de(dir, dentry, PDE(dir));
}
+/*
+ * 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.
+ */
+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)
+{
+ struct proc_dir_cursor *cursor = file->private_data;
+
+ if (!cursor) {
+ cursor = kmalloc(sizeof(*cursor), GFP_KERNEL);
+ if (!cursor)
+ return;
+ file->private_data = cursor;
+ }
+ cursor->pos = pos;
+ cursor->len = de->namelen;
+ memcpy(cursor->name, de->name, de->namelen);
+}
+
+int proc_dir_release(struct inode *inode, struct file *file)
+{
+ kfree(file->private_data);
+ return 0;
+}
+
/*
* This returns non-zero if at EOF, so that the /proc
* root directory can use this and check if it should
@@ -289,23 +345,26 @@ struct dentry *proc_lookup(struct inode *dir, struct dentry *dentry,
int proc_readdir_de(struct file *file, struct dir_context *ctx,
struct proc_dir_entry *de)
{
+ struct proc_dir_cursor *cursor = file->private_data;
+ struct proc_dir_entry *dir = de;
int i;
if (!dir_emit_dots(file, ctx))
return 0;
- i = ctx->pos - 2;
read_lock(&proc_subdir_lock);
- de = pde_subdir_first(de);
- for (;;) {
- if (!de) {
- read_unlock(&proc_subdir_lock);
- return 0;
- }
- if (!i)
- break;
- de = pde_subdir_next(de);
- i--;
+ if (cursor && cursor->pos == ctx->pos) {
+ /* Resume after the last entry emitted, wherever it is now. */
+ de = pde_subdir_after(dir, cursor->name, cursor->len);
+ } else {
+ /* Moved by a seek, count from the start like always. */
+ de = pde_subdir_first(dir);
+ for (i = ctx->pos - 2; de && i; i--)
+ de = pde_subdir_next(de);
+ }
+ if (!de) {
+ read_unlock(&proc_subdir_lock);
+ return 1;
}
do {
@@ -318,8 +377,13 @@ int proc_readdir_de(struct file *file, struct dir_context *ctx,
return 0;
}
ctx->pos++;
+ proc_dir_cursor_set(file, de, ctx->pos);
read_lock(&proc_subdir_lock);
- next = pde_subdir_next(de);
+ /* Removed in the meantime, go on after its name. */
+ if (RB_EMPTY_NODE(&de->subdir_node))
+ next = pde_subdir_after(dir, de->name, de->namelen);
+ else
+ next = pde_subdir_next(de);
pde_put(de);
de = next;
} while (de);
@@ -347,6 +411,7 @@ static const struct file_operations proc_dir_operations = {
.llseek = generic_file_llseek,
.read = generic_read_dir,
.iterate_shared = proc_readdir,
+ .release = proc_dir_release,
};
static int proc_net_d_revalidate(struct inode *dir, const struct qstr *name,
diff --git a/fs/proc/internal.h b/fs/proc/internal.h
index df873df9afc5..04f7ce2b24ed 100644
--- a/fs/proc/internal.h
+++ b/fs/proc/internal.h
@@ -287,6 +287,7 @@ 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 *);
+int proc_dir_release(struct inode *, struct file *);
static inline void pde_get(struct proc_dir_entry *pde)
{
diff --git a/fs/proc/proc_net.c b/fs/proc/proc_net.c
index b1f5eafb069a..dbcf87a257e1 100644
--- a/fs/proc/proc_net.c
+++ b/fs/proc/proc_net.c
@@ -351,6 +351,7 @@ const struct file_operations proc_net_operations = {
.llseek = generic_file_llseek,
.read = generic_read_dir,
.iterate_shared = proc_tgid_net_readdir,
+ .release = proc_dir_release,
};
static __net_init int proc_net_ns_init(struct net *net)
diff --git a/fs/proc/root.c b/fs/proc/root.c
index 7fbbe92bf73a..dfee1caa26f7 100644
--- a/fs/proc/root.c
+++ b/fs/proc/root.c
@@ -441,6 +441,7 @@ static const struct file_operations proc_root_operations = {
.read = generic_read_dir,
.iterate_shared = proc_root_readdir,
.llseek = generic_file_llseek,
+ .release = proc_dir_release,
};
/*
--
2.53.0
next prev parent reply other threads:[~2026-11-02 14:26 UTC|newest]
Thread overview: 50+ messages / expand[flat|nested] mbox.gz Atom feed top
[not found] <20261102-defizit-jobaussicht-kurios-7016c6e67442@brauner>
2026-11-02 14:21 ` [PATCH RFC 00/38] proc: fix a few old bugs in /proc handlers Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 01/38] PCI/proc: don't truncate the position of config space accesses Christian Brauner
2026-11-02 17:58 ` Linus Torvalds
2026-11-04 9:17 ` Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 02/38] PNP: isapnp: don't read past the end of the buffer on a large pread() Christian Brauner
2026-11-02 14:51 ` Rafael J. Wysocki (Intel)
2026-11-02 14:21 ` [PATCH RFC 03/38] netfilter: xt_recent: don't remove the proc entries under a spinlock Christian Brauner
2026-11-02 14:41 ` Florian Westphal
2026-11-04 9:15 ` Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 04/38] s390/dasd: don't look in front of the buffer on an empty write Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 05/38] platform/x86: toshiba_acpi: reject malformed brightness and fan writes Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 06/38] xtensa: simdisk: don't read the file name without the lock Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 07/38] powerpc/rtas_flash: don't read past the validate message Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 08/38] powerpc/rtasd: zero the error log buffer Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 09/38] ALSA: info: don't write past the end of a data entry Christian Brauner
2026-11-03 10:41 ` Takashi Iwai
2026-11-02 14:21 ` [PATCH RFC 13/38] scsi: core: NUL-terminate what is written to the host proc files Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 14/38] um: NUL-terminate what is written to /proc/exitcode Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 15/38] ntfs3: don't warn when a huge label is written Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 16/38] proc: don't walk through a non-directory when resolving a name Christian Brauner
2026-11-05 11:41 ` Jan Kara
2026-11-02 14:21 ` [PATCH RFC 17/38] net: pktgen: reject device names with a '/' Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 18/38] proc: don't WARN about a name that is already registered Christian Brauner
2026-11-04 18:43 ` Alexey Dobriyan
2026-11-05 21:31 ` Christian Brauner
2026-11-05 11:52 ` Jan Kara
2026-11-02 14:21 ` Christian Brauner [this message]
2026-11-05 12:00 ` [PATCH RFC 19/38] proc: resume readdir after the last name it returned Jan Kara
2026-11-05 16:44 ` Jann Horn
2026-11-06 14:22 ` Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 23/38] proc: don't let NOMMU map procfs files Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 24/38] proc/vmcore: don't let vmcore_cleanup() run twice at once Christian Brauner
2026-11-05 12:11 ` Jan Kara
2026-11-02 14:21 ` [PATCH RFC 25/38] powerpc/crash_dump: check ioremap_cache() in copy_oldmem_page() Christian Brauner
2026-11-11 11:54 ` Breno Leitao
2026-11-02 14:21 ` [PATCH RFC 26/38] powerpc/crash_dump: recover from a machine check " Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 27/38] sunrpc: make queue_wait a global wait queue again Christian Brauner
2026-11-02 15:12 ` Chuck Lever
2026-11-03 16:26 ` Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 28/38] scsi: esas2r: bound the lengths of the HBA, SMP and firmware ioctls Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 29/38] powerpc/pseries: bound the lengths in the ofdt property parser Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 30/38] powerpc/pseries/lparcfg: return h_get_ppp()'s status as a long Christian Brauner
2026-11-02 14:21 ` [PATCH RFC 31/38] proc: sysctl: resume readdir after the last name it returned Christian Brauner
2026-11-05 12:16 ` Jan Kara
2026-11-02 14:21 ` [PATCH RFC 33/38] proc: sysctl: copy to and from userspace without holding the table Christian Brauner
2026-11-05 12:21 ` Jan Kara
2026-11-02 14:21 ` [PATCH RFC 35/38] proc: sysctl: don't hold the tables while readdir copies to userspace Christian Brauner
2026-11-05 12:55 ` Jan Kara
2026-11-02 14:43 ` [PATCH RFC 37/38] proc: don't pin a symlink's entry while readlink() copies it out Christian Brauner
2026-11-05 12:36 ` Jan Kara
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261102-work-procfs-bugfixes-v1-19-59422ed3e465@kernel.org \
--to=brauner@kernel.org \
--cc=cel@kernel.org \
--cc=corbet@lwn.net \
--cc=coreteam@netfilter.org \
--cc=dri-devel@lists.freedesktop.org \
--cc=edumazet@kernel.org \
--cc=gregkh@suse.de \
--cc=hch@lst.de \
--cc=ibm-acpi-devel@lists.sourceforge.net \
--cc=jack@suse.com \
--cc=jack@suse.cz \
--cc=jannh@google.com \
--cc=jfs-discussion@lists.sourceforge.net \
--cc=jlayton@kernel.org \
--cc=joel.granados@kernel.org \
--cc=josef@toxicpanda.com \
--cc=kees@kernel.org \
--cc=kexec@lists.infradead.org \
--cc=linux-acpi@vger.kernel.org \
--cc=linux-alpha@vger.kernel.org \
--cc=linux-arm-kernel@lists.infradead.org \
--cc=linux-atm-general@lists.sourceforge.net \
--cc=linux-cifs@vger.kernel.org \
--cc=linux-debuggers@vger.kernel.org \
--cc=linux-doc@vger.kernel.org \
--cc=linux-ext4@vger.kernel.org \
--cc=linux-fbdev@vger.kernel.org \
--cc=linux-fsdevel@vger.kernel.org \
--cc=linux-hwmon@vger.kernel.org \
--cc=linux-input@vger.kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=linux-m68k@lists.linux-m68k.org \
--cc=linux-mm@kvack.org \
--cc=linux-modules@vger.kernel.org \
--cc=linux-nfs@vger.kernel.org \
--cc=linux-pci@vger.kernel.org \
--cc=linux-raid@vger.kernel.org \
--cc=linux-s390@vger.kernel.org \
--cc=linux-scsi@vger.kernel.org \
--cc=linux-serial@vger.kernel.org \
--cc=linux-sh@vger.kernel.org \
--cc=linux-sound@vger.kernel.org \
--cc=linux-um@lists.infradead.org \
--cc=linux-usb@vger.kernel.org \
--cc=linux-wireless@vger.kernel.org \
--cc=linuxppc-dev@lists.ozlabs.org \
--cc=mason@kernel.org \
--cc=neil@brown.name \
--cc=netdev@vger.kernel.org \
--cc=netfilter-devel@vger.kernel.org \
--cc=ntfs3@lists.linux.dev \
--cc=platform-driver-x86@vger.kernel.org \
--cc=samba-technical@lists.samba.org \
--cc=sparclinux@vger.kernel.org \
--cc=stable@vger.kernel.org \
--cc=tglx@kernel.org \
--cc=tiwai@suse.de \
--cc=torvalds@linux-foundation.org \
--cc=viro@zeniv.linux.org.uk \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox