Dive into edk2-ovmf fisrt debug trip

Prepare environment

install compiler related rpm

1
yum --disablerepo=* --enablerepo=ali* install -y make gcc binutils iasl nasm libuuid-devel gcc-c++

if cross-build firmware on x86 machine, install cross compilers:

1
yum --disablerepo=* --enablerepo=ali* install -y gcc-aarch64-linux-gnu gcc-arm-linux-gnu

get source code

1
2
3
git clone https://github.com/tianocore/edk2.git
cd edk2
git submodule update --init

than setup environment variables:

1
source edksetup.sh

build base tools at first:

1
make -C BaseTools

note: only need once

Build firmware

Then build firmware for x64 qemu:

1
build -t GCC5 -a X64 -p OvmfPkg/OvmfPkgX64.dsc

The firmware volumes built can be found in Build/OvmfX64/DEBUG_GCC5/FV.

Building the aarch64 firmware instead:

1
build -t GCC5 -a AARCH64 -p ArmVirtPkg/ArmVirtQemu.dsc

The build results land in Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV.

Qemu expects the aarch64 firmware images being 64M im size. The firmware images can’t be used as-is because of that, some padding is needed to create an image which can be used for pflash:

1
2
3
4
dd of="QEMU_EFI-pflash.raw" if="/dev/zero" bs=1M count=64
dd of="QEMU_EFI-pflash.raw" if="QEMU_EFI.fd" conv=notrunc
dd of="QEMU_VARS-pflash.raw" if="/dev/zero" bs=1M count=64
dd of="QEMU_VARS-pflash.raw" if="QEMU_VARS.fd" conv=notrunc

There are a bunch of compile time options, typically enabled using -D NAME or -D NAME=TRUE. Options which are enabled by default can be turned off using -D NAME=FALSE. Available options are defined in the *.dsc files referenced by the build command. So a feature-complete build looks more like this:

1
2
3
4
5
6
build -t GCC5 -a X64 -p OvmfPkg/OvmfPkgX64.dsc \
-D FD_SIZE_4MB \
-D NETWORK_IP6_ENABLE \
-D NETWORK_HTTP_BOOT_ENABLE \
-D NETWORK_TLS_ENABLE \
-D TPM2_ENABLE

From OvmfPkgX64.dsc lots of features is defined.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  #
# Network definition
#
DEFINE NETWORK_TLS_ENABLE = FALSE
DEFINE NETWORK_IP6_ENABLE = FALSE
DEFINE NETWORK_HTTP_BOOT_ENABLE = FALSE
DEFINE NETWORK_ALLOW_HTTP_CONNECTIONS = TRUE
DEFINE NETWORK_ISCSI_ENABLE = TRUE

!include NetworkPkg/NetworkDefines.dsc.inc

#
# Device drivers
#
DEFINE PVSCSI_ENABLE = TRUE
DEFINE MPT_SCSI_ENABLE = TRUE
DEFINE LSI_SCSI_ENABLE = FALSE

#
# Flash size selection. Setting FD_SIZE_IN_KB on the command line directly to
# one of the supported values, in place of any of the convenience macros, is
# permitted.
#
!ifdef $(FD_SIZE_1MB)
DEFINE FD_SIZE_IN_KB = 1024
!else
!ifdef $(FD_SIZE_2MB)
DEFINE FD_SIZE_IN_KB = 2048
!else
!ifdef $(FD_SIZE_4MB)
DEFINE FD_SIZE_IN_KB = 4096
!else
DEFINE FD_SIZE_IN_KB = 4096

Secure boot support (on x64) requires SMM mode. Well, it builds and works without SMM, but it’s not secure then. Without SMM nothing prevents the guest OS writing directly to flash, bypassing the firmware, so protected UEFI variables are not actually protected.

Also suspend (S3) support works with enabled SMM only in case parts of the firmware (PEI specifically, see below for details) run in 32bit mode. So the secure boot variant must be compiled this way:

1
2
3
4
5
build -t GCC5 -a IA32 -a X64 -p OvmfPkg/OvmfPkgIa32X64.dsc \
-D FD_SIZE_4MB \
-D SECURE_BOOT_ENABLE \
-D SMM_REQUIRE \
[ ... add network + tpm + other options as needed ... ]

The FD_SIZE_4MB option creates a larger firmware image, being 4MB instead of 2MB (default) in size, offering more space for both code and vars. The RHEL/CentOS builds use that. The Fedora builds are 2MB in size, for historical reasons.

If you need 32-bit firmware builds for some reason, here is how to do it:

1
2
build -t GCC5 -a ARM -p ArmVirtPkg/ArmVirtQemu.dsc
build -t GCC5 -a IA32 -p OvmfPkg/OvmfPkgIa32.dsc

The build results will be in in Build/ArmVirtQemu-ARM/DEBUG_GCC5/FV and Build/OvmfIa32/DEBUG_GCC5/FV

Booting fresh firmware builds

The x86 firmware builds create three different images:

  • OVMF_VARS.fd

    This is the firmware volume for persistent UEFI variables, i.e. where the firmware stores all configuration (boot entries and boot order, secure boot keys, …). Typically this is used as template for an empty variable store and each VM gets its own private copy, libvirt for example stores them in /var/lib/libvirt/qemu/nvram.

  • OVMF_CODE.fd

    This is the firmware volume with the code. Separating this from VARS does (a) allow for easy firmware updates, and (b) allows to map the code read-only into the guest.

  • OVMF.fd

    The all-in-one image with both CODE and VARS. This can be loaded as ROM using -bios, with two drawbacks: (a) UEFI variables are not persistent, and (b) it does not work for SMM_REQUIRE=TRUE builds.

qemu handles pflash storage as block devices, so we have to create block devices for the firmware images:

1
2
3
4
5
6
7
CODE=${WORKSPACE}/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_CODE.fd
VARS=${WORKSPACE}/Build/OvmfX64/DEBUG_GCC5/FV/OVMF_VARS.fd
qemu-system-x86_64 \
-blockdev node-name=code,driver=file,filename=${CODE},read-only=on \
-blockdev node-name=vars,driver=file,filename=${VARS},snapshot=on \
-machine q35,pflash0=code,pflash1=vars \
[ ... ]

Here is the arm version of that (using the padded files created using dd, see above):

1
2
3
4
5
6
7
CODE=${WORKSPACE}/Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV/QEMU_EFI-pflash.raw
VARS=${WORKSPACE}/Build/ArmVirtQemu-AARCH64/DEBUG_GCC5/FV/QEMU_VARS-pflash.raw
qemu-system-aarch64 \
-blockdev node-name=code,driver=file,filename=${CODE},read-only=on \
-blockdev node-name=vars,driver=file,filename=${VARS},snapshot=on \
-machine virt,pflash0=code,pflash1=vars \
[ ... ]

Source code structure

The core edk2 repo holds a number of packages, each package has its own toplevel directory. Here are the most interesting ones:

  • OvmfPkg

    This holds both the x64-specific code (i.e. OVMF itself) and virtualization-specific code shared by all architectures (virtio drivers).

  • ArmVirtPkg

    Arm specific virtual machine support code.

  • MdePkg, MdeModulePkg

    Most core code is here (PCI support, USB support, generic services and drivers, …).

  • PcAtChipsetPkg

    Some Intel architecture drivers and libs.

  • ArmPkg, ArmPlatformPkg

    Common Arm architecture support code.

  • CryptoPkg, NetworkPkg, FatPkg, CpuPkg, …

    As the names of the packages already suggest: Crypto support (using openssl), Network support (including network boot), FAT Filesystem driver, …

Firmware boot phases

The firmware modules in the edk2 repo often named after the boot phase they are running in. Most drivers are named SomeThing**Dxe** for example.

  • ResetVector

    This is where code execution starts after a machine reset. The code will do the bare minimum needed to enter SEC. On x64 the most important step is the transition from 16-bit real mode to 32-bit mode or 64bit long mode.

  • SEC (Security)

    This code typically loads and uncompresses the code for PEI and SEC. On physical hardware SEC often lives in ROM memory and can not be updated. The PEI and DXE firmware volumes are loaded from (updateable) flash.

    With OVMF both SEC firmware volume and the compressed volume holding PXE and DXE code are part of the OVMF_CODE image and will simply be mapped into guest memory.

  • PEI (Pre-EFI Initialization)

    Platform Initialization is done here. Initialize the chipset. Not much to do here in virtual machines, other than loading the x64 e820 memory map (via fw_cfg) from qemu, or get the memory map from the device tree (on aarch64). The virtual hardware is ready-to-go without much extra preaparation.

    PEIMs (PEI Modules) can implement functionality which must be executed before entering the DXE phase. This includes security-sensitive things like initializing SMM mode and locking down flash memory.

  • DXE (Driver Execution Environment)

    When PEI is done it hands over control to the full EFI environment contained in the DXE firmware volume. Most code is here. All kinds of drivers. the firmware setup efi app, …

    Strictly speaking this isn’t only one phase. The code for all phases after PEI is part of the DXE firmware volume though.

Add debug to EDK2

The default OVMF build writes debug messages to IO port 0x402. The following qemu command line options save them in the file called debug.log:

1
-debugcon file:debug.log -global isa-debugcon.iobase=0x402

Or with build option

1
-D DEBUG_ON_SERIAL_PORT

serial output can be captured

1
-serial file:serial.log

note:

The RELEASE build target (‘-b RELEASE’ build option, see below) disables all debug messages. The default build target is DEBUG.

more build scripts:

On systems with the bash shell you can use OvmfPkg/build.sh to simplify
building and running OVMF.

So, for example, to build + run OVMF X64:

1
2
$ OvmfPkg/build.sh -a X64
$ OvmfPkg/build.sh -a X64 qemu

And to run a 64-bit UEFI bootable ISO image:

1
$ OvmfPkg/build.sh -a X64 qemu -cdrom /path/to/disk-image.iso

To build a 32-bit OVMF without debug messages using GCC 4.8:

1
$ OvmfPkg/build.sh -a IA32 -b RELEASE -t GCC48

UEFI Windows 7 & Windows 2008 Server

  • One of the ‘-vga std’ and ‘-vga qxl’ QEMU options should be used.
  • Only one video mode, 1024x768x32, is supported at OS runtime.
  • The ‘-vga qxl’ QEMU option is recommended. After booting the installed guest OS, select the video card in Device Manager, and upgrade its driver to the QXL XDDM one. Download location: http://www.spice-space.org/download.html, Guest | Windows binaries. This enables further resolutions at OS runtime, and provides S3 (suspend/resume) capability.

Debug in practice

Test with qemu commandline

add with config in libvirt vm xml:

1
2
3
4
5
6
<qemu:commandline>
<qemu:arg value='-debugcon'/>
<qemu:arg value='file:/var/log/libvirt/qemu/debug.log'/>
<qemu:arg value='-global'/>
<qemu:arg value='isa-debugcon.iobase=0x402'/>
</qemu:commandline>

debug log will be found in /var/log/libvirt/qemu/debug.log

before we met Win2012 internal reboot issue, use debug to check what happen, the log repeat following lines but guest seems hang on vnc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Register PPI Notify: DCD0BE23-9586-40F4-B643-06522CED4EDE
Install PPI: 8C8CE578-8A3D-4F1C-9935-896185C32DD3
Install PPI: 5473C07A-3DCB-4DCA-BD6F-1E9689E7349A
The 0th FV start address is 0x00000820000, size is 0x000E0000, handle is 0x820000
Register PPI Notify: 49EDB1C1-BF21-4761-BB12-EB0031AABB39
Register PPI Notify: EA7CA24B-DED5-4DAD-A389-BF827E8F9B38
Install PPI: B9E0ABFE-5979-4914-977F-6DEE78C278A6
Install PPI: DBE23AA9-A345-4B97-85B6-B226F1617389
DiscoverPeimsAndOrderWithApriori(): Found 0xB PEI FFS files in the 0th FV
Loading PEIM 9B3ADA4F-AE56-4C24-8DEA-F03B7558AE50
Loading PEIM at 0x0000082BE40 EntryPoint=0x0000082F201 PcdPeim.efi
Install PPI: 06E81C58-4AD7-44BC-8390-F10265F72480
Install PPI: 01F34D25-4DE2-23AD-3FF3-36353FF323F1
Install PPI: 4D8B155B-C059-4C8F-8926-06FD4331DB8A
Install PPI: A60C6B59-E459-425D-9C69-0BCC9CB27D81
Register PPI Notify: 605EA650-C65C-42E1-BA80-91A52AB618C6
Loading PEIM A3610442-E69F-4DF3-82CA-2360C4031A23
Loading PEIM at 0x00000830B40 EntryPoint=0x00000831F5F ReportStatusCodeRouterPei.efi
Install PPI: 0065D394-9951-4144-82A3-0AFC8579C251
Install PPI: 229832D3-7A30-4B36-B827-F40CB7D45436
Loading PEIM 9D225237-FA01-464C-A949-BAABC02D31D0
Loading PEIM at 0x00000832B40 EntryPoint=0x00000833D89 StatusCodeHandlerPei.efi
Loading PEIM 222C386D-5ABC-4FB4-B124-FBB82488ACF4
Loading PEIM at 0x00000834A40 EntryPoint=0x0000083A6DF PlatformPei.efi
Select Item: 0x0
FW CFG Signature: 0x554D4551
Select Item: 0x1
FW CFG Revision: 0x3
SecCoreStartupWithStack(0xFFFCC000, 0x820000)
SEC: Normal boot
DecompressMemFvs: OutputBuffer@A00000+0xCE0090 ScratchBuffer@1700000+0x10000 PcdOvmfDecompressionScratchEnd=0x1710000

track the log to edk2 code, the entry seems start at:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
Locates the PEI Core entry point address

@param[in,out] Fv The firmware volume to search
@param[out] PeiCoreEntryPoint The entry point of the PEI Core image

@retval EFI_SUCCESS The file and section was found
@retval EFI_NOT_FOUND The file and section was not found
@retval EFI_VOLUME_CORRUPTED The firmware volume was corrupted

**/
VOID
FindPeiCoreImageBase (
IN OUT EFI_FIRMWARE_VOLUME_HEADER **BootFv,
OUT EFI_PHYSICAL_ADDRESS *PeiCoreImageBase
)
{
BOOLEAN S3Resume;

*PeiCoreImageBase = 0;

S3Resume = IsS3Resume ();
if (S3Resume && !FeaturePcdGet (PcdSmmSmramRequire)) {
//
// A malicious runtime OS may have injected something into our previously
// decoded PEI FV, but we don't care about that unless SMM/SMRAM is required.
//
DEBUG ((DEBUG_VERBOSE, "SEC: S3 resume\n"));
GetS3ResumePeiFv (BootFv);
} else {
//
// We're either not resuming, or resuming "securely" -- we'll decompress
// both PEI FV and DXE FV from pristine flash.
//
DEBUG ((DEBUG_VERBOSE, "SEC: %a\n",
S3Resume ? "S3 resume (with PEI decompression)" : "Normal boot"));
FindMainFv (BootFv);

DecompressMemFvs (BootFv);
}

FindPeiCoreImageBaseInFv (*BootFv, PeiCoreImageBase);
}

and if not IsS3Resume and not SMM required, VM will use S3 resume but in our situation vm go throught Normal boot.

Than locates the comparessed main firmware volume /usr/share/edk2/ovmf/OVMF_CODE.cc.fd

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
Locates the compressed main firmware volume and decompresses it.

@param[in,out] Fv On input, the firmware volume to search
On output, the decompressed BOOT/PEI FV

@retval EFI_SUCCESS The file and section was found
@retval EFI_NOT_FOUND The file and section was not found
@retval EFI_VOLUME_CORRUPTED The firmware volume was corrupted

**/
EFI_STATUS
DecompressMemFvs (

Next step is still find PEI core entry address, EFI_SECTION_PE32 and EFI_SECTION_TE will be used to find entry address

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
Locates the PEI Core entry point address

@param[in] Fv The firmware volume to search
@param[out] PeiCoreEntryPoint The entry point of the PEI Core image

@retval EFI_SUCCESS The file and section was found
@retval EFI_NOT_FOUND The file and section was not found
@retval EFI_VOLUME_CORRUPTED The firmware volume was corrupted

**/
EFI_STATUS
FindPeiCoreImageBaseInFv (
IN EFI_FIRMWARE_VOLUME_HEADER *Fv,
OUT EFI_PHYSICAL_ADDRESS *PeiCoreImageBase
)
{
EFI_STATUS Status;
EFI_COMMON_SECTION_HEADER *Section;

Status = FindFfsFileAndSection (
Fv,
EFI_FV_FILETYPE_PEI_CORE,
EFI_SECTION_PE32,
&Section
);
if (EFI_ERROR (Status)) {
Status = FindFfsFileAndSection (
Fv,
EFI_FV_FILETYPE_PEI_CORE,
EFI_SECTION_TE,
&Section
);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_ERROR, "Unable to find PEI Core image\n"));
return Status;
}
}

*PeiCoreImageBase = (EFI_PHYSICAL_ADDRESS)(UINTN)(Section + 1);
return EFI_SUCCESS;
}

This is the last step of find PEI Core image.

Back to where FindPeiCoreImageBase’s code path, we can find SecCoreStartupWithStack is the start function which is used in OvmfPkg/Sec/X64/SecEntry.nasm

SecCoreStartupWithStack -> SecStartupPhase2 -> FindAndReportEntryPoints -> FindPeiCoreImageBase

1
2
3
4
5
6
7
8
9
;
; Setup parameters and call SecCoreStartupWithStack
; rcx: BootFirmwareVolumePtr
; rdx: TopOfCurrentStack
;
mov rcx, rbp
mov rdx, rsp
sub rsp, 0x20
call ASM_PFX(SecCoreStartupWithStack)

combine with log, before next SecCoreStartupWithStack invoking:

1
2
3
4
5
Select Item: 0x0^M
FW CFG Signature: 0x554D4551^M
Select Item: 0x1^M
FW CFG Revision: 0x3^M
SecCoreStartupWithStack(0xFFFCC000, 0x820000)^M

Select Item is printed.

Because FV is successfully loaded:

1
The 0th FV start address is 0x00000820000, size is 0x000E0000, handle is 0x820000

And compare to first boot:

1
2
3
4
5
6
7
Loading PEIM at 0x00000834A40 EntryPoint=0x0000083A6DF PlatformPei.efi^M
Select Item: 0x0^M
FW CFG Signature: 0x554D4551^M
Select Item: 0x1^M
FW CFG Revision: 0x3^M
QemuFwCfg interface (DMA) is supported.^M
Platform PEIM Loaded^M

It seems Platform PEIM not loaded correctly.

Check QemuFwCfg related code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
RETURN_STATUS
EFIAPI
QemuFwCfgInitialize (
VOID
)
{
UINT32 Signature;
UINT32 Revision;

//
// Enable the access routines while probing to see if it is supported.
// For probing we always use the IO Port (IoReadFifo8()) access method.
//
mQemuFwCfgSupported = TRUE;
mQemuFwCfgDmaSupported = FALSE;

QemuFwCfgSelectItem (QemuFwCfgItemSignature);
Signature = QemuFwCfgRead32 ();
DEBUG ((DEBUG_INFO, "FW CFG Signature: 0x%x\n", Signature));
QemuFwCfgSelectItem (QemuFwCfgItemInterfaceVersion);
Revision = QemuFwCfgRead32 ();
DEBUG ((DEBUG_INFO, "FW CFG Revision: 0x%x\n", Revision));
if ((Signature != SIGNATURE_32 ('Q', 'E', 'M', 'U')) ||
(Revision < 1)
) {
DEBUG ((DEBUG_INFO, "QemuFwCfg interface not supported.\n"));
mQemuFwCfgSupported = FALSE;
return RETURN_SUCCESS;
}

if ((Revision & FW_CFG_F_DMA) == 0) {
DEBUG ((DEBUG_INFO, "QemuFwCfg interface (IO Port) is supported.\n"));
} else {
mQemuFwCfgDmaSupported = TRUE;
DEBUG ((DEBUG_INFO, "QemuFwCfg interface (DMA) is supported.\n"));
}

if (mQemuFwCfgDmaSupported && MemEncryptSevIsEnabled ()) {
EFI_STATUS Status;

//
// IoMmuDxe driver must have installed the IOMMU protocol. If we are not
// able to locate the protocol then something must have gone wrong.
//
Status = gBS->LocateProtocol (&gEdkiiIoMmuProtocolGuid, NULL,
(VOID **)&mIoMmuProtocol);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_ERROR,
"QemuFwCfgSevDma %a:%a Failed to locate IOMMU protocol.\n",
gEfiCallerBaseName, __FUNCTION__));
ASSERT (FALSE);
CpuDeadLoop ();
}
}

return RETURN_SUCCESS;
}

because FW CFG Revision: 0x3 is supported, according to the code, Revision is > 1 so QemuFwCfg interface is supported, but from next part 0x03 & FW_CFG_F_DMA which is BIT1 0x01 is not 0 so actually QemuFwCfg interface (DMA) is supported. is expected.

1
2
3
4
5
6
if ((Revision & FW_CFG_F_DMA) == 0) {
DEBUG ((DEBUG_INFO, "QemuFwCfg interface (IO Port) is supported.\n"));
} else {
mQemuFwCfgDmaSupported = TRUE;
DEBUG ((DEBUG_INFO, "QemuFwCfg interface (DMA) is supported.\n"));
}

so that means the boot is not correctly performed. But next boot still triggered so we should check how the boot can be triggered before we dig out the truth.

According to code,we can know edk2-ovmf is try to load PlatformPei.efi

1
2
Loading PEIM 222C386D-5ABC-4FB4-B124-FBB82488ACF4^M
Loading PEIM at 0x00000834A40 EntryPoint=0x0000083A751 PlatformPei.efi^M

use the guid 222C386D-5ABC-4FB4-B124-FBB82488ACF4 we can easily get definitions in PlatformPei.inf

1
2
3
4
5
6
7
[Defines]
INF_VERSION = 0x00010005
BASE_NAME = PlatformPei
FILE_GUID = 222c386d-5abc-4fb4-b124-fbb82488acf4
MODULE_TYPE = PEIM
VERSION_STRING = 1.0
ENTRY_POINT = InitializePlatform

which ENTRY_POINT is InitializePlatform, but before entry LibraryClasses should be initialized first:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[LibraryClasses]
BaseLib
CacheMaintenanceLib
DebugLib
HobLib
IoLib
PciLib
ResourcePublicationLib
PeiServicesLib
PeiServicesTablePointerLib
PeimEntryPoint
QemuFwCfgLib
QemuFwCfgS3Lib
QemuFwCfgSimpleParserLib
MtrrLib
MemEncryptSevLib
PcdLib

add logs code execution details.

following log shows the trace when edk2 fall into infinite loop:

1
2
3
4
5
6
7
8
9
10
11
12
13
Loading PEIM 222C386D-5ABC-4FB4-B124-FBB82488ACF4^M
Loading PEIM at 0x00000834A40 EntryPoint=0x0000083A76B PlatformPei.efi^M
Select Item: 0x0^M
FW CFG Signature: 0x554D4551^M
Select Item: 0x1^M
FW CFG Revision: 0x3^M
=========== debug entry point ===========^M
check signature match QEMU result: 0^M
check revision < 1 result: 0^M
check supported result: 2^M
=========== enter InternalMemEncryptSevStatus =========== ^M
=========== ReadSevMsr = true =========== ^M
SecCoreStartupWithStack(0xFFFCC000, 0x820000)^M

SecCoreStartupWithStack is first log when guest boot, and we end with ReadSevMsr = true but nothing after that.

Before we could see PlatformPei.efi is loading, so refer to PlatformPei.efi first. But the QemuFwCfgLib seems not successfully initialized, because the log end up during its load procedure not show Platform PEIM Loaded

1
2
3
4
5
6
7
8
EFI_STATUS
EFIAPI
InitializePlatform (
IN EFI_PEI_FILE_HANDLE FileHandle,
IN CONST EFI_PEI_SERVICES **PeiServices
)
{
DEBUG ((DEBUG_INFO, "Platform PEIM Loaded\n"));

And refer to OvmfPkgX64.dsc describes c lib every procedure should use:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
[LibraryClasses.common.PEIM]
HobLib|MdePkg/Library/PeiHobLib/PeiHobLib.inf
PeiServicesTablePointerLib|MdePkg/Library/PeiServicesTablePointerLibIdt/PeiServicesTablePointerLibIdt.inf
PeiServicesLib|MdePkg/Library/PeiServicesLib/PeiServicesLib.inf
MemoryAllocationLib|MdePkg/Library/PeiMemoryAllocationLib/PeiMemoryAllocationLib.inf
PeimEntryPoint|MdePkg/Library/PeimEntryPoint/PeimEntryPoint.inf
ReportStatusCodeLib|MdeModulePkg/Library/PeiReportStatusCodeLib/PeiReportStatusCodeLib.inf
OemHookStatusCodeLib|MdeModulePkg/Library/OemHookStatusCodeLibNull/OemHookStatusCodeLibNull.inf
PeCoffGetEntryPointLib|MdePkg/Library/BasePeCoffGetEntryPointLib/BasePeCoffGetEntryPointLib.inf
!ifdef $(DEBUG_ON_SERIAL_PORT)
DebugLib|MdePkg/Library/BaseDebugLibSerialPort/BaseDebugLibSerialPort.inf
!else
DebugLib|OvmfPkg/Library/PlatformDebugLibIoPort/PlatformDebugLibIoPort.inf
!endif
PeCoffLib|MdePkg/Library/BasePeCoffLib/BasePeCoffLib.inf
ResourcePublicationLib|MdePkg/Library/PeiResourcePublicationLib/PeiResourcePublicationLib.inf
ExtractGuidedSectionLib|MdePkg/Library/PeiExtractGuidedSectionLib/PeiExtractGuidedSectionLib.inf
!if $(SOURCE_DEBUG_ENABLE) == TRUE
DebugAgentLib|SourceLevelDebugPkg/Library/DebugAgent/SecPeiDebugAgentLib.inf
!endif
CpuExceptionHandlerLib|UefiCpuPkg/Library/CpuExceptionHandlerLib/PeiCpuExceptionHandlerLib.inf
MpInitLib|UefiCpuPkg/Library/MpInitLib/PeiMpInitLib.inf
QemuFwCfgS3Lib|OvmfPkg/Library/QemuFwCfgS3Lib/PeiQemuFwCfgS3LibFwCfg.inf
PcdLib|MdePkg/Library/PeiPcdLib/PeiPcdLib.inf
QemuFwCfgLib|OvmfPkg/Library/QemuFwCfgLib/QemuFwCfgPeiLib.inf

take eyes on QemuFwCfgLib|OvmfPkg/Library/QemuFwCfgLib/QemuFwCfgPeiLib.inf

so check more from QemuFwCfgPei.c , following shows debug log added version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
BOOLEAN
EFIAPI
QemuFwCfgIsAvailable (
VOID
)
{
return InternalQemuFwCfgIsAvailable ();
}


RETURN_STATUS
EFIAPI
QemuFwCfgInitialize (
VOID
)
{
UINT32 Signature;
UINT32 Revision;

//
// Enable the access routines while probing to see if it is supported.
// For probing we always use the IO Port (IoReadFifo8()) access method.
//
mQemuFwCfgSupported = TRUE;
mQemuFwCfgDmaSupported = FALSE;

QemuFwCfgSelectItem (QemuFwCfgItemSignature);
Signature = QemuFwCfgRead32 ();
DEBUG ((DEBUG_INFO, "FW CFG Signature: 0x%x\n", Signature));
QemuFwCfgSelectItem (QemuFwCfgItemInterfaceVersion);
Revision = QemuFwCfgRead32 ();
DEBUG ((DEBUG_INFO, "FW CFG Revision: 0x%x\n", Revision));
DEBUG ((DEBUG_INFO, "=========== debug entry point ===========\n"));

DEBUG ((DEBUG_INFO, "check signature match QEMU result: %d\n", Signature != SIGNATURE_32 ('Q', 'E', 'M', 'U')));
DEBUG ((DEBUG_INFO, "check revision < 1 result: %d\n", Revision < 1));
if ((Signature != SIGNATURE_32 ('Q', 'E', 'M', 'U')) ||
(Revision < 1)
) {
DEBUG ((DEBUG_INFO, "QemuFwCfg interface not supported.\n"));
mQemuFwCfgSupported = FALSE;
return RETURN_SUCCESS;
}

DEBUG ((DEBUG_INFO, "check supported result: %d\n", (Revision & FW_CFG_F_DMA)));
if ((Revision & FW_CFG_F_DMA) == 0) {
DEBUG ((DEBUG_INFO, "QemuFwCfg interface (IO Port) is supported.\n"));
} else {
//
// If SEV is enabled then we do not support DMA operations in PEI phase.
// This is mainly because DMA in SEV guest requires using bounce buffer
// (which need to allocate dynamic memory and allocating a PAGE size'd
// buffer can be challenge in PEI phase)
//
if (MemEncryptSevIsEnabled ()) {
DEBUG ((DEBUG_INFO, "SEV: QemuFwCfg fallback to IO Port interface.\n"));
} else {
mQemuFwCfgDmaSupported = TRUE;
DEBUG ((DEBUG_INFO, "QemuFwCfg interface (DMA) is supported.\n"));
}
}

the code enter the MemEncryptSevIsEnabled () and hang on next function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
STATIC
VOID
EFIAPI
InternalMemEncryptSevStatus (
VOID
)
{
UINT32 RegEax;
MSR_SEV_STATUS_REGISTER Msr;
CPUID_MEMORY_ENCRYPTION_INFO_EAX Eax;
BOOLEAN ReadSevMsr;
SEC_SEV_ES_WORK_AREA *SevEsWorkArea;

ReadSevMsr = FALSE;

DEBUG ((DEBUG_INFO, "=========== enter InternalMemEncryptSevStatus =========== \n"));
SevEsWorkArea = (SEC_SEV_ES_WORK_AREA *) FixedPcdGet32 (PcdSevEsWorkAreaBase);
if (SevEsWorkArea != NULL && SevEsWorkArea->EncryptionMask != 0) {
//
// The MSR has been read before, so it is safe to read it again and avoid
// having to validate the CPUID information.
//
DEBUG ((DEBUG_INFO, "=========== ReadSevMsr = true =========== \n"));
ReadSevMsr = TRUE;
} else {
//
// Check if memory encryption leaf exist
//
DEBUG ((DEBUG_INFO, "=========== AsmCpuid =========== \n"));
AsmCpuid (CPUID_EXTENDED_FUNCTION, &RegEax, NULL, NULL, NULL);
if (RegEax >= CPUID_MEMORY_ENCRYPTION_INFO) {
//
// CPUID Fn8000_001F[EAX] Bit 1 (Sev supported)
//
AsmCpuid (CPUID_MEMORY_ENCRYPTION_INFO, &Eax.Uint32, NULL, NULL, NULL);

if (Eax.Bits.SevBit) {
ReadSevMsr = TRUE;
}
}
}

if (ReadSevMsr) {
//
// Check MSR_0xC0010131 Bit 0 (Sev Enabled)
//
Msr.Uint32 = AsmReadMsr32 (MSR_SEV_STATUS);
DEBUG ((DEBUG_INFO, "=========== AsmReadMsr32 =========== \n"));
if (Msr.Bits.SevBit) {
mSevStatus = TRUE;
}

//
// Check MSR_0xC0010131 Bit 1 (Sev-Es Enabled)
//
if (Msr.Bits.SevEsBit) {
mSevEsStatus = TRUE;
}
}

DEBUG ((DEBUG_INFO, "=========== out InternalMemEncryptSevStatus =========== \n"));
mSevStatusChecked = TRUE;
}

Finally AsmReadMsr32 is the victim to blame.

note: SEC_SEV_ES_WORK_AREA is a new AREA added by amd used for their SEV feature. Normally guest without SEV feature should not modify those memory area, but it seems windows write randomly bits which caused this problem.

From edk2 groups

Same issue is found https://edk2.groups.io/g/devel/topic/87301748#84086

According to the mail:

1
2
3
4
5
Tested on Intel Platform, It is like 'SEV-ES work area' can be modified by
os(Windows etc), and will not restored on reboot, the
SevEsWorkArea->EncryptionMask may have a random value after reboot. then it
may casue fail on reboot. The msr bits already cached by mSevStatusChecked,
there is no need to try cache again in PEI phase.

it seems Windows will change SEV-ES work area which will lead QemuFwCfgLib to readMsr when guest reboot, but actually for guests, normally SEV-ES is not used, so initializing failure cause the reboot infinite loop.

Patches fix the reboot issue

check file change log

1
git log OvmfPkg/Library/BaseMemEncryptSevLib/PeiMemEncryptSevLibInternal.c

we can find related patches:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
commit d9822304ce0075b1075edf93cc6e2514685b5212
Author: Brijesh Singh <brijesh.singh@amd.com>
Date: Thu Dec 9 11:27:37 2021 +0800

OvmfPkg/MemEncryptSevLib: add MemEncryptSevSnpEnabled()

BZ: https://bugzilla.tianocore.org/show_bug.cgi?id=3275

Create a function that can be used to determine if VM is running as an
SEV-SNP guest.

Cc: Michael Roth <michael.roth@amd.com>
Cc: James Bottomley <jejb@linux.ibm.com>
Cc: Min Xu <min.m.xu@intel.com>
Cc: Jiewen Yao <jiewen.yao@intel.com>
Cc: Tom Lendacky <thomas.lendacky@amd.com>
Cc: Jordan Justen <jordan.l.justen@intel.com>
Cc: Ard Biesheuvel <ardb+tianocore@kernel.org>
Cc: Erdem Aktas <erdemaktas@google.com>
Cc: Gerd Hoffmann <kraxel@redhat.com>
Acked-by: Jiewen Yao <Jiewen.yao@intel.com>
Acked-by: Gerd Hoffmann <kraxel@redhat.com>
Signed-off-by: Brijesh Singh <brijesh.singh@amd.com>

commit ac0a286f4d747a4c6c603a7b225917293cbe1e9f
Author: Michael Kubacki <michael.kubacki@microsoft.com>
Date: Sun Dec 5 14:54:09 2021 -0800

OvmfPkg: Apply uncrustify changes

REF: https://bugzilla.tianocore.org/show_bug.cgi?id=3737

Apply uncrustify changes to .c/.h files in the OvmfPkg package

Cc: Andrew Fish <afish@apple.com>
Cc: Leif Lindholm <leif@nuviainc.com>
Cc: Michael D Kinney <michael.d.kinney@intel.com>
Signed-off-by: Michael Kubacki <michael.kubacki@microsoft.com>
Reviewed-by: Andrew Fish <afish@apple.com>

UEFI boot procedure

Before debugging on edk2 code, check UEFI boot procedure to know more about UEFI boot.

According to https://edk2-docs.gitbook.io/edk-ii-build-specification/2_design_discussion/23_boot_sequence

PI compliant system firmware must support the six phases: security (SEC), pre-efi initialization (PEI), driver execution environment (DXE), boot device selection (BDS), run time (RT) services and After Life (transition from the OS back to the firmware) of system. Refer to Figure below

Our issue occours at PEI so check the first two steps:

Security(SEC)

The Security (SEC) phase is the first phase in the PI Architecture and is responsible for the following:

  • Handling all platform restart events
  • Creating a temporary memory store
  • Serving as the root of trust in the system
  • Passing handoff information to the PEI Foundation

The security section may contain modules with code written in assembly. Therefore, some EDK II module development environment (MDE) modules may contain assembly code. Where this occurs, both Windows and GCC versions of assembly code are provided in different files.

Pre-EFI Initialization (PEI)

The Pre-EFI Initialization (PEI) phase described in the PI Architecture specifications is invoked quite early in the boot flow. Specifically, after some preliminary processing in the Security (SEC) phase, any machine restart event will invoke the PEI phase.

The PEI phase initially operates with the platform in a nascent state, leveraging only on-processor resources, such as the processor cache as a call stack, to dispatch Pre-EFI Initialization Modules (PEIMs). These PEIMs are responsible for the following:

  • Initializing some permanent memory complement
  • Describing the memory in Hand-Off Blocks (HOBs)
  • Describing the firmware volume locations in HOBs
  • Passing control into the Driver Execution Environment (DXE) phase