x86-qemu: Two way communication between OS and firmware utilizing UEFI variables
In this article, we are going to present a demonstration of bidirectional communication between the firmware and the Operating System on RT(runtime).
In the first part focus is on the creation of a custom volatile UEFI Variable in the firmware side, before the OS even boots which is going to serve as a buffer in between OS and code in the firmware side.
efivarfs - a (U)EFI variable filesystem
UEFI variables are actually used by the boot loader and the OS in early system startup for configuration purposes. Based upon them, the operating system is able to manage certain settings of the boot process, f.e boot order, or key-management for UEFI Secure Boot.
Information we can obtain from https://docs.kernel.org/filesystems/efivarfs.html shows that the reason for the efivarfs filesystem is to address the shortcomings of using entries in sysfs to maintain EFI variables(the old sysfs way). Using efivarfs , variables can be created, deleted and modified.
If systemd on startup is not instructed to mount the efivarfs filesystem, it can be mounted like this:
mount -t efivarfs none /sys/firmware/efi/efivars
In this section we are going to prepare the required firmware to be used for the emulation of the machine.
In our example system, we will be using QEMU with x86-64 architecture. The -bios for launching the actual VM, will be created using EDK2 (https://github.com/tianocore/edk2), which is a modern, feature-rich, cross-platform firmware development environment for the UEFI and PI specifications. For compiling in a way to produce firmware for an emulation, one can obtain the information from (https://github.com/tianocore/edk2/blob/master/OvmfPkg/README) using the Open Virtual Machine Firmware.
Creation of a new UEFI Variable is achieved by creating and integrating into firmware a new DXE_RUNTIME_DRIVER.
The code of the SampleUefiRuntimeDriver is the following.
#include <Uefi.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiRuntimeLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
#include <Library/DebugLib.h>
#include <Library/MemoryAllocationLib.h>
EFI_EVENT mExitBootServicesEvent = NULL;
EFI_EVENT mSetVirtualAddressMapEvent = NULL;
BOOLEAN gAtRuntime = FALSE;
//global pointer which is
//converted into a virtual address
//when Set Virtual Address Map
//event is signaled.
//
VOID *gGlobalPointer;
VOID EFIAPI SampleNotifyExitBootServices(
?? ??? ?IN EFI_EVENT?? ?Event,
?? ??? ?IN VOID?? ??? ?*Context
?? ??? ?)
{
?? ?gAtRuntime = TRUE;
?? ?DEBUG((DEBUG_ERROR,"SampleUefiRuntimeDriver :: SampleNotifyExitBootServices() success!\n"));
}
VOID EFIAPI SampleNotifySetVirtualAddressMap(
?? ??? ?IN EFI_EVENT?? ?Event,
?? ??? ?IN VOID?? ??? ?*Context
?? ??? ?)
{
?? ?DEBUG((DEBUG_ERROR, "SampleUefiRuntimeDriver :: Allocated buffer mem: %x (prior to call to EfiConvertPointer())\n", gGlobalPointer));
?? ?EFI_STATUS Status;
?? ?Status = EfiConvertPointer(
?? ??? ?EFI_OPTIONAL_PTR,
?? ??? ?(VOID **)&gGlobalPointer
?? ??? ?);?? ?
?? ?ASSERT_EFI_ERROR(Status);
?? ?DEBUG((DEBUG_ERROR,"SampleUefiRuntimeDriver :: SampleNotifySetVirtualAddressMap() success!\n"));
?? ?DEBUG((DEBUG_ERROR, "SampleUefiRuntimeDriver :: Allocated buffer mem: %x\n", gGlobalPointer));
//?? ?fill buffer with data so we can perform test via qemu command.
?? ?char *tmp = gGlobalPointer;?? ?
?? ?int i =0;
?? ?for (i = 0; i < 0x100; i++)
?? ??? ?*(tmp+i) = (0x100 - i);
}
EFI_STATUS
EFIAPI
SampleUefiRuntimeDriverEntryPoint(
?? ??? ?IN EFI_HANDLE?? ??? ?ImageHandle,
?? ??? ?IN EFI_SYSTEM_TABLE?? ?*SystemTable
?? ??? ?)
{
?? ?EFI_STATUS?? ?Status;
?? ?// play and do some test with the BootServices Table.
?? ?//allocate some memory buffer on gGlobalPointer.
?? ?UINTN bs = 0x100;
????gGlobalPointer = AllocateRuntimePool(0x100);
????if(gGlobalPointer == NULL)
?? ??? ?DEBUG((DEBUG_ERROR, "SampleUefiRuntimeDriver :: 'Error in allocating RUNTIME buffer pool'\n"));
?? ?else {
?? ??? ?DEBUG((DEBUG_ERROR, "SampleUefiRuntimeDriver :: 'Success on allocation of %d bytes of RUNTIME memory'\n",bs));
?? ??? ?DEBUG((DEBUG_ERROR, "SampleUefiRuntimeDriver :: Allocated buffer mem: %x\n", gGlobalPointer));
?? ??? ?//?? ?fill buffer with data so we can perform test via qemu command.
?? ??? ??? ?char *tmp = gGlobalPointer;?? ?
?? ??? ??? ?int i =0;
?? ??? ??? ?for (i = 0; i < 0x100; i++)
?? ??? ??? ??? ?*(tmp+i) = (0x100 - i);
?? ?}
?? ?Status = gBS->CreateEvent(
?? ??? ??? ?EVT_SIGNAL_EXIT_BOOT_SERVICES,? //type
?? ??? ??? ?TPL_NOTIFY,?? ??? ??? ?//notify tpl
?? ??? ??? ?SampleNotifyExitBootServices,?? ?//notify function
?? ??? ??? ?NULL,?? ??? ??? ??? ?//notify context
?? ??? ??? ?&mExitBootServicesEvent?? ??? ?//event
?? ??? ??? ?);
?? ?ASSERT_EFI_ERROR(Status);
?? ?
?? ?Status = gBS->CreateEvent(
?? ??? ??? ?EVT_SIGNAL_VIRTUAL_ADDRESS_CHANGE, //type
?? ??? ??? ?TPL_NOTIFY,?? ??? ??? ?? //notify tpl
?? ??? ??? ?SampleNotifySetVirtualAddressMap, //notify function
?? ??? ??? ?NULL,?? ??? ??? ??? ?? //notify context
?? ??? ??? ?&mSetVirtualAddressMapEvent?? ?? //event
?? ??? ??? ?);
?? ?ASSERT_EFI_ERROR(Status);
?? ?
?? ?DEBUG((DEBUG_ERROR, "SampleUefiRuntimeDriver executed EntryPoint() success.\n"));
?? ?CHAR16? var_name[]???? = L"TestVar";
?? ?UINT32 var_attrs = EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS;
?? ?UINTN var_data_size? = 0xa;
??????? char var_data[0xa] ="0123456789";
?? ?EFI_STATUS e;
?? ?DEBUG((DEBUG_ERROR,"Creation of %s variable\n",var_name));
?? ?e = gRT->SetVariable(var_name, &gTestVarGuid, var_attrs, var_data_size,(VOID *)var_data);
?? ?if(e == EFI_SUCCESS) {
?? ??? ?DEBUG((DEBUG_ERROR,"New UEFI Variable creation success.\n"));
?? ?} else {
?? ??? ?DEBUG((DEBUG_ERROR,"UEFI Variable creation failure: %x.\n", e));
?? ?}
?? ? ?
?? ?return EFI_SUCCESS;
}
Note that this driver needs to be integrated into the edk2 code-base in order to get the results shown in this article.(More info on creating new driver's can also be found on UEFI Driver Writer's Guide etc.)
In the code, two hooks on events ( ExitBootServices() and SetVirtualAddressMap() ) are registered, and then the driver is using Runtime Services to call SetVariable(). As we can verify from the UEFI Specs 2.10, SetVariable() is able to create a new custom UEFI Variable.
Also as inspected into the code we initiate the UEFI Variable to contain "0123456789" as data.
Booting on Linux and after mounting efivarfs we list the contents of /sys/firmware/efi/efivars and we inspect our custom TestVar variable included.
In https://blog.fpmurphy.com/2012/12/efivars-and-efivarfs.html there is a great article and code of how to use efivarfs. Slight modifications on the code was made in order to be able to read the contents of the newly created TestVar. No major changes on the code apart the respective GUID and name matching. On delete_variable() although, in order to be able to delete we need to remove immutable attribute from the UEFI variable.
efi_status_t
delete_variable(efi_variable_t *var)
{
??? char name[PATH_MAX];
??? char filename[PATH_MAX];
?
??? if (!var) {
?? ?printf("Invalid parameter, trying to delete variable.\n");
??????? return EFI_INVALID_PARAMETER;
??? }
?
??? variable_to_name(var, name);
??? snprintf(filename, PATH_MAX-1, "%s/%s", SYSFS_EFI_VARS, name);
??? //getting the file's flags.
??? char cmd[100];
??? char *ini = "chattr -i ";
? ?
??? strcpy(cmd, ini);
??? strcat(cmd, filename);
??? //command to remove immutable attribute so
??? //the UEFI variable can be removed.
??? system(cmd);
??? printf("Call to unlink: %s\n",filename);
??? int ret = unlink(filename);
??? if (ret == 0) {
?? ?printf("Uefi Variable successful removal\n");
??????? return EFI_SUCCESS;
??? }
?? ?
??? printf("Error code returned by unlink is: %d errno: %d \n", ret, errno);
??? printf("EFI out of resources on trying to delete variable.\n");
??? return EFI_OUT_OF_RESOURCES;
}
?
?
Reading the TestVar variable gives the following output: which corresponds to values {0...9} as we initialized it with the SampleUefiRuntimeDriver 's code.
So far we are able to expose to the operating system a custom UEFI Variable from firmware-side, and the operating system is able to read the data.
领英推荐
#include <linux/module.h>
#include <linux/efi.h>
#include <linux/rtc.h>
#include <linux/init.h>
#define EFI_FPMURPHY_GUID EFI_GUID(0x11111111, 0x2222, 0x3333, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb)
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Mouzakitis Nikolaos");
/// part of the function.. copied from /drivers/rtc/rtc-efi.c
//?? ?used to test correct get_time invocation.
static bool
convert_from_efi_time(efi_time_t *eft, struct rtc_time *wtime)
{
?? ?memset(wtime, 0, sizeof(*wtime));
?? ?if (eft->second >= 60)
?? ??? ?return false;
?? ?wtime->tm_sec? = eft->second;
?? ?if (eft->minute >= 60)
?? ??? ?return false;
?? ?wtime->tm_min? = eft->minute;
?? ?if (eft->hour >= 24)
?? ??? ?return false;
?? ?wtime->tm_hour = eft->hour;
?? ?if (!eft->day || eft->day > 31)
?? ??? ?return false;
?? ?wtime->tm_mday = eft->day;
?? ?if (!eft->month || eft->month > 12)
?? ??? ?return false;
?? ?wtime->tm_mon? = eft->month - 1;
?? ?if (eft->year < 1900 || eft->year > 9999)
?? ??? ?return false;
?? ?wtime->tm_year = eft->year - 1900;
?? ?return true;
}
static int efi_com_init(void)
{
??? efi_time_t eft;
??? efi_time_cap_t cap;
??? efi_status_t status;
??? struct rtc_time realt; //from rtc.h
??? efi_guid_t guid = EFI_FPMURPHY_GUID;
??? efi_char16_t name[] = L"TestVar";
?? ?
??? unsigned long data_size = 10;?? ?
??? uint32_t attr;
??? uint8_t data[10];
??? int i;
? ?
??? status = efi.get_time(&eft, &cap);
??? if(status == EFI_SUCCESS) {
?? ?pr_info("Success execution of efi.get_time()\n");
?? ?if(!convert_from_efi_time(&eft, &realt)) {
?? ??? ?pr_info("Error on convert_from_efi_time()\n");
?? ??? ?return (-1);
?? ?}
?? ?
??????? pr_info("%s: Loaded\n",__func__);
??????? pr_info("efi.get_time() results\n");
?? ?pr_info("Year: %d? Month: %d Hour: %d Min: %d Sec: %d\n", realt.tm_year, realt.tm_mon, realt.tm_hour, realt.tm_min, realt.tm_sec);
?? ?//system reset-tested.
//?? ?efi.reset_system(0 , EFI_SUCCESS, 0, NULL);
?? ?//validate efi runtime services enabled.
?? ?if (!efi_enabled(EFI_RUNTIME_SERVICES)) {
?? ??? ?pr_info("%s : EFI runtime services are not enabled\n", __func__);
?? ??? ?return 0;
?? ?} else {
?? ??? ?pr_info("%s : EFI runtime services are enabled\n", __func__);
?? ?}
?? ?/********************* get_variable() test. *******************************************/
?? ?pr_info("%s :Trying to get the new created UEFI variable.\n", __func__);
?? ?status = efi.get_variable(name, &guid, &attr, &data_size, data);
?? ?if(status == EFI_SUCCESS) {
?? ??? ?pr_info("%s :Success get_variable() call\n", __func__);
?? ??? ?pr_info("Value print-out:\n");
?? ??? ?for(i = 0; i < 10; i++)
?? ??? ???? pr_info("%x ", data[i]);
?? ??? ?pr_info("\n");
?? ?} else {
?? ??? ?pr_info("get_variable() failed: status: %x\n", (unsigned int)status);
?? ??? ?return (0);
?? ?}
?? ?return 0;
??? } else {
??????? pr_info("Error on efi.get_time()\n");
?? ?return (-1);
??? }
}
static void efi_com_exit(void)
{
??? pr_info("%s : unloaded\n",__func__);
}
module_init(efi_com_init);
module_exit(efi_com_exit);>
When the module is loaded into the kernel, we can observe the same behavior, it can read the new Variable created during the Boot Phase.
We can also write the UEFI Variable from the efi_com kernel module. The code needed to write the variable is given on the following snippet.
/*****************? set_variable() test? ********************************************
?? ??? ?pr_info("%s :Trying to set the new created UEFI variable.\n", __func__);
?? ??? ?// alter the data buffer.
?? ??? ?for(i = 0; i < data_size; i++)
?? ??? ??? ?data[i] = 84-i; //random choosen value.
?? ??? ?
?? ??? ?status = efi.set_variable(name, &guid, attr, data_size, data);
?? ??? ?if(status == EFI_SUCCESS) {
?? ??? ??? ?pr_info("%s :Success set_variable() call\n", __func__);
?? ??? ??? ?pr_info("\n");
?? ??? ?} else {
?? ??? ??? ?pr_info("set_variable() failed: status: %x\n", (unsigned int)status);
?? ??? ??? ?return (0);
?? ??? ?}
Running a program to read the variable we see that we have the integer values 84 down to 75 as we instructed it using the kernel module.
So far, we have shown how can we read/write from the Runtime OS(Linux), but if we truly need a bidirectional communication we need to implement a mechanism in order the firmware can respond depending to the info received from the Runtime OS.
For this purpose, we need to dive more into the tianocore-edk2 code, and detect which part of the code is getting executed when the UEFI Variable is getting written by the OS. After some debug and research one can detect the source file https://github.com/tianocore/edk2/blob/master/MdeModulePkg/Universal/Variable/RuntimeDxe/Variable.c
If we add the snippet in the previous source file inside UpdateVariable() function
DEBUG((DEBUG_ERROR, "UpdateVariable() 8 for %s\n", VariableName));
????? UINT8 *ip = (UINT8 *)Data;
????? char *cp = (char *)Data;
????? for(int i = 0; i < DataSize; i++)
?? ?????? DEBUG((DEBUG_ERROR, "%c %d \n", *(cp+i), *(ip+i)));
we can observe now the actual values that are passed from the RT kernel module. (following figure)
Based upon this, one could implement an application(by modifying the Variable.c source file) where the firmware side, could be responsible for deciding if it should alter or not the variable etc. depending on the appliance.
F.e, if the values written by the Runtime Linux OS shouldn't be taken in account the UEFI Variable could remain unattached or the Data of the Variable could be re-initialized to a default value etc.
References :
[1] https://github.com/tianocore/edk2/
[2] https://davysouza.medium.com/sending-data-from-uefi-to-os-through-uefi-variables-b4f9964e1883
[3] https://www.kernel.org/doc/html/latest/filesystems/efivarfs.html
[4] https://elixir.bootlin.com/linux/latest/source