]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
efivarfs: fix error on write to new variable leaving remnants
authorJames Bottomley <James.Bottomley@HansenPartnership.com>
Sun, 19 Jan 2025 15:12:12 +0000 (10:12 -0500)
committerArd Biesheuvel <ardb@kernel.org>
Sun, 19 Jan 2025 16:50:26 +0000 (17:50 +0100)
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.
In the case of multiple racing opens and closes, the open is counted
to ensure that the zero size check is done on the last close.

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 last 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 zero size variable that doesn't exist in the EFI
table.

Signed-off-by: James Bottomley <James.Bottomley@HansenPartnership.com>
Signed-off-by: Ard Biesheuvel <ardb@kernel.org>
fs/efivarfs/file.c
fs/efivarfs/internal.h
fs/efivarfs/super.c

index 23c51d62f902b0dbebbc8f1f0fe995fa93d6aa72..cb1b6d0c34545c701ca36bc881ee922d1ff41bac 100644 (file)
@@ -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 (var->removed) {
+               /*
+                * 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,36 @@ out_free:
        return size;
 }
 
+static int efivarfs_file_release(struct inode *inode, struct file *file)
+{
+       struct efivar_entry *var = inode->i_private;
+
+       inode_lock(inode);
+       var->removed = (--var->open_count == 0 && i_size_read(inode) == 0);
+       inode_unlock(inode);
+
+       if (var->removed)
+               simple_recursive_removal(file->f_path.dentry, NULL);
+
+       return 0;
+}
+
+static int efivarfs_file_open(struct inode *inode, struct file *file)
+{
+       struct efivar_entry *entry = inode->i_private;
+
+       file->private_data = entry;
+
+       inode_lock(inode);
+       entry->open_count++;
+       inode_unlock(inode);
+
+       return 0;
+}
+
 const struct file_operations efivarfs_file_operations = {
-       .open   = simple_open,
-       .read   = efivarfs_file_read,
-       .write  = efivarfs_file_write,
+       .open           = efivarfs_file_open,
+       .read           = efivarfs_file_read,
+       .write          = efivarfs_file_write,
+       .release        = efivarfs_file_release,
 };
index 4366f7949614e2a32897c1f1b45d6883577e8afb..1f84844fe0326bce798f01d4d2385c7f934a21ee 100644 (file)
@@ -27,6 +27,8 @@ struct efi_variable {
 struct efivar_entry {
        struct efi_variable var;
        struct inode vfs_inode;
+       unsigned long open_count;
+       bool removed;
 };
 
 static inline struct efivar_entry *efivar_entry(struct inode *inode)
index 7b3650c97e60c92f915ec7b62ba1bdc8172f2b60..89010c5878ce9c4383026a0fcdec9323eedbe53f 100644 (file)
@@ -47,6 +47,7 @@ static struct inode *efivarfs_alloc_inode(struct super_block *sb)
                return NULL;
 
        inode_init_once(&entry->vfs_inode);
+       entry->removed = false;
 
        return &entry->vfs_inode;
 }