Re: [PATCH 6/6] efivarfs: fix error on write to new variable leaving remnants

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



On Wed, 2024-12-11 at 07:39 -0500, James Bottomley wrote:
> On Wed, 2024-12-11 at 12:16 +0100, Christian Brauner wrote:
> > On Tue, Dec 10, 2024 at 12:02:24PM -0500, James Bottomley wrote:
> [...]
> > > +static int efivarfs_file_release(struct inode *inode, struct
> > > file
> > > *file)
> > > +{
> > > +       if (i_size_read(inode) == 0) {
> > > +               drop_nlink(inode);
> > > +               d_delete(file->f_path.dentry);
> > > +               dput(file->f_path.dentry);
> > > +       }
> > 
> > Without wider context the dput() looks UAF-y as __fput() will do:
> > 
> > struct dentry *dentry = file->f_path.dentry;
> > if (file->f_op->release)
> >         file->f_op->release(inode, file);
> > dput(dentry);
> > 
> > Is there an extra reference on file->f_path.dentry taken somewhere?
> 
> Heh, well, this is why I cc'd fsdevel to make sure I got all the fs
> bits I used to be familiar with, but knowledge of which has
> atrophied, correct.
> 
> I think it's paired with the extra dget() just after d_instantiate()
> in fs/efivarfs/inode.c:efivarfs_create().  The reason being this is a
> pseudo-filesystem so all the dentries representing objects have to be
> born with a positive reference count to prevent them being reclaimed
> under memory pressure.

Actually on further testing, this did turn out to be a use after free.
Not because of the dput, but because file->release is called for every
closed filehandle, so if you open the file for creation more than once,
both closes will try to delete it and ... oops.

The way I thought of mediating this is to check d_hashed in the file-
>release to see if the file has already been deleted.  That also means
we need a d_hashed() check in write because we can't resurrect the now
deleted file.  And finally something needs to mediate the check and
remove or check and add, so I used the inode semaphore for that.  The
updated patch is below and now passes the concurrency tests.

Regards,

James

------8>8>8><8<8<8-------------
From: James Bottomley <James.Bottomley@xxxxxxxxxxxxxxxxxxxxx>
Subject: [PATCH 6/6] efivarfs: fix error on write to new variable leaving remnants

Make variable cleanup go through the fops release mechanism and use
zero inode size as the indicator to delete the file.  Since all EFI
variables must have an initial u32 attribute, zero size occurs either
because the update deleted the variable or because an unsuccessful
write after create caused the size never to be set in the first place.

Even though this fixes the bug that a create either not followed by a
write or followed by a write that errored would leave a remnant file
for the variable, the file will appear momentarily globally visible
until the close of the fd deletes it.  This is safe because the normal
filesystem operations will mediate any races; however, it is still
possible for a directory listing at that instant between create and
close contain a variable that doesn't exist in the EFI table.

Signed-off-by: James Bottomley <James.Bottomley@xxxxxxxxxxxxxxxxxxxxx>
---
 fs/efivarfs/file.c | 44 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 35 insertions(+), 9 deletions(-)

diff --git a/fs/efivarfs/file.c b/fs/efivarfs/file.c
index 23c51d62f902..70a673e7fda3 100644
--- a/fs/efivarfs/file.c
+++ b/fs/efivarfs/file.c
@@ -36,28 +36,41 @@ static ssize_t efivarfs_file_write(struct file *file,
 	if (IS_ERR(data))
 		return PTR_ERR(data);
 
+	inode_lock(inode);
+	if (d_unhashed(file->f_path.dentry)) {
+		/*
+		 * file got removed; don't allow a set.  Caused by an
+		 * unsuccessful create or successful delete write
+		 * racing with us.
+		 */
+		bytes = -EIO;
+		goto out;
+	}
+
 	bytes = efivar_entry_set_get_size(var, attributes, &datasize,
 					  data, &set);
-	if (!set && bytes) {
+	if (!set) {
 		if (bytes == -ENOENT)
 			bytes = -EIO;
 		goto out;
 	}
 
 	if (bytes == -ENOENT) {
-		drop_nlink(inode);
-		d_delete(file->f_path.dentry);
-		dput(file->f_path.dentry);
+		/*
+		 * zero size signals to release that the write deleted
+		 * the variable
+		 */
+		i_size_write(inode, 0);
 	} else {
-		inode_lock(inode);
 		i_size_write(inode, datasize + sizeof(attributes));
 		inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
-		inode_unlock(inode);
 	}
 
 	bytes = count;
 
 out:
+	inode_unlock(inode);
+
 	kfree(data);
 
 	return bytes;
@@ -106,8 +119,21 @@ static ssize_t efivarfs_file_read(struct file *file, char __user *userbuf,
 	return size;
 }
 
+static int efivarfs_file_release(struct inode *inode, struct file *file)
+{
+	inode_lock(inode);
+	if (i_size_read(inode) == 0 && !d_unhashed(file->f_path.dentry)) {
+		drop_nlink(inode);
+		d_delete(file->f_path.dentry);
+		dput(file->f_path.dentry);
+	}
+	inode_unlock(inode);
+	return 0;
+}
+
 const struct file_operations efivarfs_file_operations = {
-	.open	= simple_open,
-	.read	= efivarfs_file_read,
-	.write	= efivarfs_file_write,
+	.open		= simple_open,
+	.read		= efivarfs_file_read,
+	.write		= efivarfs_file_write,
+	.release	= efivarfs_file_release,
 };
-- 
2.35.3






[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Security]     [Bugtraq]     [Linux OMAP]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]

  Powered by Linux