linux-fsdevel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* [RFC] odd check in ceph_encode_encrypted_dname()
@ 2025-02-14  2:47 Al Viro
  2025-02-14  3:28 ` Al Viro
  2025-02-14 15:30 ` Jeff Layton
  0 siblings, 2 replies; 21+ messages in thread
From: Al Viro @ 2025-02-14  2:47 UTC (permalink / raw)
  To: Luís Henriques; +Cc: ceph-devel, linux-fsdevel, Ilya Dryomov

AFAICS, this

        /* To understand the 240 limit, see CEPH_NOHASH_NAME_MAX comments */
        WARN_ON(elen > 240);
        if ((elen > 0) && (dir != parent)) {
                char tmp_buf[NAME_MAX];

                elen = snprintf(tmp_buf, sizeof(tmp_buf), "_%.*s_%ld",
                                elen, buf, dir->i_ino);
                memcpy(buf, tmp_buf, elen);
        }

could drop the (elen > 0) part of the test.  elen comes from
        elen = ceph_base64_encode(cryptbuf, len, buf);
and that can't return a non-positive unless the second argument is 0 or
above 1G.  The latter is flat-out impossible - right before that call
we have
        /* hash the end if the name is long enough */
        if (len > CEPH_NOHASH_NAME_MAX) {
                u8 hash[SHA256_DIGEST_SIZE];
                u8 *extra = cryptbuf + CEPH_NOHASH_NAME_MAX;

                /*
                 * hash the extra bytes and overwrite crypttext beyond that
                 * point with it
                 */
                sha256(extra, len - CEPH_NOHASH_NAME_MAX, hash);
                memcpy(extra, hash, SHA256_DIGEST_SIZE);
                len = CEPH_NOHASH_NAME_MAX + SHA256_DIGEST_SIZE;
        }
which obviously caps it with CEPH_NOHASH_NAME_MAX + SHA256_DIGEST_SIZE,
i.e. (180 - SHA256_DIGEST_SIZE) + SHA256_DIGEST_SIZE.

The former would have to come from
        if (!fscrypt_fname_encrypted_size(dir, iname.len, NAME_MAX, &len)) {
                elen = -ENAMETOOLONG;
                goto out;
        }
and since fscrypt_fname_encrypted_size() must've returned true, we have
len no less than FSCRYPT_FNAME_MIN_MSG_LEN, i.e. it's 16 or greater.

That stuff went into the tree in dd66df0053ef8 "ceph: add support for encrypted
snapshot names" and as far as I can tell, everything above had been applicable
back then too.

Am I missing something subtle here?  Can elen be non-positive at that point?

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-14  2:47 [RFC] odd check in ceph_encode_encrypted_dname() Al Viro
@ 2025-02-14  3:28 ` Al Viro
  2025-02-14 14:05   ` Luis Henriques
  2025-02-14 15:41   ` Jeff Layton
  2025-02-14 15:30 ` Jeff Layton
  1 sibling, 2 replies; 21+ messages in thread
From: Al Viro @ 2025-02-14  3:28 UTC (permalink / raw)
  To: Luís Henriques; +Cc: ceph-devel, linux-fsdevel, Ilya Dryomov

On Fri, Feb 14, 2025 at 02:47:56AM +0000, Al Viro wrote:

[snip]

> Am I missing something subtle here?  Can elen be non-positive at that point?

Another fun question: for dentries with name of form _<something>_<inumber>
we end up looking at fscrypt_has_encryption_key() not for the parent,
but for inode with inumber encoded in dentry name.  Fair enough, but...
what happens if we run into such dentry in ceph_mdsc_build_path()?

There the call of ceph_encode_encrypted_fname() is under
	if (fscrypt_has_encryption_key(d_inode(parent)))

Do we need the keys for both?

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-14  3:28 ` Al Viro
@ 2025-02-14 14:05   ` Luis Henriques
  2025-02-14 15:41   ` Jeff Layton
  1 sibling, 0 replies; 21+ messages in thread
From: Luis Henriques @ 2025-02-14 14:05 UTC (permalink / raw)
  To: Al Viro
  Cc: Jeff Layton, Luís Henriques, ceph-devel, linux-fsdevel,
	Ilya Dryomov

On Fri, Feb 14 2025, Al Viro wrote:

> On Fri, Feb 14, 2025 at 02:47:56AM +0000, Al Viro wrote:
>
> [snip]
>
>> Am I missing something subtle here?  Can elen be non-positive at that point?

It has been a while since I last looked into this code, so the details are
quite foggy.  I don't think you're missing something and that '(elen > 0)'
test could be dropped.  Unfortunately, I can only tell that through code
analysis -- I don't have a test environment anymore where I could try
that.

> Another fun question: for dentries with name of form _<something>_<inumber>
> we end up looking at fscrypt_has_encryption_key() not for the parent,
> but for inode with inumber encoded in dentry name.  Fair enough, but...
> what happens if we run into such dentry in ceph_mdsc_build_path()?
>
> There the call of ceph_encode_encrypted_fname() is under
> 	if (fscrypt_has_encryption_key(d_inode(parent)))
>
> Do we need the keys for both?

I'm not sure I totally understand your question, but here are my thoughts:
if we have the key for the parent, then we *do* have the key for an inode
under that encrypted subtree.  This is because AFAIR we can not have
nested encryption.  Thus, the call to ceph_encode_encrypted_fname()
*should* be OK.

But I'm CC'ing Jeff as he wrote most of the cephfs fscrypt code and he
might correct me.  Or maybe he has a better memory than I do.

Cheers,
-- 
Luís

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-14  2:47 [RFC] odd check in ceph_encode_encrypted_dname() Al Viro
  2025-02-14  3:28 ` Al Viro
@ 2025-02-14 15:30 ` Jeff Layton
  1 sibling, 0 replies; 21+ messages in thread
From: Jeff Layton @ 2025-02-14 15:30 UTC (permalink / raw)
  To: Al Viro, Luís Henriques; +Cc: ceph-devel, linux-fsdevel, Ilya Dryomov

On Fri, 2025-02-14 at 02:47 +0000, Al Viro wrote:
> AFAICS, this
> 
>         /* To understand the 240 limit, see CEPH_NOHASH_NAME_MAX comments */
>         WARN_ON(elen > 240);
>         if ((elen > 0) && (dir != parent)) {
>                 char tmp_buf[NAME_MAX];
> 
>                 elen = snprintf(tmp_buf, sizeof(tmp_buf), "_%.*s_%ld",
>                                 elen, buf, dir->i_ino);
>                 memcpy(buf, tmp_buf, elen);
>         }
> 
> could drop the (elen > 0) part of the test.  elen comes from
>         elen = ceph_base64_encode(cryptbuf, len, buf);
> and that can't return a non-positive unless the second argument is 0 or
> above 1G.  The latter is flat-out impossible - right before that call
> we have
>         /* hash the end if the name is long enough */
>         if (len > CEPH_NOHASH_NAME_MAX) {
>                 u8 hash[SHA256_DIGEST_SIZE];
>                 u8 *extra = cryptbuf + CEPH_NOHASH_NAME_MAX;
> 
>                 /*
>                  * hash the extra bytes and overwrite crypttext beyond that
>                  * point with it
>                  */
>                 sha256(extra, len - CEPH_NOHASH_NAME_MAX, hash);
>                 memcpy(extra, hash, SHA256_DIGEST_SIZE);
>                 len = CEPH_NOHASH_NAME_MAX + SHA256_DIGEST_SIZE;
>         }
> which obviously caps it with CEPH_NOHASH_NAME_MAX + SHA256_DIGEST_SIZE,
> i.e. (180 - SHA256_DIGEST_SIZE) + SHA256_DIGEST_SIZE.
> 
> The former would have to come from
>         if (!fscrypt_fname_encrypted_size(dir, iname.len, NAME_MAX, &len)) {
>                 elen = -ENAMETOOLONG;
>                 goto out;
>         }
> and since fscrypt_fname_encrypted_size() must've returned true, we have
> len no less than FSCRYPT_FNAME_MIN_MSG_LEN, i.e. it's 16 or greater.
> 
> That stuff went into the tree in dd66df0053ef8 "ceph: add support for encrypted
> snapshot names" and as far as I can tell, everything above had been applicable
> back then too.
> 
> Am I missing something subtle here?  Can elen be non-positive at that point?
> 

No, I think you nailed it.

-- 
Jeff Layton <jlayton@kernel.org>

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-14  3:28 ` Al Viro
  2025-02-14 14:05   ` Luis Henriques
@ 2025-02-14 15:41   ` Jeff Layton
  2025-02-14 16:05     ` Luis Henriques
  1 sibling, 1 reply; 21+ messages in thread
From: Jeff Layton @ 2025-02-14 15:41 UTC (permalink / raw)
  To: Al Viro, Luís Henriques; +Cc: ceph-devel, linux-fsdevel, Ilya Dryomov

On Fri, 2025-02-14 at 03:28 +0000, Al Viro wrote:
> On Fri, Feb 14, 2025 at 02:47:56AM +0000, Al Viro wrote:
> 
> [snip]
> 
> > Am I missing something subtle here?  Can elen be non-positive at that point?
> 
> Another fun question: for dentries with name of form _<something>_<inumber>
> we end up looking at fscrypt_has_encryption_key() not for the parent,
> but for inode with inumber encoded in dentry name.  Fair enough, but...
> what happens if we run into such dentry in ceph_mdsc_build_path()?
> 
> There the call of ceph_encode_encrypted_fname() is under
> 	if (fscrypt_has_encryption_key(d_inode(parent)))
> 
> Do we need the keys for both?
> 

That sounds like a bug, but I don't fully recall whether snapshots have
a special case here for some reason. Let me rephrase Al's question:

If I have a snapshot dir that is prefixed with '_', why does it use a
different filename encryption key than other snapshot dirs that don't
start with that character?

My guess here is that this code ought not overwrite "dir" with the
result of parse_longname(), but I don't recall the significance of a
snapshot name that starts with '_'.

        /* Handle the special case of snapshot names that start with '_' */
        if ((ceph_snap(dir) == CEPH_SNAPDIR) && (name_len > 0) &&
            (iname.name[0] == '_')) {
                dir = parse_longname(parent, iname.name, &name_len);
                if (IS_ERR(dir))
                        return PTR_ERR(dir);
                iname.name++; /* skip initial '_' */
        }
        iname.len = name_len;

        if (!fscrypt_has_encryption_key(dir)) {
                memcpy(buf, d_name->name, d_name->len);
                elen = d_name->len;
                goto out;
        }


-- 
Jeff Layton <jlayton@kernel.org>

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-14 15:41   ` Jeff Layton
@ 2025-02-14 16:05     ` Luis Henriques
  2025-02-15  4:46       ` Al Viro
  0 siblings, 1 reply; 21+ messages in thread
From: Luis Henriques @ 2025-02-14 16:05 UTC (permalink / raw)
  To: Jeff Layton; +Cc: Al Viro, ceph-devel, linux-fsdevel, Ilya Dryomov

[ Dropping my previous email from the CC list ]

On Fri, Feb 14 2025, Jeff Layton wrote:

> On Fri, 2025-02-14 at 03:28 +0000, Al Viro wrote:
>> On Fri, Feb 14, 2025 at 02:47:56AM +0000, Al Viro wrote:
>> 
>> [snip]
>> 
>> > Am I missing something subtle here?  Can elen be non-positive at that point?
>> 
>> Another fun question: for dentries with name of form _<something>_<inumber>
>> we end up looking at fscrypt_has_encryption_key() not for the parent,
>> but for inode with inumber encoded in dentry name.  Fair enough, but...
>> what happens if we run into such dentry in ceph_mdsc_build_path()?
>> 
>> There the call of ceph_encode_encrypted_fname() is under
>> 	if (fscrypt_has_encryption_key(d_inode(parent)))
>> 
>> Do we need the keys for both?
>> 
>
> That sounds like a bug, but I don't fully recall whether snapshots have
> a special case here for some reason. Let me rephrase Al's question:
>
> If I have a snapshot dir that is prefixed with '_', why does it use a
> different filename encryption key than other snapshot dirs that don't
> start with that character?
>
> My guess here is that this code ought not overwrite "dir" with the
> result of parse_longname(), but I don't recall the significance of a
> snapshot name that starts with '_'.

This bit I _think_ I remember.  Imagine this tree structure:

   /mnt/ceph
   |-- mydir1
   |-- mydir2

If you create a snapshot in /mnt/ceph:

  mkdir /mnt/ceph/.snap/my-snapshot

you'll see this:

   /mnt/ceph
   |-- .snap
   |     |-- my-snapshot
   |-- mydir1
   |     |-- _my-snapshot_1099511627782
   |-- mydir2
         |-- _my-snapshot_1099511627782

('1099511627782' is the inode number where the snapshot was created.)

So, IIRC, when encrypting the snapshot name (the "my-snapshot" string),
you'll use key from the original inode.  That's why we need to handle
snapshot names starting with '_' differently.  And why we have a
customized base64 encoding function.

Cheers,
-- 
Luís


>         /* Handle the special case of snapshot names that start with '_' */
>         if ((ceph_snap(dir) == CEPH_SNAPDIR) && (name_len > 0) &&
>             (iname.name[0] == '_')) {
>                 dir = parse_longname(parent, iname.name, &name_len);
>                 if (IS_ERR(dir))
>                         return PTR_ERR(dir);
>                 iname.name++; /* skip initial '_' */
>         }
>         iname.len = name_len;
>
>         if (!fscrypt_has_encryption_key(dir)) {
>                 memcpy(buf, d_name->name, d_name->len);
>                 elen = d_name->len;
>                 goto out;
>         }
>
>
> -- 
> Jeff Layton <jlayton@kernel.org>
>
 

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-14 16:05     ` Luis Henriques
@ 2025-02-15  4:46       ` Al Viro
  2025-02-15  4:47         ` [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes Al Viro
                           ` (2 more replies)
  0 siblings, 3 replies; 21+ messages in thread
From: Al Viro @ 2025-02-15  4:46 UTC (permalink / raw)
  To: Luis Henriques; +Cc: Jeff Layton, ceph-devel, linux-fsdevel, Ilya Dryomov

On Fri, Feb 14, 2025 at 04:05:42PM +0000, Luis Henriques wrote:

> So, IIRC, when encrypting the snapshot name (the "my-snapshot" string),
> you'll use key from the original inode.  That's why we need to handle
> snapshot names starting with '_' differently.  And why we have a
> customized base64 encoding function.

OK...  The reason I went looking at that thing was the race with rename()
that can end up with UAF in ceph_mdsc_build_path().

We copy the plaintext name under ->d_lock, but then we call
ceph_encode_encrypted_fname() which passes dentry->d_name to
ceph_encode_encrypted_dname() with no locking whatsoever.

Have it race with rename and you've got a lot of unpleasantness.

The thing is, we can have all ceph_encode_encrypted_dname() put the
plaintext name into buf; that eliminates the need to have a separate
qstr (or dentry, in case of ceph_encode_encrypted_fname()) argument and
simplifies ceph_encode_encrypted_dname() while we are at it.

Proposed fix in git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name

WARNING: it's completely untested and needs review.  It's split in two commits
(massage of ceph_encode_encrypted_dname(), then changing the calling conventions);
both patches in followups.

Please, review.

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

* [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes
  2025-02-15  4:46       ` Al Viro
@ 2025-02-15  4:47         ` Al Viro
  2025-02-15 12:41           ` Jeff Layton
  2025-02-15  4:47         ` [PATCH 2/2] ceph: fix a race with rename() in ceph_mdsc_build_path() Al Viro
  2025-02-15 15:39         ` [RFC] odd check in ceph_encode_encrypted_dname() Luis Henriques
  2 siblings, 1 reply; 21+ messages in thread
From: Al Viro @ 2025-02-15  4:47 UTC (permalink / raw)
  To: Luis Henriques; +Cc: Jeff Layton, ceph-devel, linux-fsdevel, Ilya Dryomov

ceph_encode_encrypted_dname() would be better off with plaintext name
already copied into buffer; we'll lift that into the callers on the
next step, which will allow to fix UAF on races with rename; for now
copy it in the very beginning of ceph_encode_encrypted_dname().

That has a pleasant side benefit - we don't need to mess with tmp_buf
anymore (i.e. that's 256 bytes off the stack footprint).

Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
---
 fs/ceph/crypto.c | 40 +++++++++++++++++-----------------------
 1 file changed, 17 insertions(+), 23 deletions(-)

diff --git a/fs/ceph/crypto.c b/fs/ceph/crypto.c
index 3b3c4d8d401e..09346b91215a 100644
--- a/fs/ceph/crypto.c
+++ b/fs/ceph/crypto.c
@@ -265,31 +265,28 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
 {
 	struct ceph_client *cl = ceph_inode_to_client(parent);
 	struct inode *dir = parent;
-	struct qstr iname;
+	char *p = buf;
 	u32 len;
 	int name_len;
 	int elen;
 	int ret;
 	u8 *cryptbuf = NULL;
 
-	iname.name = d_name->name;
-	name_len = d_name->len;
+	memcpy(buf, d_name->name, d_name->len);
+	elen = d_name->len;
+
+	name_len = elen;
 
 	/* Handle the special case of snapshot names that start with '_' */
-	if ((ceph_snap(dir) == CEPH_SNAPDIR) && (name_len > 0) &&
-	    (iname.name[0] == '_')) {
-		dir = parse_longname(parent, iname.name, &name_len);
+	if (ceph_snap(dir) == CEPH_SNAPDIR && *p == '_') {
+		dir = parse_longname(parent, p, &name_len);
 		if (IS_ERR(dir))
 			return PTR_ERR(dir);
-		iname.name++; /* skip initial '_' */
+		p++; /* skip initial '_' */
 	}
-	iname.len = name_len;
 
-	if (!fscrypt_has_encryption_key(dir)) {
-		memcpy(buf, d_name->name, d_name->len);
-		elen = d_name->len;
+	if (!fscrypt_has_encryption_key(dir))
 		goto out;
-	}
 
 	/*
 	 * Convert cleartext d_name to ciphertext. If result is longer than
@@ -297,7 +294,7 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
 	 *
 	 * See: fscrypt_setup_filename
 	 */
-	if (!fscrypt_fname_encrypted_size(dir, iname.len, NAME_MAX, &len)) {
+	if (!fscrypt_fname_encrypted_size(dir, name_len, NAME_MAX, &len)) {
 		elen = -ENAMETOOLONG;
 		goto out;
 	}
@@ -310,7 +307,9 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
 		goto out;
 	}
 
-	ret = fscrypt_fname_encrypt(dir, &iname, cryptbuf, len);
+	ret = fscrypt_fname_encrypt(dir,
+				    &(struct qstr)QSTR_INIT(p, name_len),
+				    cryptbuf, len);
 	if (ret) {
 		elen = ret;
 		goto out;
@@ -331,18 +330,13 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
 	}
 
 	/* base64 encode the encrypted name */
-	elen = ceph_base64_encode(cryptbuf, len, buf);
-	doutc(cl, "base64-encoded ciphertext name = %.*s\n", elen, buf);
+	elen = ceph_base64_encode(cryptbuf, len, p);
+	doutc(cl, "base64-encoded ciphertext name = %.*s\n", elen, p);
 
 	/* To understand the 240 limit, see CEPH_NOHASH_NAME_MAX comments */
 	WARN_ON(elen > 240);
-	if ((elen > 0) && (dir != parent)) {
-		char tmp_buf[NAME_MAX];
-
-		elen = snprintf(tmp_buf, sizeof(tmp_buf), "_%.*s_%ld",
-				elen, buf, dir->i_ino);
-		memcpy(buf, tmp_buf, elen);
-	}
+	if (dir != parent) // leading _ is already there; append _<inum>
+		elen += 1 + sprintf(p + elen, "_%ld", dir->i_ino);
 
 out:
 	kfree(cryptbuf);
-- 
2.39.5


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

* [PATCH 2/2] ceph: fix a race with rename() in ceph_mdsc_build_path()
  2025-02-15  4:46       ` Al Viro
  2025-02-15  4:47         ` [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes Al Viro
@ 2025-02-15  4:47         ` Al Viro
  2025-02-15 12:42           ` Jeff Layton
  2025-02-15 15:39         ` [RFC] odd check in ceph_encode_encrypted_dname() Luis Henriques
  2 siblings, 1 reply; 21+ messages in thread
From: Al Viro @ 2025-02-15  4:47 UTC (permalink / raw)
  To: Luis Henriques; +Cc: Jeff Layton, ceph-devel, linux-fsdevel, Ilya Dryomov

Lift copying the name into callers of ceph_encode_encrypted_dname()
that do not have it already copied; ceph_encode_encrypted_fname()
disappears.

That fixes a UAF in ceph_mdsc_build_path() - while the initial copy
of plaintext into buf is done under ->d_lock, we access the
original name again in ceph_encode_encrypted_fname() and that is
done without any locking.  With ceph_encode_encrypted_dname() using
the stable copy the problem goes away.

Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
---
 fs/ceph/caps.c       | 18 +++++++-----------
 fs/ceph/crypto.c     | 20 +++-----------------
 fs/ceph/crypto.h     | 18 ++++--------------
 fs/ceph/dir.c        |  7 +++----
 fs/ceph/mds_client.c |  4 ++--
 5 files changed, 19 insertions(+), 48 deletions(-)

diff --git a/fs/ceph/caps.c b/fs/ceph/caps.c
index a8d8b56cf9d2..b1a8ff612c41 100644
--- a/fs/ceph/caps.c
+++ b/fs/ceph/caps.c
@@ -4957,24 +4957,20 @@ int ceph_encode_dentry_release(void **p, struct dentry *dentry,
 	cl = ceph_inode_to_client(dir);
 	spin_lock(&dentry->d_lock);
 	if (ret && di->lease_session && di->lease_session->s_mds == mds) {
+		int len = dentry->d_name.len;
 		doutc(cl, "%p mds%d seq %d\n",  dentry, mds,
 		      (int)di->lease_seq);
 		rel->dname_seq = cpu_to_le32(di->lease_seq);
 		__ceph_mdsc_drop_dentry_lease(dentry);
+		memcpy(*p, dentry->d_name.name, len);
 		spin_unlock(&dentry->d_lock);
 		if (IS_ENCRYPTED(dir) && fscrypt_has_encryption_key(dir)) {
-			int ret2 = ceph_encode_encrypted_fname(dir, dentry, *p);
-
-			if (ret2 < 0)
-				return ret2;
-
-			rel->dname_len = cpu_to_le32(ret2);
-			*p += ret2;
-		} else {
-			rel->dname_len = cpu_to_le32(dentry->d_name.len);
-			memcpy(*p, dentry->d_name.name, dentry->d_name.len);
-			*p += dentry->d_name.len;
+			len = ceph_encode_encrypted_dname(dir, *p, len);
+			if (len < 0)
+				return len;
 		}
+		rel->dname_len = cpu_to_le32(len);
+		*p += len;
 	} else {
 		spin_unlock(&dentry->d_lock);
 	}
diff --git a/fs/ceph/crypto.c b/fs/ceph/crypto.c
index 09346b91215a..76a4a7e60270 100644
--- a/fs/ceph/crypto.c
+++ b/fs/ceph/crypto.c
@@ -260,23 +260,17 @@ static struct inode *parse_longname(const struct inode *parent,
 	return dir;
 }
 
-int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
-				char *buf)
+int ceph_encode_encrypted_dname(struct inode *parent, char *buf, int elen)
 {
 	struct ceph_client *cl = ceph_inode_to_client(parent);
 	struct inode *dir = parent;
 	char *p = buf;
 	u32 len;
-	int name_len;
-	int elen;
+	int name_len = elen;
 	int ret;
 	u8 *cryptbuf = NULL;
 
-	memcpy(buf, d_name->name, d_name->len);
-	elen = d_name->len;
-
-	name_len = elen;
-
+	buf[elen] = '\0';
 	/* Handle the special case of snapshot names that start with '_' */
 	if (ceph_snap(dir) == CEPH_SNAPDIR && *p == '_') {
 		dir = parse_longname(parent, p, &name_len);
@@ -349,14 +343,6 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
 	return elen;
 }
 
-int ceph_encode_encrypted_fname(struct inode *parent, struct dentry *dentry,
-				char *buf)
-{
-	WARN_ON_ONCE(!fscrypt_has_encryption_key(parent));
-
-	return ceph_encode_encrypted_dname(parent, &dentry->d_name, buf);
-}
-
 /**
  * ceph_fname_to_usr - convert a filename for userland presentation
  * @fname: ceph_fname to be converted
diff --git a/fs/ceph/crypto.h b/fs/ceph/crypto.h
index d0768239a1c9..f752bbb2eb06 100644
--- a/fs/ceph/crypto.h
+++ b/fs/ceph/crypto.h
@@ -102,10 +102,7 @@ int ceph_fscrypt_prepare_context(struct inode *dir, struct inode *inode,
 				 struct ceph_acl_sec_ctx *as);
 void ceph_fscrypt_as_ctx_to_req(struct ceph_mds_request *req,
 				struct ceph_acl_sec_ctx *as);
-int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
-				char *buf);
-int ceph_encode_encrypted_fname(struct inode *parent, struct dentry *dentry,
-				char *buf);
+int ceph_encode_encrypted_dname(struct inode *parent, char *buf, int len);
 
 static inline int ceph_fname_alloc_buffer(struct inode *parent,
 					  struct fscrypt_str *fname)
@@ -194,17 +191,10 @@ static inline void ceph_fscrypt_as_ctx_to_req(struct ceph_mds_request *req,
 {
 }
 
-static inline int ceph_encode_encrypted_dname(struct inode *parent,
-					      struct qstr *d_name, char *buf)
+static inline int ceph_encode_encrypted_dname(struct inode *parent, char *buf,
+					      int len)
 {
-	memcpy(buf, d_name->name, d_name->len);
-	return d_name->len;
-}
-
-static inline int ceph_encode_encrypted_fname(struct inode *parent,
-					      struct dentry *dentry, char *buf)
-{
-	return -EOPNOTSUPP;
+	return len;
 }
 
 static inline int ceph_fname_alloc_buffer(struct inode *parent,
diff --git a/fs/ceph/dir.c b/fs/ceph/dir.c
index 62e99e65250d..539954e2ea38 100644
--- a/fs/ceph/dir.c
+++ b/fs/ceph/dir.c
@@ -422,17 +422,16 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx)
 			req->r_inode_drop = CEPH_CAP_FILE_EXCL;
 		}
 		if (dfi->last_name) {
-			struct qstr d_name = { .name = dfi->last_name,
-					       .len = strlen(dfi->last_name) };
+			int len = strlen(dfi->last_name);
 
 			req->r_path2 = kzalloc(NAME_MAX + 1, GFP_KERNEL);
 			if (!req->r_path2) {
 				ceph_mdsc_put_request(req);
 				return -ENOMEM;
 			}
+			memcpy(req->r_path2, dfi->last_name, len);
 
-			err = ceph_encode_encrypted_dname(inode, &d_name,
-							  req->r_path2);
+			err = ceph_encode_encrypted_dname(inode, req->r_path2, len);
 			if (err < 0) {
 				ceph_mdsc_put_request(req);
 				return err;
diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
index 54b3421501e9..7dc854631b55 100644
--- a/fs/ceph/mds_client.c
+++ b/fs/ceph/mds_client.c
@@ -2766,8 +2766,8 @@ char *ceph_mdsc_build_path(struct ceph_mds_client *mdsc, struct dentry *dentry,
 			}
 
 			if (fscrypt_has_encryption_key(d_inode(parent))) {
-				len = ceph_encode_encrypted_fname(d_inode(parent),
-								  cur, buf);
+				len = ceph_encode_encrypted_dname(d_inode(parent),
+								  buf, len);
 				if (len < 0) {
 					dput(parent);
 					dput(cur);
-- 
2.39.5


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

* Re: [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes
  2025-02-15  4:47         ` [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes Al Viro
@ 2025-02-15 12:41           ` Jeff Layton
  0 siblings, 0 replies; 21+ messages in thread
From: Jeff Layton @ 2025-02-15 12:41 UTC (permalink / raw)
  To: Al Viro, Luis Henriques; +Cc: ceph-devel, linux-fsdevel, Ilya Dryomov

On Sat, 2025-02-15 at 04:47 +0000, Al Viro wrote:
> ceph_encode_encrypted_dname() would be better off with plaintext name
> already copied into buffer; we'll lift that into the callers on the
> next step, which will allow to fix UAF on races with rename; for now
> copy it in the very beginning of ceph_encode_encrypted_dname().
> 
> That has a pleasant side benefit - we don't need to mess with tmp_buf
> anymore (i.e. that's 256 bytes off the stack footprint).
> 
> Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
> ---
>  fs/ceph/crypto.c | 40 +++++++++++++++++-----------------------
>  1 file changed, 17 insertions(+), 23 deletions(-)
> 
> diff --git a/fs/ceph/crypto.c b/fs/ceph/crypto.c
> index 3b3c4d8d401e..09346b91215a 100644
> --- a/fs/ceph/crypto.c
> +++ b/fs/ceph/crypto.c
> @@ -265,31 +265,28 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
>  {
>  	struct ceph_client *cl = ceph_inode_to_client(parent);
>  	struct inode *dir = parent;
> -	struct qstr iname;
> +	char *p = buf;
>  	u32 len;
>  	int name_len;
>  	int elen;
>  	int ret;
>  	u8 *cryptbuf = NULL;
>  
> -	iname.name = d_name->name;
> -	name_len = d_name->len;
> +	memcpy(buf, d_name->name, d_name->len);
> +	elen = d_name->len;
> +
> +	name_len = elen;
>  
>  	/* Handle the special case of snapshot names that start with '_' */
> -	if ((ceph_snap(dir) == CEPH_SNAPDIR) && (name_len > 0) &&
> -	    (iname.name[0] == '_')) {
> -		dir = parse_longname(parent, iname.name, &name_len);
> +	if (ceph_snap(dir) == CEPH_SNAPDIR && *p == '_') {
> +		dir = parse_longname(parent, p, &name_len);
>  		if (IS_ERR(dir))
>  			return PTR_ERR(dir);
> -		iname.name++; /* skip initial '_' */
> +		p++; /* skip initial '_' */
>  	}
> -	iname.len = name_len;
>  
> -	if (!fscrypt_has_encryption_key(dir)) {
> -		memcpy(buf, d_name->name, d_name->len);
> -		elen = d_name->len;
> +	if (!fscrypt_has_encryption_key(dir))
>  		goto out;
> -	}
>  
>  	/*
>  	 * Convert cleartext d_name to ciphertext. If result is longer than
> @@ -297,7 +294,7 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
>  	 *
>  	 * See: fscrypt_setup_filename
>  	 */
> -	if (!fscrypt_fname_encrypted_size(dir, iname.len, NAME_MAX, &len)) {
> +	if (!fscrypt_fname_encrypted_size(dir, name_len, NAME_MAX, &len)) {
>  		elen = -ENAMETOOLONG;
>  		goto out;
>  	}
> @@ -310,7 +307,9 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
>  		goto out;
>  	}
>  
> -	ret = fscrypt_fname_encrypt(dir, &iname, cryptbuf, len);
> +	ret = fscrypt_fname_encrypt(dir,
> +				    &(struct qstr)QSTR_INIT(p, name_len),
> +				    cryptbuf, len);
>  	if (ret) {
>  		elen = ret;
>  		goto out;
> @@ -331,18 +330,13 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
>  	}
>  
>  	/* base64 encode the encrypted name */
> -	elen = ceph_base64_encode(cryptbuf, len, buf);
> -	doutc(cl, "base64-encoded ciphertext name = %.*s\n", elen, buf);
> +	elen = ceph_base64_encode(cryptbuf, len, p);
> +	doutc(cl, "base64-encoded ciphertext name = %.*s\n", elen, p);
>  
>  	/* To understand the 240 limit, see CEPH_NOHASH_NAME_MAX comments */
>  	WARN_ON(elen > 240);
> -	if ((elen > 0) && (dir != parent)) {
> -		char tmp_buf[NAME_MAX];
> -
> -		elen = snprintf(tmp_buf, sizeof(tmp_buf), "_%.*s_%ld",
> -				elen, buf, dir->i_ino);
> -		memcpy(buf, tmp_buf, elen);
> -	}
> +	if (dir != parent) // leading _ is already there; append _<inum>
> +		elen += 1 + sprintf(p + elen, "_%ld", dir->i_ino);
>  
>  out:
>  	kfree(cryptbuf);

Reviewed-by: Jeff Layton <jlayton@kernel.org>

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

* Re: [PATCH 2/2] ceph: fix a race with rename() in ceph_mdsc_build_path()
  2025-02-15  4:47         ` [PATCH 2/2] ceph: fix a race with rename() in ceph_mdsc_build_path() Al Viro
@ 2025-02-15 12:42           ` Jeff Layton
  0 siblings, 0 replies; 21+ messages in thread
From: Jeff Layton @ 2025-02-15 12:42 UTC (permalink / raw)
  To: Al Viro, Luis Henriques; +Cc: ceph-devel, linux-fsdevel, Ilya Dryomov

On Sat, 2025-02-15 at 04:47 +0000, Al Viro wrote:
> Lift copying the name into callers of ceph_encode_encrypted_dname()
> that do not have it already copied; ceph_encode_encrypted_fname()
> disappears.
> 
> That fixes a UAF in ceph_mdsc_build_path() - while the initial copy
> of plaintext into buf is done under ->d_lock, we access the
> original name again in ceph_encode_encrypted_fname() and that is
> done without any locking.  With ceph_encode_encrypted_dname() using
> the stable copy the problem goes away.
> 
> Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
> ---
>  fs/ceph/caps.c       | 18 +++++++-----------
>  fs/ceph/crypto.c     | 20 +++-----------------
>  fs/ceph/crypto.h     | 18 ++++--------------
>  fs/ceph/dir.c        |  7 +++----
>  fs/ceph/mds_client.c |  4 ++--
>  5 files changed, 19 insertions(+), 48 deletions(-)
> 
> diff --git a/fs/ceph/caps.c b/fs/ceph/caps.c
> index a8d8b56cf9d2..b1a8ff612c41 100644
> --- a/fs/ceph/caps.c
> +++ b/fs/ceph/caps.c
> @@ -4957,24 +4957,20 @@ int ceph_encode_dentry_release(void **p, struct dentry *dentry,
>  	cl = ceph_inode_to_client(dir);
>  	spin_lock(&dentry->d_lock);
>  	if (ret && di->lease_session && di->lease_session->s_mds == mds) {
> +		int len = dentry->d_name.len;
>  		doutc(cl, "%p mds%d seq %d\n",  dentry, mds,
>  		      (int)di->lease_seq);
>  		rel->dname_seq = cpu_to_le32(di->lease_seq);
>  		__ceph_mdsc_drop_dentry_lease(dentry);
> +		memcpy(*p, dentry->d_name.name, len);
>  		spin_unlock(&dentry->d_lock);
>  		if (IS_ENCRYPTED(dir) && fscrypt_has_encryption_key(dir)) {
> -			int ret2 = ceph_encode_encrypted_fname(dir, dentry, *p);
> -
> -			if (ret2 < 0)
> -				return ret2;
> -
> -			rel->dname_len = cpu_to_le32(ret2);
> -			*p += ret2;
> -		} else {
> -			rel->dname_len = cpu_to_le32(dentry->d_name.len);
> -			memcpy(*p, dentry->d_name.name, dentry->d_name.len);
> -			*p += dentry->d_name.len;
> +			len = ceph_encode_encrypted_dname(dir, *p, len);
> +			if (len < 0)
> +				return len;
>  		}
> +		rel->dname_len = cpu_to_le32(len);
> +		*p += len;
>  	} else {
>  		spin_unlock(&dentry->d_lock);
>  	}
> diff --git a/fs/ceph/crypto.c b/fs/ceph/crypto.c
> index 09346b91215a..76a4a7e60270 100644
> --- a/fs/ceph/crypto.c
> +++ b/fs/ceph/crypto.c
> @@ -260,23 +260,17 @@ static struct inode *parse_longname(const struct inode *parent,
>  	return dir;
>  }
>  
> -int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
> -				char *buf)
> +int ceph_encode_encrypted_dname(struct inode *parent, char *buf, int elen)
>  {
>  	struct ceph_client *cl = ceph_inode_to_client(parent);
>  	struct inode *dir = parent;
>  	char *p = buf;
>  	u32 len;
> -	int name_len;
> -	int elen;
> +	int name_len = elen;
>  	int ret;
>  	u8 *cryptbuf = NULL;
>  
> -	memcpy(buf, d_name->name, d_name->len);
> -	elen = d_name->len;
> -
> -	name_len = elen;
> -
> +	buf[elen] = '\0';
>  	/* Handle the special case of snapshot names that start with '_' */
>  	if (ceph_snap(dir) == CEPH_SNAPDIR && *p == '_') {
>  		dir = parse_longname(parent, p, &name_len);
> @@ -349,14 +343,6 @@ int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
>  	return elen;
>  }
>  
> -int ceph_encode_encrypted_fname(struct inode *parent, struct dentry *dentry,
> -				char *buf)
> -{
> -	WARN_ON_ONCE(!fscrypt_has_encryption_key(parent));
> -
> -	return ceph_encode_encrypted_dname(parent, &dentry->d_name, buf);
> -}
> -
>  /**
>   * ceph_fname_to_usr - convert a filename for userland presentation
>   * @fname: ceph_fname to be converted
> diff --git a/fs/ceph/crypto.h b/fs/ceph/crypto.h
> index d0768239a1c9..f752bbb2eb06 100644
> --- a/fs/ceph/crypto.h
> +++ b/fs/ceph/crypto.h
> @@ -102,10 +102,7 @@ int ceph_fscrypt_prepare_context(struct inode *dir, struct inode *inode,
>  				 struct ceph_acl_sec_ctx *as);
>  void ceph_fscrypt_as_ctx_to_req(struct ceph_mds_request *req,
>  				struct ceph_acl_sec_ctx *as);
> -int ceph_encode_encrypted_dname(struct inode *parent, struct qstr *d_name,
> -				char *buf);
> -int ceph_encode_encrypted_fname(struct inode *parent, struct dentry *dentry,
> -				char *buf);
> +int ceph_encode_encrypted_dname(struct inode *parent, char *buf, int len);
>  
>  static inline int ceph_fname_alloc_buffer(struct inode *parent,
>  					  struct fscrypt_str *fname)
> @@ -194,17 +191,10 @@ static inline void ceph_fscrypt_as_ctx_to_req(struct ceph_mds_request *req,
>  {
>  }
>  
> -static inline int ceph_encode_encrypted_dname(struct inode *parent,
> -					      struct qstr *d_name, char *buf)
> +static inline int ceph_encode_encrypted_dname(struct inode *parent, char *buf,
> +					      int len)
>  {
> -	memcpy(buf, d_name->name, d_name->len);
> -	return d_name->len;
> -}
> -
> -static inline int ceph_encode_encrypted_fname(struct inode *parent,
> -					      struct dentry *dentry, char *buf)
> -{
> -	return -EOPNOTSUPP;
> +	return len;
>  }
>  
>  static inline int ceph_fname_alloc_buffer(struct inode *parent,
> diff --git a/fs/ceph/dir.c b/fs/ceph/dir.c
> index 62e99e65250d..539954e2ea38 100644
> --- a/fs/ceph/dir.c
> +++ b/fs/ceph/dir.c
> @@ -422,17 +422,16 @@ static int ceph_readdir(struct file *file, struct dir_context *ctx)
>  			req->r_inode_drop = CEPH_CAP_FILE_EXCL;
>  		}
>  		if (dfi->last_name) {
> -			struct qstr d_name = { .name = dfi->last_name,
> -					       .len = strlen(dfi->last_name) };
> +			int len = strlen(dfi->last_name);
>  
>  			req->r_path2 = kzalloc(NAME_MAX + 1, GFP_KERNEL);
>  			if (!req->r_path2) {
>  				ceph_mdsc_put_request(req);
>  				return -ENOMEM;
>  			}
> +			memcpy(req->r_path2, dfi->last_name, len);
>  
> -			err = ceph_encode_encrypted_dname(inode, &d_name,
> -							  req->r_path2);
> +			err = ceph_encode_encrypted_dname(inode, req->r_path2, len);
>  			if (err < 0) {
>  				ceph_mdsc_put_request(req);
>  				return err;
> diff --git a/fs/ceph/mds_client.c b/fs/ceph/mds_client.c
> index 54b3421501e9..7dc854631b55 100644
> --- a/fs/ceph/mds_client.c
> +++ b/fs/ceph/mds_client.c
> @@ -2766,8 +2766,8 @@ char *ceph_mdsc_build_path(struct ceph_mds_client *mdsc, struct dentry *dentry,
>  			}
>  
>  			if (fscrypt_has_encryption_key(d_inode(parent))) {
> -				len = ceph_encode_encrypted_fname(d_inode(parent),
> -								  cur, buf);
> +				len = ceph_encode_encrypted_dname(d_inode(parent),
> +								  buf, len);
>  				if (len < 0) {
>  					dput(parent);
>  					dput(cur);

Reviewed-by: Jeff Layton <jlayton@kernel.org>

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-15  4:46       ` Al Viro
  2025-02-15  4:47         ` [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes Al Viro
  2025-02-15  4:47         ` [PATCH 2/2] ceph: fix a race with rename() in ceph_mdsc_build_path() Al Viro
@ 2025-02-15 15:39         ` Luis Henriques
  2025-02-17 17:56           ` Viacheslav Dubeyko
  2 siblings, 1 reply; 21+ messages in thread
From: Luis Henriques @ 2025-02-15 15:39 UTC (permalink / raw)
  To: Al Viro
  Cc: Viacheslav Dubeyko, Jeff Layton, ceph-devel, linux-fsdevel,
	Ilya Dryomov

On Sat, Feb 15 2025, Al Viro wrote:

> On Fri, Feb 14, 2025 at 04:05:42PM +0000, Luis Henriques wrote:
>
>> So, IIRC, when encrypting the snapshot name (the "my-snapshot" string),
>> you'll use key from the original inode.  That's why we need to handle
>> snapshot names starting with '_' differently.  And why we have a
>> customized base64 encoding function.
>
> OK...  The reason I went looking at that thing was the race with rename()
> that can end up with UAF in ceph_mdsc_build_path().
>
> We copy the plaintext name under ->d_lock, but then we call
> ceph_encode_encrypted_fname() which passes dentry->d_name to
> ceph_encode_encrypted_dname() with no locking whatsoever.
>
> Have it race with rename and you've got a lot of unpleasantness.
>
> The thing is, we can have all ceph_encode_encrypted_dname() put the
> plaintext name into buf; that eliminates the need to have a separate
> qstr (or dentry, in case of ceph_encode_encrypted_fname()) argument and
> simplifies ceph_encode_encrypted_dname() while we are at it.
>
> Proposed fix in git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name
>
> WARNING: it's completely untested and needs review.  It's split in two commits
> (massage of ceph_encode_encrypted_dname(), then changing the calling conventions);
> both patches in followups.
>
> Please, review.

I've reviewed both patches and they seem to be OK, so feel free to add my

Reviewed-by: Luis Henriques <luis@igalia.com>

But as I said, I don't have a test environment at the moment.  I'm adding
Slava to CC with the hope that he may be able to run some fscrypt-specific
tests (including snapshots creation) against these patches.

Cheers,
-- 
Luís

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

* RE: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-15 15:39         ` [RFC] odd check in ceph_encode_encrypted_dname() Luis Henriques
@ 2025-02-17 17:56           ` Viacheslav Dubeyko
  2025-02-17 18:48             ` Luis Henriques
  0 siblings, 1 reply; 21+ messages in thread
From: Viacheslav Dubeyko @ 2025-02-17 17:56 UTC (permalink / raw)
  To: luis@igalia.com, viro@zeniv.linux.org.uk
  Cc: ceph-devel@vger.kernel.org, jlayton@kernel.org,
	linux-fsdevel@vger.kernel.org, idryomov@gmail.com

On Sat, 2025-02-15 at 15:39 +0000, Luis Henriques wrote:
> On Sat, Feb 15 2025, Al Viro wrote:
> 
> > On Fri, Feb 14, 2025 at 04:05:42PM +0000, Luis Henriques wrote:
> > 
> > > So, IIRC, when encrypting the snapshot name (the "my-snapshot" string),
> > > you'll use key from the original inode.  That's why we need to handle
> > > snapshot names starting with '_' differently.  And why we have a
> > > customized base64 encoding function.
> > 
> > OK...  The reason I went looking at that thing was the race with rename()
> > that can end up with UAF in ceph_mdsc_build_path().
> > 
> > We copy the plaintext name under ->d_lock, but then we call
> > ceph_encode_encrypted_fname() which passes dentry->d_name to
> > ceph_encode_encrypted_dname() with no locking whatsoever.
> > 
> > Have it race with rename and you've got a lot of unpleasantness.
> > 
> > The thing is, we can have all ceph_encode_encrypted_dname() put the
> > plaintext name into buf; that eliminates the need to have a separate
> > qstr (or dentry, in case of ceph_encode_encrypted_fname()) argument and
> > simplifies ceph_encode_encrypted_dname() while we are at it.
> > 
> > Proposed fix in git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name
> > 
> > WARNING: it's completely untested and needs review.  It's split in two commits
> > (massage of ceph_encode_encrypted_dname(), then changing the calling conventions);
> > both patches in followups.
> > 
> > Please, review.
> 
> I've reviewed both patches and they seem to be OK, so feel free to add my
> 
> Reviewed-by: Luis Henriques <luis@igalia.com>
> 
> But as I said, I don't have a test environment at the moment.  I'm adding
> Slava to CC with the hope that he may be able to run some fscrypt-specific
> tests (including snapshots creation) against these patches.
> 
> 

Let me apply the patches and test it. I'll share the testing results ASAP.

Thanks,
Slava.


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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-17 17:56           ` Viacheslav Dubeyko
@ 2025-02-17 18:48             ` Luis Henriques
  2025-02-17 22:04               ` Viacheslav Dubeyko
  0 siblings, 1 reply; 21+ messages in thread
From: Luis Henriques @ 2025-02-17 18:48 UTC (permalink / raw)
  To: Viacheslav Dubeyko
  Cc: viro@zeniv.linux.org.uk, ceph-devel@vger.kernel.org,
	jlayton@kernel.org, linux-fsdevel@vger.kernel.org,
	idryomov@gmail.com

On Mon, Feb 17 2025, Viacheslav Dubeyko wrote:

> On Sat, 2025-02-15 at 15:39 +0000, Luis Henriques wrote:
>> On Sat, Feb 15 2025, Al Viro wrote:
>> 
>> > On Fri, Feb 14, 2025 at 04:05:42PM +0000, Luis Henriques wrote:
>> > 
>> > > So, IIRC, when encrypting the snapshot name (the "my-snapshot" string),
>> > > you'll use key from the original inode.  That's why we need to handle
>> > > snapshot names starting with '_' differently.  And why we have a
>> > > customized base64 encoding function.
>> > 
>> > OK...  The reason I went looking at that thing was the race with rename()
>> > that can end up with UAF in ceph_mdsc_build_path().
>> > 
>> > We copy the plaintext name under ->d_lock, but then we call
>> > ceph_encode_encrypted_fname() which passes dentry->d_name to
>> > ceph_encode_encrypted_dname() with no locking whatsoever.
>> > 
>> > Have it race with rename and you've got a lot of unpleasantness.
>> > 
>> > The thing is, we can have all ceph_encode_encrypted_dname() put the
>> > plaintext name into buf; that eliminates the need to have a separate
>> > qstr (or dentry, in case of ceph_encode_encrypted_fname()) argument and
>> > simplifies ceph_encode_encrypted_dname() while we are at it.
>> > 
>> > Proposed fix in git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name
>> > 
>> > WARNING: it's completely untested and needs review.  It's split in two commits
>> > (massage of ceph_encode_encrypted_dname(), then changing the calling conventions);
>> > both patches in followups.
>> > 
>> > Please, review.
>> 
>> I've reviewed both patches and they seem to be OK, so feel free to add my
>> 
>> Reviewed-by: Luis Henriques <luis@igalia.com>
>> 
>> But as I said, I don't have a test environment at the moment.  I'm adding
>> Slava to CC with the hope that he may be able to run some fscrypt-specific
>> tests (including snapshots creation) against these patches.
>> 
>> 
>
> Let me apply the patches and test it. I'll share the testing results ASAP.

Awesome!  Thanks a lot.

Cheers,
-- 
Luís

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

* RE: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-17 18:48             ` Luis Henriques
@ 2025-02-17 22:04               ` Viacheslav Dubeyko
  2025-02-18  1:21                 ` Al Viro
  0 siblings, 1 reply; 21+ messages in thread
From: Viacheslav Dubeyko @ 2025-02-17 22:04 UTC (permalink / raw)
  To: luis@igalia.com
  Cc: ceph-devel@vger.kernel.org, jlayton@kernel.org,
	viro@zeniv.linux.org.uk, idryomov@gmail.com,
	linux-fsdevel@vger.kernel.org

On Mon, 2025-02-17 at 18:48 +0000, Luis Henriques wrote:
> On Mon, Feb 17 2025, Viacheslav Dubeyko wrote:
> 
> > On Sat, 2025-02-15 at 15:39 +0000, Luis Henriques wrote:
> > > On Sat, Feb 15 2025, Al Viro wrote:
> > > 
> > > > On Fri, Feb 14, 2025 at 04:05:42PM +0000, Luis Henriques wrote:
> > > > 
> > > > > So, IIRC, when encrypting the snapshot name (the "my-snapshot" string),
> > > > > you'll use key from the original inode.  That's why we need to handle
> > > > > snapshot names starting with '_' differently.  And why we have a
> > > > > customized base64 encoding function.
> > > > 
> > > > OK...  The reason I went looking at that thing was the race with rename()
> > > > that can end up with UAF in ceph_mdsc_build_path().
> > > > 
> > > > We copy the plaintext name under ->d_lock, but then we call
> > > > ceph_encode_encrypted_fname() which passes dentry->d_name to
> > > > ceph_encode_encrypted_dname() with no locking whatsoever.
> > > > 
> > > > Have it race with rename and you've got a lot of unpleasantness.
> > > > 
> > > > The thing is, we can have all ceph_encode_encrypted_dname() put the
> > > > plaintext name into buf; that eliminates the need to have a separate
> > > > qstr (or dentry, in case of ceph_encode_encrypted_fname()) argument and
> > > > simplifies ceph_encode_encrypted_dname() while we are at it.
> > > > 
> > > > Proposed fix in git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name
> > > > 
> > > > WARNING: it's completely untested and needs review.  It's split in two commits
> > > > (massage of ceph_encode_encrypted_dname(), then changing the calling conventions);
> > > > both patches in followups.
> > > > 
> > > > Please, review.
> > > 
> > > I've reviewed both patches and they seem to be OK, so feel free to add my
> > > 
> > > Reviewed-by: Luis Henriques <luis@igalia.com>
> > > 
> > > But as I said, I don't have a test environment at the moment.  I'm adding
> > > Slava to CC with the hope that he may be able to run some fscrypt-specific
> > > tests (including snapshots creation) against these patches.
> > > 
> > > 
> > 
> > Let me apply the patches and test it. I'll share the testing results ASAP.
> 
> Awesome!  Thanks a lot.
> 

As far as I can see, we have issue in the patchset. I was able to reproduce the
issue two times. 

sudo ./check -g encrypt
FSTYP         -- ceph
PLATFORM      -- Linux/x86_64 ceph-testing-0001 6.14.0-rc3+ #174 SMP
PREEMPT_DYNAMIC Mon Feb 17 21:02:23 UTC 2025
MKFS_OPTIONS  -- 127.0.0.1:40364:/scratch
MOUNT_OPTIONS -- -o name=fs,secret=<secret>,ms_mode=crc,nowsync,copyfrom
127.0.0.1:40364:/scratch /mnt/scratch

generic/395 10s ...  10s
generic/396 9s ...  9s
generic/397 10s ... _check_dmesg: something found in dmesg (see
/home/slavad/XFSTESTS/xfstests-dev/results//generic/397.dmesg)

generic/398 1s ... [not run] kernel doesn't support renameat2 syscall
generic/399 28s ... [not run] Filesystem ceph not supported in
_scratch_mkfs_sized_encrypted
generic/419 1s ... [not run] kernel doesn't support renameat2 syscall
generic/421 14s ...  13s
generic/429 22s ...  22s
generic/435 891s ...  848s
generic/440 14s ...  13s
generic/548 2s ... [not run] xfs_io fiemap  failed (old kernel/wrong fs?)
generic/549 2s ... [not run] encryption policy '-c 5 -n 6 -f 0' is unusable;
probably missing kernel crypto API support
generic/550 4s ... [not run] encryption policy '-c 9 -n 9 -f 0' is unusable;
probably missing kernel crypto API support
generic/576       [not run] fsverity utility required, skipped this test
generic/580 15s ...  14s
generic/581 22s ...  21s
generic/582 2s ... [not run] xfs_io fiemap  failed (old kernel/wrong fs?)
generic/583 2s ... [not run] encryption policy '-c 5 -n 6 -v 2 -f 0' is
unusable; probably missing kernel crypto API support
generic/584 3s ... [not run] encryption policy '-c 9 -n 9 -v 2 -f 0' is
unusable; probably missing kernel crypto API support
generic/592 3s ... [not run] kernel does not support encryption policy: '-c 1 -n
4 -v 2 -f 8'
generic/593 15s ...  16s
generic/595 19s ...  18s
generic/602 2s ... [not run] kernel does not support encryption policy: '-c 1 -n
4 -v 2 -f 16'
generic/613 5s ... [not run] _get_encryption_nonce() isn't implemented on ceph
generic/621 6s ... [not run] kernel doesn't support renameat2 syscall
generic/693 6s ... [not run] encryption policy '-c 1 -n 10 -v 2 -f 0' is
unusable; probably missing kernel crypto API support
generic/739       [not run] xfs_io set_encpolicy doesn't support -s
Ran: generic/395 generic/396 generic/397 generic/398 generic/399 generic/419
generic/421 generic/429 generic/435 generic/440 generic/548 generic/549
generic/550 generic/576 generic/580 generic/581 generic/582 generic/583
generic/584 generic/592 generic/593 generic/595 generic/602 generic/613
generic/621 generic/693 generic/739
Not run: generic/398 generic/399 generic/419 generic/548 generic/549 generic/550
generic/576 generic/582 generic/583 generic/584 generic/592 generic/602
generic/613 generic/621 generic/693 generic/739
Failures: generic/397
Failed 1 of 27 tests

[  324.470491] run fstests generic/397 at 2025-02-17 12:18:13
[  325.099452] libceph: mon0 (2)127.0.0.1:40489 session established
[  325.100845] libceph: client4422 fsid 5b237794-984b-474e-a140-606ec05b28a3
[  325.597065] libceph: mon0 (2)127.0.0.1:40489 session established
[  325.598444] libceph: client4425 fsid 5b237794-984b-474e-a140-606ec05b28a3
[  325.679627] libceph: mon0 (2)127.0.0.1:40489 session established
[  325.687399] libceph: client4428 fsid 5b237794-984b-474e-a140-606ec05b28a3
[  325.803731] xfs_io (pid 8916) is setting deprecated v1 encryption policy;
recommend upgrading to v2.
[  325.883128] libceph: mon0 (2)127.0.0.1:40489 session established
[  325.884512] libceph: client4431 fsid 5b237794-984b-474e-a140-606ec05b28a3
[  325.946397] libceph: mon0 (2)127.0.0.1:40489 session established
[  325.947903] libceph: client4434 fsid 5b237794-984b-474e-a140-606ec05b28a3
[  326.321358] fscrypt: AES-256-CBC-CTS using implementation "cts(cbc(ecb(aes-
generic)))"
[  326.354897] fscrypt: AES-256-XTS using implementation "xts(ecb(aes-generic))"
[  326.439844]
==================================================================
[  326.440593] BUG: KASAN: stack-out-of-bounds in
ceph_encode_encrypted_dname+0x4a5/0x560
[  326.441355] Write of size 1 at addr ffff8881b6c5f27f by task xfs_io/9048

[  326.442149] CPU: 0 UID: 0 PID: 9048 Comm: xfs_io Not tainted 6.14.0-rc3+ #172
[  326.442156] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS rel-
1.16.3-0-ga6ed6b701f0a-prebuilt.qemu.org 04/01/2014
[  326.442162] Call Trace:
[  326.442166]  <TASK>
[  326.442171]  dump_stack_lvl+0x76/0xa0
[  326.442182]  print_report+0xce/0x5f0
[  326.442187]  ? ceph_encode_encrypted_dname+0x4a5/0x560
[  326.442191]  ? kasan_addr_to_slab+0xd/0xb0
[  326.442198]  ? ceph_encode_encrypted_dname+0x4a5/0x560
[  326.442203]  kasan_report+0xbe/0x110
[  326.442206]  ? ceph_encode_encrypted_dname+0x4a5/0x560
[  326.442212]  __asan_report_store1_noabort+0x17/0x30
[  326.442217]  ceph_encode_encrypted_dname+0x4a5/0x560
[  326.442221]  ? lockref_get_not_zero+0xd5/0x1d0
[  326.442227]  ? __pfx_ceph_encode_encrypted_dname+0x10/0x10
[  326.442232]  ? __kasan_check_write+0x14/0x30
[  326.442237]  ? _raw_spin_lock+0x82/0xf0
[  326.442242]  ceph_mdsc_build_path+0x710/0xb00
[  326.442249]  ? __pfx_ceph_mdsc_build_path+0x10/0x10
[  326.442258]  set_request_path_attr.constprop.0+0x286/0xa50
[  326.442262]  ? __kernel_text_address+0x12/0x50
[  326.442269]  __prepare_send_request+0x6ad/0x49f0
[  326.442277]  ? __pfx___prepare_send_request+0x10/0x10
[  326.442282]  ? __kasan_check_write+0x14/0x30
[  326.442286]  ? _raw_spin_lock_irqsave+0x96/0x110
[  326.442293]  ? ceph_get_mds_session+0xd7/0x160
[  326.442298]  ? __pfx_ceph_get_mds_session+0x10/0x10
[  326.442302]  ? _atomic_dec_and_lock+0x19/0xc0
[  326.442307]  ? iput+0x104/0x5c0
[  326.442312]  __do_request+0x172b/0x4590
[  326.442319]  ? __pfx___do_request+0x10/0x10
[  326.442324]  ? __kasan_check_write+0x14/0x30
[  326.442328]  ? _raw_spin_lock+0x82/0xf0
[  326.442331]  ? __pfx__raw_spin_lock+0x10/0x10
[  326.442335]  ceph_mdsc_submit_request+0x6a8/0xe70
[  326.442341]  ceph_mdsc_do_request+0x6f/0x200
[  326.442346]  ceph_atomic_open+0xa18/0x24c0
[  326.442354]  ? __pfx_ceph_atomic_open+0x10/0x10
[  326.442358]  ? __pfx_make_vfsuid+0x10/0x10
[  326.442363]  ? __pfx_map_id_up+0x10/0x10
[  326.442367]  ? generic_permission+0x149/0x670
[  326.442373]  ? ceph_permission+0x54/0x80
[  326.442379]  ? inode_permission+0x130/0x4f0
[  326.442383]  path_openat+0x2262/0x4430
[  326.442389]  ? __pfx_path_openat+0x10/0x10
[  326.442392]  ? stack_depot_save_flags+0x2d/0x800
[  326.442397]  ? local_clock_noinstr+0xe/0xd0
[  326.442403]  ? stack_depot_save+0xe/0x20
[  326.442407]  do_filp_open+0x1d4/0x430
[  326.442411]  ? _raw_spin_lock_irqsave+0x96/0x110
[  326.442415]  ? __pfx_do_filp_open+0x10/0x10
[  326.442421]  ? _raw_spin_unlock+0xe/0x40
[  326.442426]  do_sys_openat2+0x136/0x180
[  326.442431]  ? __pfx_do_sys_openat2+0x10/0x10
[  326.442436]  ? __count_memcg_events+0x19e/0x490
[  326.442440]  __x64_sys_openat+0x128/0x220
[  326.442445]  ? __pfx___x64_sys_openat+0x10/0x10
[  326.442451]  x64_sys_call+0x1998/0x26f0
[  326.442457]  do_syscall_64+0x7c/0x170
[  326.442463]  ? irqentry_exit+0x43/0x50
[  326.442466]  ? clear_bhb_loop+0x15/0x70
[  326.442472]  ? clear_bhb_loop+0x15/0x70
[  326.442476]  ? clear_bhb_loop+0x15/0x70
[  326.442481]  entry_SYSCALL_64_after_hwframe+0x76/0x7e
[  326.442485] RIP: 0033:0x748c12b1453b
[  326.442495] Code: 25 00 00 41 00 3d 00 00 41 00 74 4b 64 8b 04 25 18 00 00 00
85 c0 75 67 44 89 e2 48 89 ee bf 9c ff ff ff b8 01 01 00 00 0f 05 <48> 3d 00 f0
ff ff 0f 87 91 00 00 00 48 8b 54 24 28 64 48 2b 14 25
[  326.442499] RSP: 002b:00007fffdf0d0730 EFLAGS: 00000246 ORIG_RAX:
0000000000000101
[  326.442504] RAX: ffffffffffffffda RBX: 0000000000000242 RCX: 0000748c12b1453b
[  326.442508] RDX: 0000000000000242 RSI: 00007fffdf0d1599 RDI: 00000000ffffff9c
[  326.442510] RBP: 00007fffdf0d1599 R08: 00007fffdf0d09e0 R09: 0000000000000000
[  326.442513] R10: 0000000000000180 R11: 0000000000000246 R12: 0000000000000242
[  326.442515] R13: 0000748c12c1a42c R14: 0000000000000180 R15: 0000000000000060
[  326.442521]  </TASK>

[  326.475514] The buggy address belongs to stack of task xfs_io/9048
[  326.476100]  and is located at offset 287 in frame:
[  326.476555]  ceph_mdsc_build_path+0x0/0xb00

[  326.477120] This frame has 1 object:
[  326.477466]  [32, 287) 'buf'

[  326.477899] The buggy address belongs to the physical page:
[  326.478428] page: refcount:0 mapcount:0 mapping:0000000000000000
index:0x10af98 pfn:0x1b6c5f
[  326.478437] flags: 0x17ffffc0000000(node=0|zone=2|lastcpupid=0x1fffff)
[  326.478445] raw: 0017ffffc0000000 0000000000000000 dead000000000122
0000000000000000
[  326.478449] raw: 000000000010af98 0000000000000000 00000000ffffffff
0000000000000000
[  326.478452] page dumped because: kasan: bad access detected

[  326.478619] Memory state around the buggy address:
[  326.479079]  ffff8881b6c5f100: 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1 f1
f1
[  326.479749]  ffff8881b6c5f180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00
[  326.480431] >ffff8881b6c5f200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
07
[  326.481106]                                                                 ^
[  326.481770]  ffff8881b6c5f280: f3 f3 f3 f3 f3 f3 f3 f3 00 00 00 00 00 00 00
00
[  326.482455]  ffff8881b6c5f300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00
[  326.483128]
==================================================================
[  326.484102] Disabling lock debugging due to kernel taint
[  326.752698] libceph: mon0 (2)127.0.0.1:40489 session established
[  326.755111] libceph: client4437 fsid 5b237794-984b-474e-a140-606ec05b28a3
[  327.128197] libceph: mon0 (2)127.0.0.1:40489 session established
[  327.129890] libceph: client4440 fsid 5b237794-984b-474e-a140-606ec05b28a3

(gdb) l *ceph_encode_encrypted_dname+0x4a5
0xffffffff829d6605 is in ceph_encode_encrypted_dname (fs/ceph/crypto.c:273).
268		u32 len;
269		int name_len = elen;
270		int ret;
271		u8 *cryptbuf = NULL;
272	
273		buf[elen] = '\0';
274		/* Handle the special case of snapshot names that start with
'_' */
275		if (ceph_snap(dir) == CEPH_SNAPDIR && *p == '_') {
276			dir = parse_longname(parent, p, &name_len);
277			if (IS_ERR(dir))
(gdb) l *ceph_mdsc_build_path+0x710
0xffffffff829a4c70 is in ceph_mdsc_build_path (fs/ceph/mds_client.c:2769).
2764					dput(cur);
2765					return ERR_PTR(ret);
2766				}
2767	
2768				if
(fscrypt_has_encryption_key(d_inode(parent))) {
2769					len =
ceph_encode_encrypted_dname(d_inode(parent),
2770									  buf,
len);
2771					if (len < 0) {
2772						dput(parent);
2773						dput(cur);

Thanks,
Slava.


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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-17 22:04               ` Viacheslav Dubeyko
@ 2025-02-18  1:21                 ` Al Viro
  2025-02-18 23:52                   ` Al Viro
  0 siblings, 1 reply; 21+ messages in thread
From: Al Viro @ 2025-02-18  1:21 UTC (permalink / raw)
  To: Viacheslav Dubeyko
  Cc: luis@igalia.com, ceph-devel@vger.kernel.org, jlayton@kernel.org,
	idryomov@gmail.com, linux-fsdevel@vger.kernel.org

On Mon, Feb 17, 2025 at 10:04:50PM +0000, Viacheslav Dubeyko wrote:

> [  326.477120] This frame has 1 object:
> [  326.477466]  [32, 287) 'buf'

> (gdb) l *ceph_encode_encrypted_dname+0x4a5
> 0xffffffff829d6605 is in ceph_encode_encrypted_dname (fs/ceph/crypto.c:273).
> 268		u32 len;
> 269		int name_len = elen;
> 270		int ret;
> 271		u8 *cryptbuf = NULL;
> 272	
> 273		buf[elen] = '\0';

Cute...  The fix is obvious (should be
                        char buf[NAME_MAX + 1];
rather than
                        char buf[NAME_MAX];
), but the funny part is that it had been a bug all along -
if you give the mainline a name that has a 255-character component
that happens to start with _, you'll get buf[] filled with a copy
of that component (no NUL in sight) and have it passed as 'name' to
parse_longname() that starts with

        /* Skip initial '_' */
	name++;
	name_end = strrchr(name, '_');

See the problem?  strrchr() expects a NUL-terminated string; giving it an
array that has no zero bytes in it is an UB.

That one is -stable fodder on its own, IMO...

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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-18  1:21                 ` Al Viro
@ 2025-02-18 23:52                   ` Al Viro
  2025-02-19  0:58                     ` Viacheslav Dubeyko
  0 siblings, 1 reply; 21+ messages in thread
From: Al Viro @ 2025-02-18 23:52 UTC (permalink / raw)
  To: Viacheslav Dubeyko
  Cc: luis@igalia.com, ceph-devel@vger.kernel.org, jlayton@kernel.org,
	idryomov@gmail.com, linux-fsdevel@vger.kernel.org

On Tue, Feb 18, 2025 at 01:21:32AM +0000, Al Viro wrote:

> See the problem?  strrchr() expects a NUL-terminated string; giving it an
> array that has no zero bytes in it is an UB.
> 
> That one is -stable fodder on its own, IMO...

FWIW, it's more unpleasant; there are other call chains for parse_longname()
where it's not feasible to NUL-terminate in place.  I suspect that the
patch below is a better way to handle that.  Comments?

From ed016e5ea89550b567306207ba3ca8b60e147d89 Mon Sep 17 00:00:00 2001
From: Al Viro <viro@zeniv.linux.org.uk>
Date: Tue, 18 Feb 2025 17:57:17 -0500
Subject: [PATCH 1/3] [ceph] parse_longname(): strrchr() expects NUL-terminated
 string

... and parse_longname() is not guaranteed that.  That's the reason
why it uses kmemdup_nul() to build the argument for kstrtou64();
the problem is, kstrtou64() is not the only thing that need it.

Just get a NUL-terminated copy of the entire thing and be done
with that...

Fixes: dd66df0053ef "ceph: add support for encrypted snapshot names"
Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
---
 fs/ceph/crypto.c | 32 ++++++++++++--------------------
 1 file changed, 12 insertions(+), 20 deletions(-)

diff --git a/fs/ceph/crypto.c b/fs/ceph/crypto.c
index 3b3c4d8d401e..164e7981aecb 100644
--- a/fs/ceph/crypto.c
+++ b/fs/ceph/crypto.c
@@ -215,35 +215,30 @@ static struct inode *parse_longname(const struct inode *parent,
 	struct ceph_client *cl = ceph_inode_to_client(parent);
 	struct inode *dir = NULL;
 	struct ceph_vino vino = { .snap = CEPH_NOSNAP };
-	char *inode_number;
 	char *name_end;
-	int orig_len = *name_len;
 	int ret = -EIO;
-
+	/* NUL-terminate */
+	char *s __free(kfree) = kmemdup_nul(name, *name_len, GFP_KERNEL);
+	if (!s)
+		return ERR_PTR(-ENOMEM);
 	/* Skip initial '_' */
-	name++;
-	name_end = strrchr(name, '_');
+	s++;
+	name_end = strrchr(s, '_');
 	if (!name_end) {
-		doutc(cl, "failed to parse long snapshot name: %s\n", name);
+		doutc(cl, "failed to parse long snapshot name: %s\n", s);
 		return ERR_PTR(-EIO);
 	}
-	*name_len = (name_end - name);
+	*name_len = (name_end - s);
 	if (*name_len <= 0) {
 		pr_err_client(cl, "failed to parse long snapshot name\n");
 		return ERR_PTR(-EIO);
 	}
 
 	/* Get the inode number */
-	inode_number = kmemdup_nul(name_end + 1,
-				   orig_len - *name_len - 2,
-				   GFP_KERNEL);
-	if (!inode_number)
-		return ERR_PTR(-ENOMEM);
-	ret = kstrtou64(inode_number, 10, &vino.ino);
+	ret = kstrtou64(name_end + 1, 10, &vino.ino);
 	if (ret) {
-		doutc(cl, "failed to parse inode number: %s\n", name);
-		dir = ERR_PTR(ret);
-		goto out;
+		doutc(cl, "failed to parse inode number: %s\n", s);
+		return ERR_PTR(ret);
 	}
 
 	/* And finally the inode */
@@ -252,11 +247,8 @@ static struct inode *parse_longname(const struct inode *parent,
 		/* This can happen if we're not mounting cephfs on the root */
 		dir = ceph_get_inode(parent->i_sb, vino, NULL);
 		if (IS_ERR(dir))
-			doutc(cl, "can't find inode %s (%s)\n", inode_number, name);
+			doutc(cl, "can't find inode %s (%s)\n", name_end + 1, name);
 	}
-
-out:
-	kfree(inode_number);
 	return dir;
 }
 
-- 
2.39.5


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

* RE: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-18 23:52                   ` Al Viro
@ 2025-02-19  0:58                     ` Viacheslav Dubeyko
  2025-02-19  2:18                       ` Al Viro
  0 siblings, 1 reply; 21+ messages in thread
From: Viacheslav Dubeyko @ 2025-02-19  0:58 UTC (permalink / raw)
  To: viro@zeniv.linux.org.uk
  Cc: luis@igalia.com, ceph-devel@vger.kernel.org, jlayton@kernel.org,
	linux-fsdevel@vger.kernel.org, idryomov@gmail.com

On Tue, 2025-02-18 at 23:52 +0000, Al Viro wrote:
> On Tue, Feb 18, 2025 at 01:21:32AM +0000, Al Viro wrote:
> 
> > See the problem?  strrchr() expects a NUL-terminated string; giving it an
> > array that has no zero bytes in it is an UB.
> > 
> > That one is -stable fodder on its own, IMO...
> 
> FWIW, it's more unpleasant; there are other call chains for parse_longname()
> where it's not feasible to NUL-terminate in place.  I suspect that the
> patch below is a better way to handle that.  Comments?
> 

Let me test the patch.

> From ed016e5ea89550b567306207ba3ca8b60e147d89 Mon Sep 17 00:00:00 2001
> From: Al Viro <viro@zeniv.linux.org.uk>
> Date: Tue, 18 Feb 2025 17:57:17 -0500
> Subject: [PATCH 1/3] [ceph] parse_longname(): strrchr() expects NUL-terminated
>  string
> 
> ... and parse_longname() is not guaranteed that.  That's the reason
> why it uses kmemdup_nul() to build the argument for kstrtou64();
> the problem is, kstrtou64() is not the only thing that need it.
> 
> Just get a NUL-terminated copy of the entire thing and be done
> with that...
> 
> Fixes: dd66df0053ef "ceph: add support for encrypted snapshot names"
> Signed-off-by: Al Viro <viro@zeniv.linux.org.uk>
> ---
>  fs/ceph/crypto.c | 32 ++++++++++++--------------------
>  1 file changed, 12 insertions(+), 20 deletions(-)
> 
> diff --git a/fs/ceph/crypto.c b/fs/ceph/crypto.c
> index 3b3c4d8d401e..164e7981aecb 100644
> --- a/fs/ceph/crypto.c
> +++ b/fs/ceph/crypto.c
> @@ -215,35 +215,30 @@ static struct inode *parse_longname(const struct inode *parent,
>  	struct ceph_client *cl = ceph_inode_to_client(parent);
>  	struct inode *dir = NULL;
>  	struct ceph_vino vino = { .snap = CEPH_NOSNAP };
> -	char *inode_number;
>  	char *name_end;
> -	int orig_len = *name_len;
>  	int ret = -EIO;
> -
> +	/* NUL-terminate */
> +	char *s __free(kfree) = kmemdup_nul(name, *name_len, GFP_KERNEL);

Maybe, str here? The s looks too extreme for my taste. :)

> +	if (!s)
> +		return ERR_PTR(-ENOMEM);
>  	/* Skip initial '_' */
> -	name++;
> -	name_end = strrchr(name, '_');
> +	s++;
> +	name_end = strrchr(s, '_');
>  	if (!name_end) {
> -		doutc(cl, "failed to parse long snapshot name: %s\n", name);
> +		doutc(cl, "failed to parse long snapshot name: %s\n", s);
>  		return ERR_PTR(-EIO);
>  	}
> -	*name_len = (name_end - name);
> +	*name_len = (name_end - s);
>  	if (*name_len <= 0) {
>  		pr_err_client(cl, "failed to parse long snapshot name\n");
>  		return ERR_PTR(-EIO);
>  	}
>  
>  	/* Get the inode number */
> -	inode_number = kmemdup_nul(name_end + 1,
> -				   orig_len - *name_len - 2,
> -				   GFP_KERNEL);
> -	if (!inode_number)
> -		return ERR_PTR(-ENOMEM);
> -	ret = kstrtou64(inode_number, 10, &vino.ino);
> +	ret = kstrtou64(name_end + 1, 10, &vino.ino);

The inode_number explains by itself what we are converting here. But name_end
sounds confusing and not informative for my taste. But I could have a bad taste.
:)

>  	if (ret) {
> -		doutc(cl, "failed to parse inode number: %s\n", name);
> -		dir = ERR_PTR(ret);
> -		goto out;
> +		doutc(cl, "failed to parse inode number: %s\n", s);
> +		return ERR_PTR(ret);
>  	}
>  
>  	/* And finally the inode */
> @@ -252,11 +247,8 @@ static struct inode *parse_longname(const struct inode *parent,
>  		/* This can happen if we're not mounting cephfs on the root */
>  		dir = ceph_get_inode(parent->i_sb, vino, NULL);
>  		if (IS_ERR(dir))
> -			doutc(cl, "can't find inode %s (%s)\n", inode_number, name);
> +			doutc(cl, "can't find inode %s (%s)\n", name_end + 1, name);

It's not critical, but we use name_end + 1 two times. Potentially, it could be a
source of somebody mistake in the future.

Thanks,
Slava.

>  	}
> -
> -out:
> -	kfree(inode_number);
>  	return dir;
>  }
>  


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

* Re: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-19  0:58                     ` Viacheslav Dubeyko
@ 2025-02-19  2:18                       ` Al Viro
  2025-02-19 23:22                         ` Viacheslav Dubeyko
  2025-02-21  1:21                         ` Viacheslav Dubeyko
  0 siblings, 2 replies; 21+ messages in thread
From: Al Viro @ 2025-02-19  2:18 UTC (permalink / raw)
  To: Viacheslav Dubeyko
  Cc: luis@igalia.com, ceph-devel@vger.kernel.org, jlayton@kernel.org,
	linux-fsdevel@vger.kernel.org, idryomov@gmail.com

On Wed, Feb 19, 2025 at 12:58:54AM +0000, Viacheslav Dubeyko wrote:
> On Tue, 2025-02-18 at 23:52 +0000, Al Viro wrote:
> > On Tue, Feb 18, 2025 at 01:21:32AM +0000, Al Viro wrote:
> > 
> > > See the problem?  strrchr() expects a NUL-terminated string; giving it an
> > > array that has no zero bytes in it is an UB.
> > > 
> > > That one is -stable fodder on its own, IMO...
> > 
> > FWIW, it's more unpleasant; there are other call chains for parse_longname()
> > where it's not feasible to NUL-terminate in place.  I suspect that the
> > patch below is a better way to handle that.  Comments?
> > 
> 
> Let me test the patch.

That one is on top of mainline (-rc2); the entire branch is

git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name

The first commit in there is this one, then two posted earlier rebased
on top of that (without the "NUL-terminate in place" in the last one,
which is what tripped KASAN and is no longer needed due to the first
commit).

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

* RE: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-19  2:18                       ` Al Viro
@ 2025-02-19 23:22                         ` Viacheslav Dubeyko
  2025-02-21  1:21                         ` Viacheslav Dubeyko
  1 sibling, 0 replies; 21+ messages in thread
From: Viacheslav Dubeyko @ 2025-02-19 23:22 UTC (permalink / raw)
  To: viro@zeniv.linux.org.uk
  Cc: luis@igalia.com, ceph-devel@vger.kernel.org, jlayton@kernel.org,
	linux-fsdevel@vger.kernel.org, idryomov@gmail.com

On Wed, 2025-02-19 at 02:18 +0000, Al Viro wrote:
> On Wed, Feb 19, 2025 at 12:58:54AM +0000, Viacheslav Dubeyko wrote:
> > On Tue, 2025-02-18 at 23:52 +0000, Al Viro wrote:
> > > On Tue, Feb 18, 2025 at 01:21:32AM +0000, Al Viro wrote:
> > > 
> > > > See the problem?  strrchr() expects a NUL-terminated string; giving it an
> > > > array that has no zero bytes in it is an UB.
> > > > 
> > > > That one is -stable fodder on its own, IMO...
> > > 
> > > FWIW, it's more unpleasant; there are other call chains for parse_longname()
> > > where it's not feasible to NUL-terminate in place.  I suspect that the
> > > patch below is a better way to handle that.  Comments?
> > > 
> > 
> > Let me test the patch.
> 
> That one is on top of mainline (-rc2); the entire branch is
> 
> git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name
> 
> The first commit in there is this one, then two posted earlier rebased
> on top of that (without the "NUL-terminate in place" in the last one,
> which is what tripped KASAN and is no longer needed due to the first
> commit).

I did run xfstests and I don't see any issues with the patchset.
But I need to play with snapshots yet to check that I cannot trigger
any issue there. Let me spend some time for this.

Thanks,
Slava.


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

* RE: [RFC] odd check in ceph_encode_encrypted_dname()
  2025-02-19  2:18                       ` Al Viro
  2025-02-19 23:22                         ` Viacheslav Dubeyko
@ 2025-02-21  1:21                         ` Viacheslav Dubeyko
  1 sibling, 0 replies; 21+ messages in thread
From: Viacheslav Dubeyko @ 2025-02-21  1:21 UTC (permalink / raw)
  To: viro@zeniv.linux.org.uk
  Cc: luis@igalia.com, ceph-devel@vger.kernel.org, jlayton@kernel.org,
	linux-fsdevel@vger.kernel.org, idryomov@gmail.com

On Wed, 2025-02-19 at 02:18 +0000, Al Viro wrote:
> On Wed, Feb 19, 2025 at 12:58:54AM +0000, Viacheslav Dubeyko wrote:
> > On Tue, 2025-02-18 at 23:52 +0000, Al Viro wrote:
> > > On Tue, Feb 18, 2025 at 01:21:32AM +0000, Al Viro wrote:
> > > 
> > > > See the problem?  strrchr() expects a NUL-terminated string; giving it an
> > > > array that has no zero bytes in it is an UB.
> > > > 
> > > > That one is -stable fodder on its own, IMO...
> > > 
> > > FWIW, it's more unpleasant; there are other call chains for parse_longname()
> > > where it's not feasible to NUL-terminate in place.  I suspect that the
> > > patch below is a better way to handle that.  Comments?
> > > 
> > 
> > Let me test the patch.
> 
> That one is on top of mainline (-rc2); the entire branch is
> 
> git://git.kernel.org/pub/scm/linux/kernel/git/viro/vfs.git #d_name
> 
> The first commit in there is this one, then two posted earlier rebased
> on top of that (without the "NUL-terminate in place" in the last one,
> which is what tripped KASAN and is no longer needed due to the first
> commit).

I have tested patchset by xfstests and I have played with snapshots manually. I
haven't found any new or critical issues. Everything looks good.

Tested-by: Viacheslav Dubeyko <Slava.Dubeyko@ibm.com>

Thanks,
Slava.


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

end of thread, other threads:[~2025-02-21  1:21 UTC | newest]

Thread overview: 21+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-02-14  2:47 [RFC] odd check in ceph_encode_encrypted_dname() Al Viro
2025-02-14  3:28 ` Al Viro
2025-02-14 14:05   ` Luis Henriques
2025-02-14 15:41   ` Jeff Layton
2025-02-14 16:05     ` Luis Henriques
2025-02-15  4:46       ` Al Viro
2025-02-15  4:47         ` [PATCH 1/2] prep for ceph_encode_encrypted_fname() fixes Al Viro
2025-02-15 12:41           ` Jeff Layton
2025-02-15  4:47         ` [PATCH 2/2] ceph: fix a race with rename() in ceph_mdsc_build_path() Al Viro
2025-02-15 12:42           ` Jeff Layton
2025-02-15 15:39         ` [RFC] odd check in ceph_encode_encrypted_dname() Luis Henriques
2025-02-17 17:56           ` Viacheslav Dubeyko
2025-02-17 18:48             ` Luis Henriques
2025-02-17 22:04               ` Viacheslav Dubeyko
2025-02-18  1:21                 ` Al Viro
2025-02-18 23:52                   ` Al Viro
2025-02-19  0:58                     ` Viacheslav Dubeyko
2025-02-19  2:18                       ` Al Viro
2025-02-19 23:22                         ` Viacheslav Dubeyko
2025-02-21  1:21                         ` Viacheslav Dubeyko
2025-02-14 15:30 ` Jeff Layton

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).