linux-mm.kvack.org archive mirror
 help / color / mirror / Atom feed
* [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior
@ 2024-11-27 15:28 cel
  2024-11-27 15:28 ` [RFC PATCH v3 1/5] libfs: Return ENOSPC when the directory offset range is exhausted cel
                   ` (4 more replies)
  0 siblings, 5 replies; 6+ messages in thread
From: cel @ 2024-11-27 15:28 UTC (permalink / raw)
  To: Hugh Dickens, Christian Brauner, Al Viro
  Cc: linux-fsdevel, linux-mm, yukuai3, yangerkun, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

The purpose of this series is construct a set of upstream fixes that
can be backported to v6.6 to address CVE-2024-46701.

The v3 series updates yesterday's v2. Some bugs and review comments
have been addressed, and the rationale for reverting 64a7ce76fb90
("libfs: fix infinite directory reads for offset dir") has been
clarified.

v3 passes xfstests except for generic/637.

The series has been pushed to:

https://git.kernel.org/pub/scm/linux/kernel/git/cel/linux.git/log/?h=tmpfs-fixes

Chuck Lever (5):
  libfs: Return ENOSPC when the directory offset range is exhausted
  libfs: Remove unnecessary locking from simple_offset_empty()
  Revert "libfs: fix infinite directory reads for offset dir"
  libfs: Refactor end-of-directory detection for simple_offset
    directories
  libfs: Refactor offset_iterate_dir()

 fs/libfs.c | 145 +++++++++++++++++++++++++++++++++++++++--------------
 1 file changed, 107 insertions(+), 38 deletions(-)

-- 
2.47.0



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

* [RFC PATCH v3 1/5] libfs: Return ENOSPC when the directory offset range is exhausted
  2024-11-27 15:28 [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior cel
@ 2024-11-27 15:28 ` cel
  2024-11-27 15:28 ` [RFC PATCH v3 2/5] libfs: Remove unnecessary locking from simple_offset_empty() cel
                   ` (3 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: cel @ 2024-11-27 15:28 UTC (permalink / raw)
  To: Hugh Dickens, Christian Brauner, Al Viro
  Cc: linux-fsdevel, linux-mm, yukuai3, yangerkun, Chuck Lever, stable,
	Jeff Layton, Yang Erkun

From: Chuck Lever <chuck.lever@oracle.com>

Testing shows that the EBUSY error return from mtree_alloc_cyclic()
leaks into user space. The ERRORS section of "man creat(2)" says:

>	EBUSY	O_EXCL was specified in flags and pathname refers
>		to a block device that is in use by the system
>		(e.g., it is mounted).

ENOSPC is closer to what applications expect in this situation.

Note that the normal range of simple directory offset values is
2..2^63, so hitting this error is going to be rare to impossible.

Fixes: 6faddda69f62 ("libfs: Add directory operations for stable offsets")
Cc: <stable@vger.kernel.org> # v6.9+
Reviewed-by: Jeff Layton <jlayton@kernel.org>
Reviewed-by: Yang Erkun <yangerkun@huawei.com>
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 fs/libfs.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/fs/libfs.c b/fs/libfs.c
index 46966fd8bcf9..bf67954b525b 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -288,7 +288,9 @@ int simple_offset_add(struct offset_ctx *octx, struct dentry *dentry)
 
 	ret = mtree_alloc_cyclic(&octx->mt, &offset, dentry, DIR_OFFSET_MIN,
 				 LONG_MAX, &octx->next_offset, GFP_KERNEL);
-	if (ret < 0)
+	if (unlikely(ret == -EBUSY))
+		return -ENOSPC;
+	if (unlikely(ret < 0))
 		return ret;
 
 	offset_set(dentry, offset);
-- 
2.47.0



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

* [RFC PATCH v3 2/5] libfs: Remove unnecessary locking from simple_offset_empty()
  2024-11-27 15:28 [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior cel
  2024-11-27 15:28 ` [RFC PATCH v3 1/5] libfs: Return ENOSPC when the directory offset range is exhausted cel
@ 2024-11-27 15:28 ` cel
  2024-11-27 15:28 ` [RFC PATCH v3 3/5] Revert "libfs: fix infinite directory reads for offset dir" cel
                   ` (2 subsequent siblings)
  4 siblings, 0 replies; 6+ messages in thread
From: cel @ 2024-11-27 15:28 UTC (permalink / raw)
  To: Hugh Dickens, Christian Brauner, Al Viro
  Cc: linux-fsdevel, linux-mm, yukuai3, yangerkun, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

I hit the DEBUG_LOCKS_WARN_ON() in hlock_class() several times
during testing. This indicates that simple_offset_empty() is
attempting to lock a negative dentry. That warning is of course
silent on a kernel that is built without lock debugging.

The simple_positive() check can be done without holding the
child's d_lock.

Fixes: ecba88a3b32d ("libfs: Add simple_offset_empty()")
Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 fs/libfs.c | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/fs/libfs.c b/fs/libfs.c
index bf67954b525b..f686336489a3 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -347,13 +347,10 @@ int simple_offset_empty(struct dentry *dentry)
 	index = DIR_OFFSET_MIN;
 	octx = inode->i_op->get_offset_ctx(inode);
 	mt_for_each(&octx->mt, child, index, LONG_MAX) {
-		spin_lock(&child->d_lock);
 		if (simple_positive(child)) {
-			spin_unlock(&child->d_lock);
 			ret = 0;
 			break;
 		}
-		spin_unlock(&child->d_lock);
 	}
 
 	return ret;
-- 
2.47.0



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

* [RFC PATCH v3 3/5] Revert "libfs: fix infinite directory reads for offset dir"
  2024-11-27 15:28 [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior cel
  2024-11-27 15:28 ` [RFC PATCH v3 1/5] libfs: Return ENOSPC when the directory offset range is exhausted cel
  2024-11-27 15:28 ` [RFC PATCH v3 2/5] libfs: Remove unnecessary locking from simple_offset_empty() cel
@ 2024-11-27 15:28 ` cel
  2024-11-27 15:28 ` [RFC PATCH v3 4/5] libfs: Refactor end-of-directory detection for simple_offset directories cel
  2024-11-27 15:28 ` [RFC PATCH v3 5/5] libfs: Refactor offset_iterate_dir() cel
  4 siblings, 0 replies; 6+ messages in thread
From: cel @ 2024-11-27 15:28 UTC (permalink / raw)
  To: Hugh Dickens, Christian Brauner, Al Viro
  Cc: linux-fsdevel, linux-mm, yukuai3, yangerkun, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

Using octx->next_offset to determine the newest entries works only
because the offset value range is 63-bits. If an offset were to
wrap, existing entries are no longer visible to readdir because
offset_readdir() stops listing entries once an entry's offset is
larger than octx->next_offset.

This fix is effective, but it would be better not to use next_offset
at all when iterating a directory. Revert this fix to prepare for
replacing the current offset_readdir() mechanism. Reverting also
makes it easier to apply the replacement code to v6.6.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 fs/libfs.c | 35 +++++++++++------------------------
 1 file changed, 11 insertions(+), 24 deletions(-)

diff --git a/fs/libfs.c b/fs/libfs.c
index f686336489a3..a673427d3416 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -449,14 +449,6 @@ void simple_offset_destroy(struct offset_ctx *octx)
 	mtree_destroy(&octx->mt);
 }
 
-static int offset_dir_open(struct inode *inode, struct file *file)
-{
-	struct offset_ctx *ctx = inode->i_op->get_offset_ctx(inode);
-
-	file->private_data = (void *)ctx->next_offset;
-	return 0;
-}
-
 /**
  * offset_dir_llseek - Advance the read position of a directory descriptor
  * @file: an open directory whose position is to be updated
@@ -470,9 +462,6 @@ static int offset_dir_open(struct inode *inode, struct file *file)
  */
 static loff_t offset_dir_llseek(struct file *file, loff_t offset, int whence)
 {
-	struct inode *inode = file->f_inode;
-	struct offset_ctx *ctx = inode->i_op->get_offset_ctx(inode);
-
 	switch (whence) {
 	case SEEK_CUR:
 		offset += file->f_pos;
@@ -486,8 +475,7 @@ static loff_t offset_dir_llseek(struct file *file, loff_t offset, int whence)
 	}
 
 	/* In this case, ->private_data is protected by f_pos_lock */
-	if (!offset)
-		file->private_data = (void *)ctx->next_offset;
+	file->private_data = NULL;
 	return vfs_setpos(file, offset, LONG_MAX);
 }
 
@@ -518,7 +506,7 @@ static bool offset_dir_emit(struct dir_context *ctx, struct dentry *dentry)
 			  inode->i_ino, fs_umode_to_dtype(inode->i_mode));
 }
 
-static void offset_iterate_dir(struct inode *inode, struct dir_context *ctx, long last_index)
+static void *offset_iterate_dir(struct inode *inode, struct dir_context *ctx)
 {
 	struct offset_ctx *octx = inode->i_op->get_offset_ctx(inode);
 	struct dentry *dentry;
@@ -526,21 +514,17 @@ static void offset_iterate_dir(struct inode *inode, struct dir_context *ctx, lon
 	while (true) {
 		dentry = offset_find_next(octx, ctx->pos);
 		if (!dentry)
-			return;
-
-		if (dentry2offset(dentry) >= last_index) {
-			dput(dentry);
-			return;
-		}
+			return ERR_PTR(-ENOENT);
 
 		if (!offset_dir_emit(ctx, dentry)) {
 			dput(dentry);
-			return;
+			break;
 		}
 
 		ctx->pos = dentry2offset(dentry) + 1;
 		dput(dentry);
 	}
+	return NULL;
 }
 
 /**
@@ -567,19 +551,22 @@ static void offset_iterate_dir(struct inode *inode, struct dir_context *ctx, lon
 static int offset_readdir(struct file *file, struct dir_context *ctx)
 {
 	struct dentry *dir = file->f_path.dentry;
-	long last_index = (long)file->private_data;
 
 	lockdep_assert_held(&d_inode(dir)->i_rwsem);
 
 	if (!dir_emit_dots(file, ctx))
 		return 0;
 
-	offset_iterate_dir(d_inode(dir), ctx, last_index);
+	/* In this case, ->private_data is protected by f_pos_lock */
+	if (ctx->pos == DIR_OFFSET_MIN)
+		file->private_data = NULL;
+	else if (file->private_data == ERR_PTR(-ENOENT))
+		return 0;
+	file->private_data = offset_iterate_dir(d_inode(dir), ctx);
 	return 0;
 }
 
 const struct file_operations simple_offset_dir_operations = {
-	.open		= offset_dir_open,
 	.llseek		= offset_dir_llseek,
 	.iterate_shared	= offset_readdir,
 	.read		= generic_read_dir,
-- 
2.47.0



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

* [RFC PATCH v3 4/5] libfs: Refactor end-of-directory detection for simple_offset directories
  2024-11-27 15:28 [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior cel
                   ` (2 preceding siblings ...)
  2024-11-27 15:28 ` [RFC PATCH v3 3/5] Revert "libfs: fix infinite directory reads for offset dir" cel
@ 2024-11-27 15:28 ` cel
  2024-11-27 15:28 ` [RFC PATCH v3 5/5] libfs: Refactor offset_iterate_dir() cel
  4 siblings, 0 replies; 6+ messages in thread
From: cel @ 2024-11-27 15:28 UTC (permalink / raw)
  To: Hugh Dickens, Christian Brauner, Al Viro
  Cc: linux-fsdevel, linux-mm, yukuai3, yangerkun, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

This mechanism seems have been misunderstood more than once. Make
the code more self-documentary.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 fs/libfs.c | 54 ++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 42 insertions(+), 12 deletions(-)

diff --git a/fs/libfs.c b/fs/libfs.c
index a673427d3416..0deff5390abb 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -449,6 +449,34 @@ void simple_offset_destroy(struct offset_ctx *octx)
 	mtree_destroy(&octx->mt);
 }
 
+static void offset_set_eod(struct file *file)
+{
+	file->private_data = ERR_PTR(-ENOENT);
+}
+
+static void offset_clear_eod(struct file *file)
+{
+	file->private_data = NULL;
+}
+
+static bool offset_at_eod(struct file *file)
+{
+	return file->private_data == ERR_PTR(-ENOENT);
+}
+
+/**
+ * offset_dir_open - Open a directory descriptor
+ * @inode: directory to be opened
+ * @file: struct file to instantiate
+ *
+ * Returns zero on success, or a negative errno value.
+ */
+static int offset_dir_open(struct inode *inode, struct file *file)
+{
+	offset_clear_eod(file);
+	return 0;
+}
+
 /**
  * offset_dir_llseek - Advance the read position of a directory descriptor
  * @file: an open directory whose position is to be updated
@@ -474,8 +502,8 @@ static loff_t offset_dir_llseek(struct file *file, loff_t offset, int whence)
 		return -EINVAL;
 	}
 
-	/* In this case, ->private_data is protected by f_pos_lock */
-	file->private_data = NULL;
+	/* ->private_data is protected by f_pos_lock */
+	offset_clear_eod(file);
 	return vfs_setpos(file, offset, LONG_MAX);
 }
 
@@ -506,15 +534,20 @@ static bool offset_dir_emit(struct dir_context *ctx, struct dentry *dentry)
 			  inode->i_ino, fs_umode_to_dtype(inode->i_mode));
 }
 
-static void *offset_iterate_dir(struct inode *inode, struct dir_context *ctx)
+static void offset_iterate_dir(struct file *file, struct dir_context *ctx)
 {
+	struct dentry *dir = file->f_path.dentry;
+	struct inode *inode = d_inode(dir);
 	struct offset_ctx *octx = inode->i_op->get_offset_ctx(inode);
 	struct dentry *dentry;
 
 	while (true) {
 		dentry = offset_find_next(octx, ctx->pos);
-		if (!dentry)
-			return ERR_PTR(-ENOENT);
+		if (!dentry) {
+			/* ->private_data is protected by f_pos_lock */
+			offset_set_eod(file);
+			return;
+		}
 
 		if (!offset_dir_emit(ctx, dentry)) {
 			dput(dentry);
@@ -524,7 +557,6 @@ static void *offset_iterate_dir(struct inode *inode, struct dir_context *ctx)
 		ctx->pos = dentry2offset(dentry) + 1;
 		dput(dentry);
 	}
-	return NULL;
 }
 
 /**
@@ -557,16 +589,14 @@ static int offset_readdir(struct file *file, struct dir_context *ctx)
 	if (!dir_emit_dots(file, ctx))
 		return 0;
 
-	/* In this case, ->private_data is protected by f_pos_lock */
-	if (ctx->pos == DIR_OFFSET_MIN)
-		file->private_data = NULL;
-	else if (file->private_data == ERR_PTR(-ENOENT))
-		return 0;
-	file->private_data = offset_iterate_dir(d_inode(dir), ctx);
+	/* ->private_data is protected by f_pos_lock */
+	if (!offset_at_eod(file))
+		offset_iterate_dir(file, ctx);
 	return 0;
 }
 
 const struct file_operations simple_offset_dir_operations = {
+	.open		= offset_dir_open,
 	.llseek		= offset_dir_llseek,
 	.iterate_shared	= offset_readdir,
 	.read		= generic_read_dir,
-- 
2.47.0



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

* [RFC PATCH v3 5/5] libfs: Refactor offset_iterate_dir()
  2024-11-27 15:28 [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior cel
                   ` (3 preceding siblings ...)
  2024-11-27 15:28 ` [RFC PATCH v3 4/5] libfs: Refactor end-of-directory detection for simple_offset directories cel
@ 2024-11-27 15:28 ` cel
  4 siblings, 0 replies; 6+ messages in thread
From: cel @ 2024-11-27 15:28 UTC (permalink / raw)
  To: Hugh Dickens, Christian Brauner, Al Viro
  Cc: linux-fsdevel, linux-mm, yukuai3, yangerkun, Chuck Lever

From: Chuck Lever <chuck.lever@oracle.com>

This line in offset_iterate_dir():

		ctx->pos = dentry2offset(dentry) + 1;

assumes that the next child entry has an offset value that is
greater than the current child entry. Since directory offsets are
actually cookies, this heuristic is not always correct.

We have tested the current code with a limited offset range to see
if this is an operational problem. It doesn't seem to be, but doing
a "+ 1" on what is supposed to be an opaque cookie is very likely
wrong and brittle.

Instead of using the mtree to emit entries in the order of their
offset values, use it only to map the initial ctx->pos to a starting
entry. Then use the directory's d_children list, which is already
maintained by the dcache, to find the next child to emit, as the
simple cursor-based implementation still does.

Signed-off-by: Chuck Lever <chuck.lever@oracle.com>
---
 fs/libfs.c | 95 ++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 74 insertions(+), 21 deletions(-)

diff --git a/fs/libfs.c b/fs/libfs.c
index 0deff5390abb..2616421bbe0e 100644
--- a/fs/libfs.c
+++ b/fs/libfs.c
@@ -241,9 +241,9 @@ const struct inode_operations simple_dir_inode_operations = {
 };
 EXPORT_SYMBOL(simple_dir_inode_operations);
 
-/* 0 is '.', 1 is '..', so always start with offset 2 or more */
 enum {
-	DIR_OFFSET_MIN	= 2,
+	DIR_OFFSET_FIRST	= 2,	/* seek to the first real entry */
+	DIR_OFFSET_MIN		= 3,	/* minimum allocated offset value */
 };
 
 static void offset_set(struct dentry *dentry, long offset)
@@ -507,19 +507,53 @@ static loff_t offset_dir_llseek(struct file *file, loff_t offset, int whence)
 	return vfs_setpos(file, offset, LONG_MAX);
 }
 
-static struct dentry *offset_find_next(struct offset_ctx *octx, loff_t offset)
+/* Cf. find_next_child() */
+static struct dentry *find_next_sibling_locked(struct dentry *dentry)
 {
-	MA_STATE(mas, &octx->mt, offset, offset);
+	struct dentry *found = NULL;
+
+	hlist_for_each_entry_from(dentry, d_sib) {
+		if (!simple_positive(dentry))
+			continue;
+		spin_lock_nested(&dentry->d_lock, DENTRY_D_LOCK_NESTED);
+		if (simple_positive(dentry))
+			found = dget_dlock(dentry);
+		spin_unlock(&dentry->d_lock);
+		if (likely(found))
+			break;
+	}
+	return found;
+}
+
+static noinline_for_stack struct dentry *offset_dir_first(struct file *file)
+{
+	struct dentry *parent = file->f_path.dentry;
+	struct dentry *found;
+
+	spin_lock(&parent->d_lock);
+	found = find_next_sibling_locked(d_first_child(parent));
+	spin_unlock(&parent->d_lock);
+	return found;
+}
+
+static noinline_for_stack struct dentry *
+offset_dir_lookup(struct file *file, loff_t offset)
+{
+	struct dentry *parent = file->f_path.dentry;
 	struct dentry *child, *found = NULL;
+	struct inode *inode = d_inode(parent);
+	struct offset_ctx *octx = inode->i_op->get_offset_ctx(inode);
+
+	MA_STATE(mas, &octx->mt, offset, offset);
 
 	rcu_read_lock();
 	child = mas_find(&mas, LONG_MAX);
 	if (!child)
 		goto out;
-	spin_lock(&child->d_lock);
-	if (simple_positive(child))
-		found = dget_dlock(child);
-	spin_unlock(&child->d_lock);
+
+	spin_lock(&parent->d_lock);
+	found = find_next_sibling_locked(child);
+	spin_unlock(&parent->d_lock);
 out:
 	rcu_read_unlock();
 	return found;
@@ -534,29 +568,48 @@ static bool offset_dir_emit(struct dir_context *ctx, struct dentry *dentry)
 			  inode->i_ino, fs_umode_to_dtype(inode->i_mode));
 }
 
+static struct dentry *offset_dir_next(struct dentry *child)
+{
+	struct dentry *parent = child->d_parent;
+	struct dentry *found;
+
+	spin_lock(&parent->d_lock);
+	found = find_next_sibling_locked(d_next_sibling(child));
+	spin_unlock(&parent->d_lock);
+	return found;
+}
+
 static void offset_iterate_dir(struct file *file, struct dir_context *ctx)
 {
-	struct dentry *dir = file->f_path.dentry;
-	struct inode *inode = d_inode(dir);
-	struct offset_ctx *octx = inode->i_op->get_offset_ctx(inode);
-	struct dentry *dentry;
+	struct dentry *dentry, *next = NULL;
+
+	if (ctx->pos == DIR_OFFSET_FIRST)
+		dentry = offset_dir_first(file);
+	else
+		dentry = offset_dir_lookup(file, ctx->pos);
+	if (!dentry) {
+		/* ->private_data is protected by f_pos_lock */
+		offset_set_eod(file);
+		return;
+	}
 
 	while (true) {
-		dentry = offset_find_next(octx, ctx->pos);
-		if (!dentry) {
-			/* ->private_data is protected by f_pos_lock */
-			offset_set_eod(file);
-			return;
-		}
-
 		if (!offset_dir_emit(ctx, dentry)) {
-			dput(dentry);
+			ctx->pos = dentry2offset(dentry);
+			break;
+		}
+
+		next = offset_dir_next(dentry);
+		if (!next) {
+			offset_set_eod(file);
 			break;
 		}
 
-		ctx->pos = dentry2offset(dentry) + 1;
 		dput(dentry);
+		dentry = next;
 	}
+
+	dput(dentry);
 }
 
 /**
-- 
2.47.0



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

end of thread, other threads:[~2024-11-27 15:28 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-11-27 15:28 [RFC PATCH v3 0/5] Improve simple directory offset wrap behavior cel
2024-11-27 15:28 ` [RFC PATCH v3 1/5] libfs: Return ENOSPC when the directory offset range is exhausted cel
2024-11-27 15:28 ` [RFC PATCH v3 2/5] libfs: Remove unnecessary locking from simple_offset_empty() cel
2024-11-27 15:28 ` [RFC PATCH v3 3/5] Revert "libfs: fix infinite directory reads for offset dir" cel
2024-11-27 15:28 ` [RFC PATCH v3 4/5] libfs: Refactor end-of-directory detection for simple_offset directories cel
2024-11-27 15:28 ` [RFC PATCH v3 5/5] libfs: Refactor offset_iterate_dir() cel

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