According to section 7.1 of the UEFI spec, Runtime Services are not fully
reentrant, and there are particular combinations of calls that need to be
serialized. Use a spinlock to serialize all Runtime Services with respect
to all others, even if this is more than strictly needed.

Signed-off-by: Ard Biesheuvel <[email protected]>
---
 arch/x86/include/asm/efi.h              |   2 +
 drivers/firmware/efi/runtime-wrappers.c | 154 +++++++++++++++++++++++++++++---
 2 files changed, 146 insertions(+), 10 deletions(-)

diff --git a/arch/x86/include/asm/efi.h b/arch/x86/include/asm/efi.h
index 044a2fd3c5fe..b39ee5f2c02d 100644
--- a/arch/x86/include/asm/efi.h
+++ b/arch/x86/include/asm/efi.h
@@ -86,6 +86,8 @@ extern void __iomem *efi_ioremap(unsigned long addr, unsigned 
long size,
 
 #endif /* CONFIG_X86_32 */
 
+#define efi_in_nmi()   in_nmi()
+
 extern int add_efi_memmap;
 extern struct efi_scratch efi_scratch;
 extern void efi_set_executable(efi_memory_desc_t *md, bool executable);
diff --git a/drivers/firmware/efi/runtime-wrappers.c 
b/drivers/firmware/efi/runtime-wrappers.c
index 10daa4bbb258..9694cba665c4 100644
--- a/drivers/firmware/efi/runtime-wrappers.c
+++ b/drivers/firmware/efi/runtime-wrappers.c
@@ -14,11 +14,83 @@
  * This file is released under the GPLv2.
  */
 
+#include <linux/bug.h>
 #include <linux/efi.h>
-#include <linux/spinlock.h>             /* spinlock_t */
+#include <linux/mutex.h>
+#include <linux/spinlock.h>
 #include <asm/efi.h>
 
 /*
+ * According to section 7.1 of the UEFI spec, Runtime Services are not fully
+ * reentrant, and there are particular combinations of calls that need to be
+ * serialized. (source: UEFI Specification v2.4A)
+ *
+ * Table 31. Rules for Reentry Into Runtime Services
+ * +------------------------------------+-------------------------------+
+ * | If previous call is busy in       | Forbidden to call             |
+ * +------------------------------------+-------------------------------+
+ * | Any                               | SetVirtualAddressMap()        |
+ * +------------------------------------+-------------------------------+
+ * | ConvertPointer()                  | ConvertPointer()              |
+ * +------------------------------------+-------------------------------+
+ * | SetVariable()                     | ResetSystem()                 |
+ * | UpdateCapsule()                   |                               |
+ * | SetTime()                         |                               |
+ * | SetWakeupTime()                   |                               |
+ * | GetNextHighMonotonicCount()       |                               |
+ * +------------------------------------+-------------------------------+
+ * | GetVariable()                     | GetVariable()                 |
+ * | GetNextVariableName()             | GetNextVariableName()         |
+ * | SetVariable()                     | SetVariable()                 |
+ * | QueryVariableInfo()               | QueryVariableInfo()           |
+ * | UpdateCapsule()                   | UpdateCapsule()               |
+ * | QueryCapsuleCapabilities()                | QueryCapsuleCapabilities()    
|
+ * | GetNextHighMonotonicCount()       | GetNextHighMonotonicCount()   |
+ * +------------------------------------+-------------------------------+
+ * | GetTime()                         | GetTime()                     |
+ * | SetTime()                         | SetTime()                     |
+ * | GetWakeupTime()                   | GetWakeupTime()               |
+ * | SetWakeupTime()                   | SetWakeupTime()               |
+ * +------------------------------------+-------------------------------+
+ *
+ * Due to the fact that the EFI pstore may write to the variable store in
+ * interrupt context, we need to use a spinlock for at least the groups that
+ * contain SetVariable() and QueryVariableInfo(). That leaves little else, as
+ * none of the remaining functions are actually ever called at runtime.
+ * So let's just use a single spinlock to serialize all Runtime Services calls.
+ */
+static DEFINE_SPINLOCK(efi_runtime_lock);
+
+/*
+ * Some runtime services calls can be reentrant under NMI, even if the table
+ * above says they are not. (source: UEFI Specification v2.4A)
+ *
+ * Table 32. Functions that may be called after Machine Check, INIT and NMI
+ * +----------------------------+------------------------------------------+
+ * | Function                  | Called after Machine Check, INIT and NMI |
+ * +----------------------------+------------------------------------------+
+ * | GetTime()                 | Yes, even if previously busy.            |
+ * | GetVariable()             | Yes, even if previously busy             |
+ * | GetNextVariableName()     | Yes, even if previously busy             |
+ * | QueryVariableInfo()       | Yes, even if previously busy             |
+ * | SetVariable()             | Yes, even if previously busy             |
+ * | UpdateCapsule()           | Yes, even if previously busy             |
+ * | QueryCapsuleCapabilities()        | Yes, even if previously busy          
   |
+ * | ResetSystem()             | Yes, even if previously busy             |
+ * +----------------------------+------------------------------------------+
+ *
+ * In order to prevent deadlocks under NMI, the wrappers for these functions
+ * may only grab the efi_runtime_lock or rtc_lock spinlocks if !efi_in_nmi().
+ * However, not all of the services listed are reachable through NMI code 
paths,
+ * so the the special handling as suggested by the UEFI spec is only 
implemented
+ * for QueryVariableInfo() and SetVariable(), as these can be reached in NMI
+ * context through efi_pstore_write().
+ */
+#ifndef efi_in_nmi
+#define efi_in_nmi()   (0)
+#endif
+
+/*
  * As per commit ef68c8f87ed1 ("x86: Serialize EFI time accesses on rtc_lock"),
  * the EFI specification requires that callers of the time related runtime
  * functions serialize with other CMOS accesses in the kernel, as the EFI time
@@ -32,7 +104,9 @@ static efi_status_t virt_efi_get_time(efi_time_t *tm, 
efi_time_cap_t *tc)
        efi_status_t status;
 
        spin_lock_irqsave(&rtc_lock, flags);
+       spin_lock(&efi_runtime_lock);
        status = efi_call_virt(get_time, tm, tc);
+       spin_unlock(&efi_runtime_lock);
        spin_unlock_irqrestore(&rtc_lock, flags);
        return status;
 }
@@ -43,7 +117,9 @@ static efi_status_t virt_efi_set_time(efi_time_t *tm)
        efi_status_t status;
 
        spin_lock_irqsave(&rtc_lock, flags);
+       spin_lock(&efi_runtime_lock);
        status = efi_call_virt(set_time, tm);
+       spin_unlock(&efi_runtime_lock);
        spin_unlock_irqrestore(&rtc_lock, flags);
        return status;
 }
@@ -56,7 +132,9 @@ static efi_status_t virt_efi_get_wakeup_time(efi_bool_t 
*enabled,
        efi_status_t status;
 
        spin_lock_irqsave(&rtc_lock, flags);
+       spin_lock(&efi_runtime_lock);
        status = efi_call_virt(get_wakeup_time, enabled, pending, tm);
+       spin_unlock(&efi_runtime_lock);
        spin_unlock_irqrestore(&rtc_lock, flags);
        return status;
 }
@@ -67,7 +145,9 @@ static efi_status_t virt_efi_set_wakeup_time(efi_bool_t 
enabled, efi_time_t *tm)
        efi_status_t status;
 
        spin_lock_irqsave(&rtc_lock, flags);
+       spin_lock(&efi_runtime_lock);
        status = efi_call_virt(set_wakeup_time, enabled, tm);
+       spin_unlock(&efi_runtime_lock);
        spin_unlock_irqrestore(&rtc_lock, flags);
        return status;
 }
@@ -78,14 +158,27 @@ static efi_status_t virt_efi_get_variable(efi_char16_t 
*name,
                                          unsigned long *data_size,
                                          void *data)
 {
-       return efi_call_virt(get_variable, name, vendor, attr, data_size, data);
+       unsigned long flags;
+       efi_status_t status;
+
+       spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(get_variable, name, vendor, attr, data_size,
+                              data);
+       spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 static efi_status_t virt_efi_get_next_variable(unsigned long *name_size,
                                               efi_char16_t *name,
                                               efi_guid_t *vendor)
 {
-       return efi_call_virt(get_next_variable, name_size, name, vendor);
+       unsigned long flags;
+       efi_status_t status;
+
+       spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(get_next_variable, name_size, name, vendor);
+       spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 static efi_status_t virt_efi_set_variable(efi_char16_t *name,
@@ -94,7 +187,17 @@ static efi_status_t virt_efi_set_variable(efi_char16_t 
*name,
                                          unsigned long data_size,
                                          void *data)
 {
-       return efi_call_virt(set_variable, name, vendor, attr, data_size, data);
+       unsigned long flags;
+       efi_status_t status;
+       bool __in_nmi = efi_in_nmi();
+
+       if (!__in_nmi)
+               spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(set_variable, name, vendor, attr, data_size,
+                              data);
+       if (!__in_nmi)
+               spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 static efi_status_t virt_efi_query_variable_info(u32 attr,
@@ -102,16 +205,31 @@ static efi_status_t virt_efi_query_variable_info(u32 attr,
                                                 u64 *remaining_space,
                                                 u64 *max_variable_size)
 {
+       unsigned long flags;
+       efi_status_t status;
+       bool __in_nmi = efi_in_nmi();
+
        if (efi.runtime_version < EFI_2_00_SYSTEM_TABLE_REVISION)
                return EFI_UNSUPPORTED;
 
-       return efi_call_virt(query_variable_info, attr, storage_space,
-                            remaining_space, max_variable_size);
+       if (!__in_nmi)
+               spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(query_variable_info, attr, storage_space,
+                              remaining_space, max_variable_size);
+       if (!__in_nmi)
+               spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 static efi_status_t virt_efi_get_next_high_mono_count(u32 *count)
 {
-       return efi_call_virt(get_next_high_mono_count, count);
+       unsigned long flags;
+       efi_status_t status;
+
+       spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(get_next_high_mono_count, count);
+       spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 static void virt_efi_reset_system(int reset_type,
@@ -119,17 +237,27 @@ static void virt_efi_reset_system(int reset_type,
                                  unsigned long data_size,
                                  efi_char16_t *data)
 {
+       unsigned long flags;
+
+       spin_lock_irqsave(&efi_runtime_lock, flags);
        __efi_call_virt(reset_system, reset_type, status, data_size, data);
+       spin_unlock_irqrestore(&efi_runtime_lock, flags);
 }
 
 static efi_status_t virt_efi_update_capsule(efi_capsule_header_t **capsules,
                                            unsigned long count,
                                            unsigned long sg_list)
 {
+       unsigned long flags;
+       efi_status_t status;
+
        if (efi.runtime_version < EFI_2_00_SYSTEM_TABLE_REVISION)
                return EFI_UNSUPPORTED;
 
-       return efi_call_virt(update_capsule, capsules, count, sg_list);
+       spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(update_capsule, capsules, count, sg_list);
+       spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 static efi_status_t virt_efi_query_capsule_caps(efi_capsule_header_t 
**capsules,
@@ -137,11 +265,17 @@ static efi_status_t 
virt_efi_query_capsule_caps(efi_capsule_header_t **capsules,
                                                u64 *max_size,
                                                int *reset_type)
 {
+       unsigned long flags;
+       efi_status_t status;
+
        if (efi.runtime_version < EFI_2_00_SYSTEM_TABLE_REVISION)
                return EFI_UNSUPPORTED;
 
-       return efi_call_virt(query_capsule_caps, capsules, count, max_size,
-                            reset_type);
+       spin_lock_irqsave(&efi_runtime_lock, flags);
+       status = efi_call_virt(query_capsule_caps, capsules, count, max_size,
+                              reset_type);
+       spin_unlock_irqrestore(&efi_runtime_lock, flags);
+       return status;
 }
 
 void efi_native_runtime_setup(void)
-- 
1.8.3.2

--
To unsubscribe from this list: send the line "unsubscribe linux-efi" in
the body of a message to [email protected]
More majordomo info at  http://vger.kernel.org/majordomo-info.html

Reply via email to