/** @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 <Library/BaseLib.h> | |
#include <Library/DebugLib.h> | |
#include <Library/NestedInterruptTplLib.h> | |
#include <Library/UefiBootServicesTableLib.h> | |
#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 interrupts already disabled before raising the | |
// TPL). | |
// | |
ASSERT (GetInterruptState () == FALSE); | |
InterruptedTPL = gBS->RaiseTPL (TPL_HIGH_LEVEL); | |
// | |
// At TPL_HIGH_LEVEL, CPU interrupts are disabled (as per the UEFI | |
// specification) and so we should never encounter a situation in | |
// which InterruptedTPL==TPL_HIGH_LEVEL. The specification also | |
// restricts usage of TPL_HIGH_LEVEL to the firmware itself. | |
// | |
// However, nothing actually prevents a UEFI application from | |
// invalidly calling gBS->RaiseTPL(TPL_HIGH_LEVEL) and then | |
// violating the invariant by enabling interrupts via the STI or | |
// equivalent instruction. Some versions of the Microsoft Windows | |
// bootloader are known to do this. | |
// | |
if (InterruptedTPL >= TPL_HIGH_LEVEL) { | |
DEBUG ((DEBUG_ERROR, "ERROR: Interrupts enabled at TPL_HIGH_LEVEL!\n")); | |
} | |
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. | |
// | |
ASSERT (GetInterruptState () == FALSE); | |
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 (GetInterruptState () == FALSE); | |
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 (if | |
// InterruptedTPL is below TPL_HIGH_LEVEL), even though we are | |
// still inside the interrupt handler. | |
// | |
gBS->RestoreTPL (InterruptedTPL); | |
// | |
// Re-disable interrupts after the call to RestoreTPL() to ensure | |
// that we have exclusive access to the shared state. Interrupts | |
// will be re-enabled by the IRET or equivalent instruction when | |
// we subsequently return from the interrupt handler. | |
// | |
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; | |
} | |
} | |
} |