efi: Validate UEFI boot variables
Matthew Garrett [Mon, 30 Apr 2012 20:11:30 +0000 (16:11 -0400)]
A common flaw in UEFI systems is a refusal to POST triggered by a malformed
boot variable. Once in this state, machines may only be restored by
reflashing their firmware with an external hardware device. While this is
obviously a firmware bug, the serious nature of the outcome suggests that
operating systems should filter their variable writes in order to prevent
a malicious user from rendering the machine unusable.

Signed-off-by: Matthew Garrett <mjg@redhat.com>
Cc: stable@vger.kernel.org
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>

drivers/firmware/efivars.c

index d25599f..891e467 100644 (file)
@@ -191,6 +191,176 @@ utf16_strncmp(const efi_char16_t *a, const efi_char16_t *b, size_t len)
        }
 }
 
+static bool
+validate_device_path(struct efi_variable *var, int match, u8 *buffer, int len)
+{
+       struct efi_generic_dev_path *node;
+       int offset = 0;
+
+       node = (struct efi_generic_dev_path *)buffer;
+
+       while (offset < len) {
+               offset += node->length;
+
+               if (offset > len)
+                       return false;
+
+               if ((node->type == EFI_DEV_END_PATH ||
+                    node->type == EFI_DEV_END_PATH2) &&
+                   node->sub_type == EFI_DEV_END_ENTIRE)
+                       return true;
+
+               node = (struct efi_generic_dev_path *)(buffer + offset);
+       }
+
+       /*
+        * If we're here then either node->length pointed past the end
+        * of the buffer or we reached the end of the buffer without
+        * finding a device path end node.
+        */
+       return false;
+}
+
+static bool
+validate_boot_order(struct efi_variable *var, int match, u8 *buffer, int len)
+{
+       /* An array of 16-bit integers */
+       if ((len % 2) != 0)
+               return false;
+
+       return true;
+}
+
+static bool
+validate_load_option(struct efi_variable *var, int match, u8 *buffer, int len)
+{
+       u16 filepathlength;
+       int i, desclength = 0;
+
+       /* Either "Boot" or "Driver" followed by four digits of hex */
+       for (i = match; i < match+4; i++) {
+               if (hex_to_bin(var->VariableName[i] & 0xff) < 0)
+                       return true;
+       }
+
+       /* A valid entry must be at least 6 bytes */
+       if (len < 6)
+               return false;
+
+       filepathlength = buffer[4] | buffer[5] << 8;
+
+       /*
+        * There's no stored length for the description, so it has to be
+        * found by hand
+        */
+       desclength = utf16_strsize((efi_char16_t *)(buffer + 6), len) + 2;
+
+       /* Each boot entry must have a descriptor */
+       if (!desclength)
+               return false;
+
+       /*
+        * If the sum of the length of the description, the claimed filepath
+        * length and the original header are greater than the length of the
+        * variable, it's malformed
+        */
+       if ((desclength + filepathlength + 6) > len)
+               return false;
+
+       /*
+        * And, finally, check the filepath
+        */
+       return validate_device_path(var, match, buffer + desclength + 6,
+                                   filepathlength);
+}
+
+static bool
+validate_uint16(struct efi_variable *var, int match, u8 *buffer, int len)
+{
+       /* A single 16-bit integer */
+       if (len != 2)
+               return false;
+
+       return true;
+}
+
+static bool
+validate_ascii_string(struct efi_variable *var, int match, u8 *buffer, int len)
+{
+       int i;
+
+       for (i = 0; i < len; i++) {
+               if (buffer[i] > 127)
+                       return false;
+
+               if (buffer[i] == 0)
+                       return true;
+       }
+
+       return false;
+}
+
+struct variable_validate {
+       char *name;
+       bool (*validate)(struct efi_variable *var, int match, u8 *data,
+                        int len);
+};
+
+static const struct variable_validate variable_validate[] = {
+       { "BootNext", validate_uint16 },
+       { "BootOrder", validate_boot_order },
+       { "DriverOrder", validate_boot_order },
+       { "Boot*", validate_load_option },
+       { "Driver*", validate_load_option },
+       { "ConIn", validate_device_path },
+       { "ConInDev", validate_device_path },
+       { "ConOut", validate_device_path },
+       { "ConOutDev", validate_device_path },
+       { "ErrOut", validate_device_path },
+       { "ErrOutDev", validate_device_path },
+       { "Timeout", validate_uint16 },
+       { "Lang", validate_ascii_string },
+       { "PlatformLang", validate_ascii_string },
+       { "", NULL },
+};
+
+static bool
+validate_var(struct efi_variable *var, u8 *data, int len)
+{
+       int i;
+       u16 *unicode_name = var->VariableName;
+
+       for (i = 0; variable_validate[i].validate != NULL; i++) {
+               const char *name = variable_validate[i].name;
+               int match;
+
+               for (match = 0; ; match++) {
+                       char c = name[match];
+                       u16 u = unicode_name[match];
+
+                       /* All special variables are plain ascii */
+                       if (u > 127)
+                               return true;
+
+                       /* Wildcard in the matching name means we've matched */
+                       if (c == '*')
+                               return variable_validate[i].validate(var,
+                                                            match, data, len);
+
+                       /* Case sensitive match */
+                       if (c != u)
+                               break;
+
+                       /* Reached the end of the string while matching */
+                       if (!c)
+                               return variable_validate[i].validate(var,
+                                                            match, data, len);
+               }
+       }
+
+       return true;
+}
+
 static efi_status_t
 get_var_data_locked(struct efivars *efivars, struct efi_variable *var)
 {
@@ -324,6 +494,12 @@ efivar_store_raw(struct efivar_entry *entry, const char *buf, size_t count)
                return -EINVAL;
        }
 
+       if ((new_var->Attributes & ~EFI_VARIABLE_MASK) != 0 ||
+           validate_var(new_var, new_var->Data, new_var->DataSize) == false) {
+               printk(KERN_ERR "efivars: Malformed variable content\n");
+               return -EINVAL;
+       }
+
        spin_lock(&efivars->lock);
        status = efivars->ops->set_variable(new_var->VariableName,
                                            &new_var->VendorGuid,
@@ -626,6 +802,12 @@ static ssize_t efivar_create(struct file *filp, struct kobject *kobj,
        if (!capable(CAP_SYS_ADMIN))
                return -EACCES;
 
+       if ((new_var->Attributes & ~EFI_VARIABLE_MASK) != 0 ||
+           validate_var(new_var, new_var->Data, new_var->DataSize) == false) {
+               printk(KERN_ERR "efivars: Malformed variable content\n");
+               return -EINVAL;
+       }
+
        spin_lock(&efivars->lock);
 
        /*