diff --git a/OvmfPkg/Include/Library/NestedInterruptTplLib.h b/OvmfPkg/Include/Library/NestedInterruptTplLib.h new file mode 100644 index 0000000000..0ead6e4b34 --- /dev/null +++ b/OvmfPkg/Include/Library/NestedInterruptTplLib.h @@ -0,0 +1,87 @@ +/** @file + Handle raising and lowering TPL from within nested interrupt handlers. + + Allows interrupt handlers to safely raise and lower the TPL to + dispatch event notifications, correctly allowing for nested + interrupts to occur without risking stack exhaustion. + + Copyright (C) 2022, Fen Systems Ltd. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#ifndef __NESTED_INTERRUPT_TPL_LIB__ +#define __NESTED_INTERRUPT_TPL_LIB__ + +#include +#include +#include + +/// +/// State shared between all invocations of a nested interrupt handler. +/// +typedef struct { + /// + /// Highest TPL that is currently the target of a call to + /// RestoreTPL() by an instance of this interrupt handler. + /// + EFI_TPL InProgressRestoreTPL; + /// + /// Flag used to defer a call to RestoreTPL() from an inner instance + /// of the interrupt handler to an outer instance of the same + /// interrupt handler. + /// + BOOLEAN DeferredRestoreTPL; +} NESTED_INTERRUPT_STATE; + +/** + Raise the task priority level to TPL_HIGH_LEVEL. + + @param None. + + @return The task priority level at which the interrupt occurred. +**/ +EFI_TPL +EFIAPI +NestedInterruptRaiseTPL ( + VOID + ); + +/** + Lower the task priority back to the value at which the interrupt + occurred. + + This is unfortunately messy. UEFI requires us to support nested + interrupts, but provides no way for an interrupt handler to call + RestoreTPL() without implicitly re-enabling interrupts. In a + virtual machine, it is possible for a large burst of interrupts to + arrive. We must prevent such a burst from leading to stack + exhaustion, while continuing to allow nested interrupts to occur. + + Since nested interrupts are permitted, an interrupt handler may be + invoked as an inner interrupt handler while an outer instance of the + same interrupt handler is still inside its call to RestoreTPL(). + + To avoid stack exhaustion, this call may therefore (when provably + safe to do so) defer the actual TPL lowering to be performed by an + outer instance of the same interrupt handler. + + @param InterruptedTPL The task priority level at which the interrupt + occurred, as previously returned from + NestedInterruptRaiseTPL(). + + @param SystemContext A pointer to the system context when the + interrupt occurred. + + @param IsrState A pointer to the state shared between all + invocations of the nested interrupt handler. +**/ +VOID +EFIAPI +NestedInterruptRestoreTPL ( + IN EFI_TPL InterruptedTPL, + IN OUT EFI_SYSTEM_CONTEXT SystemContext, + IN OUT NESTED_INTERRUPT_STATE *IsrState + ); + +#endif // __NESTED_INTERRUPT_TPL_LIB__ diff --git a/OvmfPkg/Library/NestedInterruptTplLib/Iret.c b/OvmfPkg/Library/NestedInterruptTplLib/Iret.c new file mode 100644 index 0000000000..f6b2c51b6c --- /dev/null +++ b/OvmfPkg/Library/NestedInterruptTplLib/Iret.c @@ -0,0 +1,62 @@ +/** @file + Force interrupt handler to return with interrupts still disabled. + + Copyright (C) 2022, Fen Systems Ltd. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include +#include + +#include "Iret.h" + +/** + Force interrupt handler to return with interrupts still disabled. + + @param SystemContext A pointer to the system context when the + interrupt occurred. +**/ +VOID +DisableInterruptsOnIret ( + IN OUT EFI_SYSTEM_CONTEXT SystemContext + ) +{ + #if defined (MDE_CPU_X64) + + IA32_EFLAGS32 Rflags; + + // + // Get flags from system context. + // + Rflags.UintN = SystemContext.SystemContextX64->Rflags; + ASSERT (Rflags.Bits.IF); + + // + // Clear interrupts-enabled flag. + // + Rflags.Bits.IF = 0; + SystemContext.SystemContextX64->Rflags = Rflags.UintN; + + #elif defined (MDE_CPU_IA32) + + IA32_EFLAGS32 Eflags; + + // + // Get flags from system context. + // + Eflags.UintN = SystemContext.SystemContextIa32->Eflags; + ASSERT (Eflags.Bits.IF); + + // + // Clear interrupts-enabled flag. + // + Eflags.Bits.IF = 0; + SystemContext.SystemContextIa32->Eflags = Eflags.UintN; + + #else + + #error "Unsupported CPU" + + #endif +} diff --git a/OvmfPkg/Library/NestedInterruptTplLib/Iret.h b/OvmfPkg/Library/NestedInterruptTplLib/Iret.h new file mode 100644 index 0000000000..278c1e22b3 --- /dev/null +++ b/OvmfPkg/Library/NestedInterruptTplLib/Iret.h @@ -0,0 +1,19 @@ +/** @file + Force interrupt handler to return with interrupts still disabled. + + Copyright (C) 2022, Fen Systems Ltd. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#ifndef _IRET_H_ +#define _IRET_H_ + +#include + +VOID +DisableInterruptsOnIret ( + IN OUT EFI_SYSTEM_CONTEXT SystemContext + ); + +#endif // _IRET_H_ diff --git a/OvmfPkg/Library/NestedInterruptTplLib/NestedInterruptTplLib.inf b/OvmfPkg/Library/NestedInterruptTplLib/NestedInterruptTplLib.inf new file mode 100644 index 0000000000..5eafb41978 --- /dev/null +++ b/OvmfPkg/Library/NestedInterruptTplLib/NestedInterruptTplLib.inf @@ -0,0 +1,35 @@ +## @file +# Handle raising and lowering TPL from within nested interrupt handlers. +# +# Allows interrupt handlers to safely raise and lower the TPL to +# dispatch event notifications, correctly allowing for nested +# interrupts to occur without risking stack exhaustion. +# +# Copyright (C) 2022, Fen Systems Ltd. +# +# SPDX-License-Identifier: BSD-2-Clause-Patent +## + +[Defines] + INF_VERSION = 1.29 + BASE_NAME = NestedInterruptTplLib + FILE_GUID = 8df39823-2f9e-4ef2-b971-243b44c32c67 + MODULE_TYPE = DXE_DRIVER + VERSION_STRING = 1.0 + LIBRARY_CLASS = NestedInterruptTplLib|DXE_DRIVER + +[Sources] + Tpl.c + Iret.c + +[Packages] + MdePkg/MdePkg.dec + OvmfPkg/OvmfPkg.dec + +[LibraryClasses] + BaseLib + DebugLib + UefiBootServicesTableLib + +[Depex.common.DXE_DRIVER] + TRUE diff --git a/OvmfPkg/Library/NestedInterruptTplLib/Tpl.c b/OvmfPkg/Library/NestedInterruptTplLib/Tpl.c new file mode 100644 index 0000000000..e19d98878e --- /dev/null +++ b/OvmfPkg/Library/NestedInterruptTplLib/Tpl.c @@ -0,0 +1,216 @@ +/** @file + Handle raising and lowering TPL from within nested interrupt handlers. + + Allows interrupt handlers to safely raise and lower the TPL to + dispatch event notifications, correctly allowing for nested + interrupts to occur without risking stack exhaustion. + + Copyright (C) 2022, Fen Systems Ltd. + + SPDX-License-Identifier: BSD-2-Clause-Patent +**/ + +#include +#include +#include +#include + +#include "Iret.h" + +/** + Raise the task priority level to TPL_HIGH_LEVEL. + + @param None. + + @return The task priority level at which the interrupt occurred. +**/ +EFI_TPL +EFIAPI +NestedInterruptRaiseTPL ( + VOID + ) +{ + EFI_TPL InterruptedTPL; + + // + // Raise TPL and assert that we were called from within an interrupt + // handler (i.e. with TPL below TPL_HIGH_LEVEL but with interrupts + // disabled). + // + ASSERT (GetInterruptState () == FALSE); + InterruptedTPL = gBS->RaiseTPL (TPL_HIGH_LEVEL); + ASSERT (InterruptedTPL < TPL_HIGH_LEVEL); + + return InterruptedTPL; +} + +/** + Lower the task priority back to the value at which the interrupt + occurred. + + This is unfortunately messy. UEFI requires us to support nested + interrupts, but provides no way for an interrupt handler to call + RestoreTPL() without implicitly re-enabling interrupts. In a + virtual machine, it is possible for a large burst of interrupts to + arrive. We must prevent such a burst from leading to stack + exhaustion, while continuing to allow nested interrupts to occur. + + Since nested interrupts are permitted, an interrupt handler may be + invoked as an inner interrupt handler while an outer instance of the + same interrupt handler is still inside its call to RestoreTPL(). + + To avoid stack exhaustion, this call may therefore (when provably + safe to do so) defer the actual TPL lowering to be performed by an + outer instance of the same interrupt handler. + + @param InterruptedTPL The task priority level at which the interrupt + occurred, as previously returned from + NestedInterruptRaiseTPL(). + + @param SystemContext A pointer to the system context when the + interrupt occurred. + + @param IsrState A pointer to the state shared between all + invocations of the nested interrupt handler. +**/ +VOID +EFIAPI +NestedInterruptRestoreTPL ( + IN EFI_TPL InterruptedTPL, + IN OUT EFI_SYSTEM_CONTEXT SystemContext, + IN OUT NESTED_INTERRUPT_STATE *IsrState + ) +{ + EFI_TPL SavedInProgressRestoreTPL; + BOOLEAN DeferredRestoreTPL; + + // + // If the TPL at which this interrupt occurred is equal to that of + // the in-progress RestoreTPL() for an outer instance of the same + // interrupt handler, then that outer handler's call to RestoreTPL() + // must have finished dispatching all event notifications. This + // interrupt must therefore have occurred at the point that the + // outer handler's call to RestoreTPL() had finished and was about + // to return to the outer handler. + // + // If we were to call RestoreTPL() at this point, then we would open + // up the possibility for unlimited stack consumption in the event + // of an interrupt storm. We therefore cannot safely call + // RestoreTPL() from within this stack frame (i.e. from within this + // instance of the interrupt handler). + // + // Instead, we arrange to return from this interrupt with the TPL + // still at TPL_HIGH_LEVEL and with interrupts disabled, and to + // defer our call to RestoreTPL() to the in-progress outer instance + // of the same interrupt handler. + // + if (InterruptedTPL == IsrState->InProgressRestoreTPL) { + // + // Trigger outer instance of this interrupt handler to perform the + // RestoreTPL() call that we cannot issue at this point without + // risking stack exhaustion. + // + ASSERT (IsrState->DeferredRestoreTPL == FALSE); + IsrState->DeferredRestoreTPL = TRUE; + + // + // DEFERRAL INVOCATION POINT + // + // Return from this interrupt handler with interrupts still + // disabled (by clearing the "interrupts-enabled" bit in the CPU + // flags that will be restored by the IRET or equivalent + // instruction). + // + // This ensures that no further interrupts may occur before + // control reaches the outer interrupt handler's RestoreTPL() loop + // at the point marked "DEFERRAL RETURN POINT" (see below). + // + DisableInterruptsOnIret (SystemContext); + return; + } + + // + // If the TPL at which this interrupt occurred is higher than that + // of the in-progress RestoreTPL() for an outer instance of the same + // interrupt handler, then that outer handler's call to RestoreTPL() + // must still be dispatching event notifications. + // + // We must therefore call RestoreTPL() at this point to allow more + // event notifications to be dispatched, since those event + // notification callback functions may themselves be waiting upon + // other events. + // + // We cannot avoid creating a new stack frame for this call to + // RestoreTPL(), but the total number of such stack frames is + // intrinsically limited by the number of distinct TPLs. + // + // We may need to issue the call to RestoreTPL() more than once, if + // an inner instance of the same interrupt handler needs to defer + // its RestoreTPL() call to be performed from within this stack + // frame (see above). + // + while (TRUE) { + // + // Check shared state loop invariants. + // + ASSERT (IsrState->InProgressRestoreTPL < InterruptedTPL); + ASSERT (IsrState->DeferredRestoreTPL == FALSE); + + // + // Record the in-progress RestoreTPL() value in the shared state + // where it will be visible to an inner instance of the same + // interrupt handler, in case a nested interrupt occurs during our + // call to RestoreTPL(). + // + SavedInProgressRestoreTPL = IsrState->InProgressRestoreTPL; + IsrState->InProgressRestoreTPL = InterruptedTPL; + + // + // Call RestoreTPL() to allow event notifications to be + // dispatched. This will implicitly re-enable interrupts. + // + gBS->RestoreTPL (InterruptedTPL); + + // + // Re-disable interrupts after the call to RestoreTPL() to ensure + // that we have exclusive access to the shared state. + // + DisableInterrupts (); + + // + // DEFERRAL RETURN POINT + // + // An inner instance of the same interrupt handler may have chosen + // to defer its RestoreTPL() call to be performed from within this + // stack frame. If so, it is guaranteed that no further event + // notifications or interrupts have been processed between the + // DEFERRAL INVOCATION POINT (see above) and this DEFERRAL RETURN + // POINT. + // + + // + // Restore the locally saved in-progress RestoreTPL() value in the + // shared state, now that our call to RestoreTPL() has returned + // and is therefore no longer in progress. + // + ASSERT (IsrState->InProgressRestoreTPL == InterruptedTPL); + IsrState->InProgressRestoreTPL = SavedInProgressRestoreTPL; + + // + // Check (and clear) the shared state to see if an inner instance + // of the same interrupt handler deferred its call to + // RestoreTPL(). + // + DeferredRestoreTPL = IsrState->DeferredRestoreTPL; + IsrState->DeferredRestoreTPL = FALSE; + + // + // If no inner interrupt handler deferred its call to + // RestoreTPL(), then the TPL has been successfully restored and + // we may return from the interrupt handler. + // + if (DeferredRestoreTPL == FALSE) { + return; + } + } +} diff --git a/OvmfPkg/OvmfPkg.dec b/OvmfPkg/OvmfPkg.dec index a350bb8f84..693925a1dc 100644 --- a/OvmfPkg/OvmfPkg.dec +++ b/OvmfPkg/OvmfPkg.dec @@ -38,6 +38,10 @@ # MemEncryptTdxLib|Include/Library/MemEncryptTdxLib.h + ## @libraryclass Handle TPL changes within nested interrupt handlers + # + NestedInterruptTplLib|Include/Library/NestedInterruptTplLib.h + ## @libraryclass Save and restore variables using a file # NvVarsFileLib|Include/Library/NvVarsFileLib.h