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>
154 lines
3.2 KiB
C
154 lines
3.2 KiB
C
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (C) 2012 Red Hat, Inc.
|
|
* Copyright (C) 2012 Jeremy Kerr <jeremy.kerr@canonical.com>
|
|
*/
|
|
|
|
#include <linux/efi.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/fs.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/mount.h>
|
|
|
|
#include "internal.h"
|
|
|
|
static ssize_t efivarfs_file_write(struct file *file,
|
|
const char __user *userbuf, size_t count, loff_t *ppos)
|
|
{
|
|
struct efivar_entry *var = file->private_data;
|
|
void *data;
|
|
u32 attributes;
|
|
struct inode *inode = file->f_mapping->host;
|
|
unsigned long datasize = count - sizeof(attributes);
|
|
ssize_t bytes;
|
|
bool set = false;
|
|
|
|
if (count < sizeof(attributes))
|
|
return -EINVAL;
|
|
|
|
if (copy_from_user(&attributes, userbuf, sizeof(attributes)))
|
|
return -EFAULT;
|
|
|
|
if (attributes & ~(EFI_VARIABLE_MASK))
|
|
return -EINVAL;
|
|
|
|
data = memdup_user(userbuf + sizeof(attributes), datasize);
|
|
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) {
|
|
if (bytes == -ENOENT)
|
|
bytes = -EIO;
|
|
goto out;
|
|
}
|
|
|
|
if (bytes == -ENOENT) {
|
|
/*
|
|
* zero size signals to release that the write deleted
|
|
* the variable
|
|
*/
|
|
i_size_write(inode, 0);
|
|
} else {
|
|
i_size_write(inode, datasize + sizeof(attributes));
|
|
inode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));
|
|
}
|
|
|
|
bytes = count;
|
|
|
|
out:
|
|
inode_unlock(inode);
|
|
|
|
kfree(data);
|
|
|
|
return bytes;
|
|
}
|
|
|
|
static ssize_t efivarfs_file_read(struct file *file, char __user *userbuf,
|
|
size_t count, loff_t *ppos)
|
|
{
|
|
struct efivar_entry *var = file->private_data;
|
|
unsigned long datasize = 0;
|
|
u32 attributes;
|
|
void *data;
|
|
ssize_t size = 0;
|
|
int err;
|
|
|
|
while (!__ratelimit(&file->f_cred->user->ratelimit))
|
|
msleep(50);
|
|
|
|
err = efivar_entry_size(var, &datasize);
|
|
|
|
/*
|
|
* efivarfs represents uncommitted variables with
|
|
* zero-length files. Reading them should return EOF.
|
|
*/
|
|
if (err == -ENOENT)
|
|
return 0;
|
|
else if (err)
|
|
return err;
|
|
|
|
data = kmalloc(datasize + sizeof(attributes), GFP_KERNEL);
|
|
|
|
if (!data)
|
|
return -ENOMEM;
|
|
|
|
size = efivar_entry_get(var, &attributes, &datasize,
|
|
data + sizeof(attributes));
|
|
if (size)
|
|
goto out_free;
|
|
|
|
memcpy(data, &attributes, sizeof(attributes));
|
|
size = simple_read_from_buffer(userbuf, count, ppos,
|
|
data, datasize + sizeof(attributes));
|
|
out_free:
|
|
kfree(data);
|
|
|
|
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 = efivarfs_file_open,
|
|
.read = efivarfs_file_read,
|
|
.write = efivarfs_file_write,
|
|
.release = efivarfs_file_release,
|
|
};
|