Files
ipxe/src/interface/efi/efi_timer.c
Michael Brown 562c74e1ea [efi] Run ExitBootServices shutdown hook at TPL_NOTIFY
On some systems (observed with the Thunderbolt ports on a ThinkPad X1
Extreme Gen3 and a ThinkPad P53), if the IOMMU is enabled then the
system firmware will install an ExitBootServices notification event
that disables bus mastering on the Thunderbolt xHCI controller and all
PCI bridges, and destroys any extant IOMMU mappings.  This leaves the
xHCI controller unable to perform any DMA operations.

As described in commit 236299b ("[xhci] Avoid DMA during shutdown if
firmware has disabled bus mastering"), any subsequent DMA operation
attempted by the xHCI controller will end up completing after the
operating system kernel has reenabled bus mastering, resulting in a
DMA operation to an area of memory that the hardware is no longer
permitted to access and, on Windows with the Driver Verifier enabled,
a STOP 0xE6 (DRIVER_VERIFIER_DMA_VIOLATION).

That commit avoids triggering any DMA attempts during the shutdown of
the xHCI controller itself.  However, this is not a complete solution
since any attached and opened USB device (e.g. a USB NIC) may
asynchronously trigger DMA attempts that happen to occur after bus
mastering has been disabled but before we reset the xHCI controller.

Avoid this problem by installing our own ExitBootServices notification
event at TPL_NOTIFY, thereby causing it to be invoked before the
firmware's own ExitBootServices notification event that disables bus
mastering.

This unsurprisingly causes the shutdown hook itself to be invoked at
TPL_NOTIFY, which causes a fatal error when later code attempts to
raise the TPL to TPL_CALLBACK (which is a lower TPL).  Work around
this problem by redefining the "internal" iPXE TPL to be variable, and
set this internal TPL to TPL_NOTIFY when the shutdown hook is invoked.

Avoid calling into an underlying SNP protocol instance from within our
shutdown hook at TPL_NOTIFY, since the underlying SNP driver may
attempt to raise the TPL to TPL_CALLBACK (which would cause a fatal
error).  Failing to shut down the underlying SNP device is safe to do
since the underlying device must, in any case, have installed its own
ExitBootServices hook if any shutdown actions are required.

Reported-by: Andreas Hammarskjöld <junior@2PintSoftware.com>
Tested-by: Andreas Hammarskjöld <junior@2PintSoftware.com>
Signed-off-by: Michael Brown <mcb30@ipxe.org>
2021-11-23 15:55:01 +00:00

235 lines
7.1 KiB
C

/*
* Copyright (C) 2008 Michael Brown <mbrown@fensystems.co.uk>.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the
* License, or any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*
* You can also choose to distribute this program under the terms of
* the Unmodified Binary Distribution Licence (as given in the file
* COPYING.UBDL), provided that you have satisfied its requirements.
*/
FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <ipxe/timer.h>
#include <ipxe/init.h>
#include <ipxe/efi/efi.h>
/** @file
*
* iPXE timer API for EFI
*
*/
/**
* Number of jiffies per second
*
* This is a policy decision.
*/
#define EFI_JIFFIES_PER_SEC 32
/** Current tick count */
static unsigned long efi_jiffies;
/** Timer tick event */
static EFI_EVENT efi_tick_event;
/** Colour for debug messages */
#define colour &efi_jiffies
/**
* Delay for a fixed number of microseconds
*
* @v usecs Number of microseconds for which to delay
*/
static void efi_udelay ( unsigned long usecs ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
EFI_STATUS efirc;
int rc;
if ( ( efirc = bs->Stall ( usecs ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( colour, "EFI could not delay for %ldus: %s\n",
usecs, strerror ( rc ) );
/* Probably screwed */
}
}
/**
* Get current system time in ticks
*
* @ret ticks Current time, in ticks
*/
static unsigned long efi_currticks ( void ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
/* UEFI manages to ingeniously combine the worst aspects of
* both polling and interrupt-driven designs. There is no way
* to support proper interrupt-driven operation, since there
* is no way to hook in an interrupt service routine. A
* mockery of interrupts is provided by UEFI timers, which
* trigger at a preset rate and can fire at any time.
*
* We therefore have all of the downsides of a polling design
* (inefficiency and inability to sleep until something
* interesting happens) combined with all of the downsides of
* an interrupt-driven design (the complexity of code that
* could be preempted at any time).
*
* The UEFI specification expects us to litter the entire
* codebase with calls to RaiseTPL() as needed for sections of
* code that are not reentrant. Since this doesn't actually
* gain us any substantive benefits (since even with such
* calls we would still be suffering from the limitations of a
* polling design), we instead choose to run at TPL_CALLBACK
* almost all of the time, dropping to a lower TPL to allow
* timer ticks to occur.
*
* We record the external TPL at the point of entry into iPXE,
* and drop back only as far as this external TPL. This
* avoids the unexpected behaviour that may arise from having
* iPXE temporarily drop to TPL_APPLICATION in the middle of
* an entry point invoked at TPL_CALLBACK. The side effect is
* that iPXE's view of the system time is effectively frozen
* for the duration of any call made in to iPXE at
* TPL_CALLBACK or higher.
*
*
* For added excitement, UEFI provides no clean way for device
* drivers to shut down in preparation for handover to a
* booted operating system. The platform firmware simply
* doesn't bother to call the drivers' Stop() methods.
* Instead, all non-trivial drivers must register an
* EVT_SIGNAL_EXIT_BOOT_SERVICES event to be signalled when
* ExitBootServices() is called, and clean up without any
* reference to the EFI driver model.
*
* Unfortunately, all timers silently stop working when
* ExitBootServices() is called. Even more unfortunately, and
* for no discernible reason, this happens before any
* EVT_SIGNAL_EXIT_BOOT_SERVICES events are signalled. The
* net effect of this entertaining design choice is that any
* timeout loops on the shutdown path (e.g. for gracefully
* closing outstanding TCP connections) may wait indefinitely.
*
* There is no way to report failure from currticks(), since
* the API lazily assumes that the host system continues to
* travel through time in the usual direction. Work around
* EFI's violation of this assumption by falling back to a
* simple free-running monotonic counter during shutdown.
*/
if ( efi_shutdown_in_progress ) {
efi_jiffies++;
} else {
bs->RestoreTPL ( efi_external_tpl );
bs->RaiseTPL ( efi_internal_tpl );
}
return ( efi_jiffies * ( TICKS_PER_SEC / EFI_JIFFIES_PER_SEC ) );
}
/**
* Timer tick
*
* @v event Timer tick event
* @v context Event context
*/
static EFIAPI void efi_tick ( EFI_EVENT event __unused,
void *context __unused ) {
/* Increment tick count */
efi_jiffies++;
}
/**
* Start timer tick
*
*/
static void efi_tick_startup ( void ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
EFI_STATUS efirc;
int rc;
/* Create timer tick event */
if ( ( efirc = bs->CreateEvent ( ( EVT_TIMER | EVT_NOTIFY_SIGNAL ),
TPL_CALLBACK, efi_tick, NULL,
&efi_tick_event ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( colour, "EFI could not create timer tick: %s\n",
strerror ( rc ) );
/* Nothing we can do about it */
return;
}
/* Start timer tick */
if ( ( efirc = bs->SetTimer ( efi_tick_event, TimerPeriodic,
( 10000000 / EFI_JIFFIES_PER_SEC ) ))!=0){
rc = -EEFI ( efirc );
DBGC ( colour, "EFI could not start timer tick: %s\n",
strerror ( rc ) );
/* Nothing we can do about it */
return;
}
DBGC ( colour, "EFI timer started at %d ticks per second\n",
EFI_JIFFIES_PER_SEC );
}
/**
* Stop timer tick
*
* @v booting System is shutting down in order to boot
*/
static void efi_tick_shutdown ( int booting __unused ) {
EFI_BOOT_SERVICES *bs = efi_systab->BootServices;
EFI_STATUS efirc;
int rc;
/* Stop timer tick */
if ( ( efirc = bs->SetTimer ( efi_tick_event, TimerCancel, 0 ) ) != 0 ){
rc = -EEFI ( efirc );
DBGC ( colour, "EFI could not stop timer tick: %s\n",
strerror ( rc ) );
/* Self-destruct initiated */
return;
}
DBGC ( colour, "EFI timer stopped\n" );
/* Destroy timer tick event */
if ( ( efirc = bs->CloseEvent ( efi_tick_event ) ) != 0 ) {
rc = -EEFI ( efirc );
DBGC ( colour, "EFI could not destroy timer tick: %s\n",
strerror ( rc ) );
/* Probably non-fatal */
return;
}
}
/** Timer tick startup function */
struct startup_fn efi_tick_startup_fn __startup_fn ( STARTUP_EARLY ) = {
.name = "efi_tick",
.startup = efi_tick_startup,
.shutdown = efi_tick_shutdown,
};
/** EFI timer */
struct timer efi_timer __timer ( TIMER_NORMAL ) = {
.name = "efi",
.currticks = efi_currticks,
.udelay = efi_udelay,
};