2019-08-04 21:27:44 +03:00

656 lines
22 KiB
C

/** @file
Copyright (C) 2013, dmazar. All rights reserved.
Copyright (C) 2019, vit9696. All rights reserved.
All rights reserved.
This program and the accompanying materials
are licensed and made available under the terms and conditions of the BSD License
which accompanies this distribution. The full text of the license may be found at
http://opensource.org/licenses/bsd-license.php
THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
**/
#include "BootCompatInternal.h"
#include <IndustryStandard/AppleHibernate.h>
#include <Library/BaseLib.h>
#include <Library/BaseMemoryLib.h>
#include <Library/DebugLib.h>
#include <Library/OcBootManagementLib.h>
#include <Library/OcMachoLib.h>
#include <Library/OcMemoryLib.h>
#include <Library/OcMiscLib.h>
#include <Library/OcStringLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Library/UefiLib.h>
#include <Library/UefiRuntimeServicesTableLib.h>
/**
Protect RT data from boot.efi relocation by marking them MemMapIO.
See more details in the function definition.
@param[in,out] RtReloc Relocation entry list to store entry types.
@param[in] MemoryMapSize Memory map size.
@param[in] DescriptorSize Memory map descriptor size.
@param[in,out] MemoryMap MemoryMap to protect entries in.
@param[in] SysTableArea Special address that should not be protected.
**/
STATIC
VOID
ProtectRtMemoryFromRelocation (
IN OUT RT_RELOC_PROTECT_DATA *RtReloc,
IN UINTN MemoryMapSize,
IN UINTN DescriptorSize,
IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap,
IN EFI_PHYSICAL_ADDRESS SysTableArea
)
{
//
// We protect RT data & code from relocation by marking them MemMapIO except EFI_SYSTEM_TABLE area.
//
// This fixes NVRAM issues on some boards where access to NVRAM after boot services is possible
// only in SMM mode. RT driver passes data to SMM handler through previously negotiated buffer
// and this buffer must not be relocated.
// Explained and examined in detail by CodeRush and night199uk:
// https://web.archive.org/web/20141025080709/http://www.projectosx.com/forum/lofiversion/index.php/t3298.html
//
// Starting with APTIO V for NVRAM to work not only RT data but RT code too can no longer be moved
// due to the use of commbuffers. This, however, creates a memory protection issue, because
// XNU maps RT data as RW and code as RX, and AMI appears use global variables in some RT drivers.
// For this reason we shim (most?) affected RT services via wrapers that unset the WP bit during
// the UEFI call and set it back on return in a separate driver.
// Explained in detail by Download-Fritz and vit9696:
// http://www.insanelymac.com/forum/topic/331381-aptiomemoryfix (first 2 links in particular).
//
// EFI_SYSTEM_TABLE is passed directly through kernel boot arguments, and thus goes through static
// mapping (ml_static_ptovirt) in efi_set_tables_64 call. This mapping works as PHYS | CONST = VIRT.
// To avoid kernel accessing unmapped virtual address we let boot.efi relocate the page with
// EFI_SYSTEM_TABLE area. While technically it is possible to let the original page to be relocated,
// we pick a safer root by using a private copy.
//
// The primary downside of this approach is that boot.efi will still reserve the contiguous memory
// for runtime services after the kernel: efiRuntimeServicesPageCount pages starting from
// efiRuntimeServicesPageStart within kaddr ~ ksize range. However, unlike Macs, which have reserved
// gaps only for ACPI NVS, MemMapIO and similar regions, with this approach almost no physical memory
// in efiRuntimeServicesPageStart area is used at all. This memory is never reclaimed by XNU, which
// marks it as allocated in i386_vm_init. Expirements show that at least 85 MBs (Z170) are used for
// this process. On server systems the issue is much worse due to many devices in place.
// Ideally boot.efi should only count RT code and RT data pages, but it is not easy to change.
//
UINTN NumEntries;
UINTN Index;
EFI_MEMORY_DESCRIPTOR *Desc;
RT_RELOC_PROTECT_INFO *RelocInfo;
RtReloc->NumEntries = 0;
RelocInfo = &RtReloc->RelocInfo[0];
NumEntries = MemoryMapSize / DescriptorSize;
for (Index = 0; Index < NumEntries; ++Index) {
if ((Desc->Attribute & EFI_MEMORY_RUNTIME) == 0
|| Desc->NumberOfPages == 0) {
continue;
}
if (Desc->Type == EfiRuntimeServicesCode
|| (Desc->Type == EfiRuntimeServicesData
&& Desc->PhysicalStart != SysTableArea)) {
if (RtReloc->NumEntries == ARRAY_SIZE (RtReloc->RelocInfo)) {
RUNTIME_DEBUG ((
DEBUG_ERROR,
"OCABC: Cannot save mem type for entry: %Lx (type 0x%x)\n",
(UINT64) Desc->PhysicalStart,
(UINT32) Desc->Type
));
return;
}
RelocInfo->PhysicalStart = Desc->PhysicalStart;
RelocInfo->PhysicalEnd = Desc->PhysicalStart + (EFI_PAGES_TO_SIZE (Desc->NumberOfPages) - 1);
RelocInfo->Type = Desc->Type;
Desc->Type = EfiMemoryMappedIO;
++RelocInfo;
++RtReloc->NumEntries;
}
Desc = NEXT_MEMORY_DESCRIPTOR (Desc, DescriptorSize);
}
}
/**
Copy RT flagged areas to separate memmap, define virtual to physical address mapping,
and call SetVirtualAddressMap() only with that partial memmap.
@param[in,out] KernelState Kernel support state.
@param[in] MemoryMapSize Memory map size.
@param[in] DescriptorSize Memory map descriptor size.
@param[in] DescriptorVersion Memor map descriptor version.
@param[in,out] MemoryMap Complete memory map with all entries.
@retval EFI_SUCCESS on success.
**/
STATIC
EFI_STATUS
PerformRtMemoryVirtualMapping (
IN OUT KERNEL_SUPPORT_STATE *KernelState,
IN UINTN MemoryMapSize,
IN UINTN DescriptorSize,
IN UINT32 DescriptorVersion,
IN EFI_MEMORY_DESCRIPTOR *MemoryMap
)
{
//
// About partial memmap:
// Some UEFIs are converting pointers to virtual addresses even if they do not
// point to regions with RT flag. This means that those UEFIs are using
// Desc->VirtualStart even for non-RT regions. Linux had issues with this:
// http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commit;h=7cb00b72876ea2451eb79d468da0e8fb9134aa8a
// They are doing it Windows way now - copying RT descriptors to separate
// mem map and passing that stripped map to SetVirtualAddressMap().
// We'll do the same, although it seems that just assigning
// VirtualStart = PhysicalStart for non-RT areas also does the job.
//
// About virtual to physical mappings:
// Also adds virtual to physical address mappings for RT areas. This is needed since
// SetVirtualAddressMap() does not work on my Aptio without that. Probably because some driver
// has a bug and is trying to access new virtual addresses during the change.
// Linux and Windows are doing the same thing and problem is
// not visible there.
//
UINTN NumEntries;
UINTN Index;
EFI_MEMORY_DESCRIPTOR *Desc;
EFI_MEMORY_DESCRIPTOR *VirtualDesc;
EFI_STATUS Status;
PAGE_MAP_AND_DIRECTORY_POINTER *PageTable;
UINTN Flags;
Desc = MemoryMap;
NumEntries = MemoryMapSize / DescriptorSize;
VirtualDesc = KernelState->VmMap;
KernelState->VmMapSize = 0;
KernelState->VmMapDescSize = DescriptorSize;
//
// Get current VM page table
//
GetCurrentPageTable (&PageTable, &Flags);
for (Index = 0; Index < NumEntries; ++Index) {
//
// Some UEFIs end up with "reserved" area with EFI_MEMORY_RUNTIME flag set when Intel HD3000 or HD4000 is used.
// For example, on GA-H81N-D2H there is a single 1 GB descriptor:
// 000000009F800000-00000000DF9FFFFF 0000000000040200 8000000000000000
//
// All known boot.efi starting from at least 10.5.8 properly handle this flag and do not assign virtual addresses
// to reserved descriptors.
// However, the issue was with AptioFix itself, which did not check for EfiReservedMemoryType and replaced
// it by EfiMemoryMappedIO to prevent boot.efi relocations.
//
// The relevant discussion and the original fix can be found here:
// http://web.archive.org/web/20141111124211/http://www.projectosx.com:80/forum/lofiversion/index.php/t2428-450.html
// https://sourceforge.net/p/cloverefiboot/code/605/
//
// Since it is not the bug in boot.efi, AptioMemoryFix only needs to properly handle EfiReservedMemoryType with
// EFI_MEMORY_RUNTIME attribute set, and there is no reason to mess with the memory map passed to boot.efi.
//
if (Desc->Type != EfiReservedMemoryType && (Desc->Attribute & EFI_MEMORY_RUNTIME) != 0) {
//
// Check if there is enough space in virtual map.
//
if (KernelState->VmMapSize + DescriptorSize > sizeof (KernelState->VmMap)) {
RUNTIME_DEBUG ((DEBUG_ERROR, "OCABC: Too many RT entries to memory map\n"));
return EFI_OUT_OF_RESOURCES;
}
//
// Copy region with EFI_MEMORY_RUNTIME flag to virtual map.
//
CopyMem (VirtualDesc, Desc, DescriptorSize);
//
// Define virtual to physical mapping.
//
Status = VmMapVirtualPages (
&KernelState->VmContext,
PageTable,
Desc->VirtualStart,
Desc->NumberOfPages,
Desc->PhysicalStart
);
if (EFI_ERROR (Status)) {
RUNTIME_DEBUG ((DEBUG_ERROR, "OCABC: RT mapping failure - %r\n", Status));
return EFI_OUT_OF_RESOURCES;
}
//
// Proceed to next virtual map slot.
//
VirtualDesc = NEXT_MEMORY_DESCRIPTOR (VirtualDesc, DescriptorSize);
KernelState->VmMapSize += DescriptorSize;
}
//
// Proceed to next original map slot.
//
Desc = NEXT_MEMORY_DESCRIPTOR (Desc, DescriptorSize);
}
VmFlushCaches ();
Status = gRT->SetVirtualAddressMap (
KernelState->VmMapSize,
DescriptorSize,
DescriptorVersion,
KernelState->VmMap
);
return Status;
}
/**
Revert RT data protected types to let XNU kernel kernel properly map data.
@param[in] RtReloc Relocated entry list with entry types.
@param[in] MemoryMapSize Memory map size.
@param[in] DescriptorSize Memory map descriptor size.
@param[in,out] MemoryMap MemoryMap to restore protected entries in.
**/
STATIC
VOID
RestoreProtectedRtMemoryTypes (
IN RT_RELOC_PROTECT_DATA *RtReloc,
IN UINTN MemoryMapSize,
IN UINTN DescriptorSize,
IN OUT EFI_MEMORY_DESCRIPTOR *MemoryMap
)
{
UINTN Index;
UINTN Index2;
UINTN NumEntriesLeft;
UINTN NumEntries;
EFI_PHYSICAL_ADDRESS PhysicalStart;
EFI_PHYSICAL_ADDRESS PhysicalEnd;
EFI_MEMORY_DESCRIPTOR *Desc;
BOOLEAN Found;
NumEntriesLeft = RtReloc->NumEntries;
NumEntries = MemoryMapSize / DescriptorSize;
Desc = MemoryMap;
for (Index = 0; Index < NumEntries && NumEntriesLeft > 0; ++Index) {
Found = FALSE;
PhysicalStart = Desc->PhysicalStart;
PhysicalEnd = PhysicalStart + (EFI_PAGES_TO_SIZE (Desc->NumberOfPages) - 1);
for (Index2 = 0; Index2 < RtReloc->NumEntries; ++Index2) {
//
// I would expect PhysicalStart and PhysicalEnd to always match here, but
// the region can be merged with a nearby one, thus we check for overlap
// rather than full match. "Desc contains RtReloc" should also do.
//
if (PhysicalStart <= RtReloc->RelocInfo[Index2].PhysicalEnd
&& RtReloc->RelocInfo[Index2].PhysicalStart <= PhysicalEnd) {
Desc->Type = RtReloc->RelocInfo[Index2].Type;
Found = TRUE;
}
}
if (Found) {
--NumEntriesLeft;
}
Desc = NEXT_MEMORY_DESCRIPTOR (Desc, DescriptorSize);
}
if (NumEntriesLeft > 0) {
RUNTIME_DEBUG ((
DEBUG_ERROR,
"OCABC: Failed to restore %u entries out of %u\n",
(UINT32) NumEntriesLeft,
(UINT32) RtReloc->NumEntries
));
}
}
/**
Prepare environment for normal booting. Called when boot.efi jumps to kernel.
@param[in,out] BootCompat Boot compatibility context.
@param[in,out] BootArgs Apple kernel boot arguments.
**/
STATIC
VOID
AppleMapPrepareForBooting (
IN OUT BOOT_COMPAT_CONTEXT *BootCompat,
IN OUT VOID *BootArgs
)
{
OC_BOOT_ARGUMENTS BA;
UINTN MemoryMapSize;
EFI_MEMORY_DESCRIPTOR *MemoryMap;
UINTN DescriptorSize;
OcParseBootArgs (&BA, BootArgs);
//
// FIXME: Restore the variables we tempered with to support custom slides.
//
// RestoreCustomSlideOverrides (&BA);
MemoryMapSize = *BA.MemoryMapSize;
MemoryMap = (EFI_MEMORY_DESCRIPTOR *)(UINTN) (*BA.MemoryMap);
DescriptorSize = *BA.MemoryMapDescriptorSize;
//
// We must restore EfiRuntimeServicesCode memory area types, because otherwise
// RuntimeServices won't be mapped.
//
RestoreProtectedRtMemoryTypes (
&BootCompat->RtReloc,
MemoryMapSize,
DescriptorSize,
MemoryMap
);
}
/**
Prepare environment for hibernate wake. Called when boot.efi jumps to kernel.
@param[in,out] BootCompat Boot compatibility context.
@param[in,out] ImageHeaderPage Apple hibernate image page number.
**/
STATIC
VOID
AppleMapPrepareForHibernateWake (
IN OUT BOOT_COMPAT_CONTEXT *BootCompat,
IN UINTN ImageHeaderPage
)
{
IOHibernateImageHeader *ImageHeader;
IOHibernateHandoff *Handoff;
ImageHeader = (IOHibernateImageHeader *) EFI_PAGES_TO_SIZE (ImageHeaderPage);
//
// Legacy note. In legacy implementations systemTableOffset was unconditionally overwritten
// with a wrong address due to ImageHeader->runtimePages not being converted from pages to bytes.
// Fortunately systemTableOffset was unused when kIOHibernateHandoffTypeMemoryMap is unspecified.
// systemTableOffset is calculated properly by boot.efi itself starting from 10.6.8 at least,
// and thus this assignment was useless in the first place.
//
//
// At this step we have two routes.
//
// 1. Remove newly generated memory map from hibernate image to let XNU use the original mapping.
// This is known to work well on most systems primarily because Windows requires UEFI firmwares
// to preserve physical memory consistency at S4 wake. "On a UEFI platform, firmware runtime memory
// must be consistent across S4 sleep state transitions, in both size and location.", see:
// https://docs.microsoft.com/en-us/windows-hardware/design/device-experiences/oem-uefi#hibernation-state-s4-transition-requirements
// 2. Recover memory map just as we do for normal booting. This was causing issues on some firmwares,
// which provided very strange memory maps after S4 wake. In other cases this should not immediately
// break things. XNU will entirely remove efiRuntimeServicesPageStart/efiRuntimeServicesPageSize
// mapping, and our new memory map entries will unconditionally overwrite previous ones. In case
// no physical memory changes happened this should work fine.
//
Handoff = (IOHibernateHandoff *) EFI_PAGES_TO_SIZE ((UINTN) ImageHeader->handoffPages);
while (Handoff->type != kIOHibernateHandoffTypeEnd) {
if (Handoff->type == kIOHibernateHandoffTypeMemoryMap) {
if (BootCompat->Settings.DiscardAppleS4Map) {
//
// Route 1. Discard the new memory map here, and let XNU use what it had.
//
Handoff->type = kIOHibernateHandoffType;
} else {
//
// Route 2. Recovery memory protection types just as normal boot.
//
if (BootCompat->KernelState.VmMapDescSize == 0) {
RUNTIME_DEBUG (("OCABC: Saved descriptor size cannot be 0\n"));
BootCompat->KernelState.VmMapDescSize = sizeof (EFI_MEMORY_DESCRIPTOR);
}
RestoreProtectedRtMemoryTypes (
&BootCompat->RtReloc,
Handoff->bytecount,
MIN (BootCompat->KernelState.VmMapDescSize, sizeof (EFI_MEMORY_DESCRIPTOR)),
(EFI_MEMORY_DESCRIPTOR *)(UINTN) Handoff->data
);
}
break;
}
Handoff = (IOHibernateHandoff *) ((UINTN) Handoff + sizeof(Handoff) + Handoff->bytecount);
}
}
VOID
AppleMapPrepareMemoryPool (
IN OUT BOOT_COMPAT_CONTEXT *BootCompat
)
{
EFI_STATUS Status;
Status = VmAllocateMemoryPool (
&BootCompat->KernelState.VmContext,
OC_DEFAULT_VMEM_PAGE_COUNT
);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_ERROR, "OCABC: Memory pool allocation failure - %r\n", Status));
}
}
VOID
AppleMapPrepareBooterState (
IN OUT BOOT_COMPAT_CONTEXT *BootCompat,
IN OUT EFI_LOADED_IMAGE *LoadedImage,
IN EFI_GET_MEMORY_MAP GetMemoryMap OPTIONAL
)
{
EFI_STATUS Status;
//
// This function may be called twice, do not redo in this case.
//
if (BootCompat->KernelState.SysTableRtArea == 0) {
AppleMapPlatformSaveState (
&BootCompat->KernelState.AsmState,
&BootCompat->KernelState.KernelJump
);
//
// Allocate 1 RT data page for copy of UEFI system table for kernel.
// This one also has to be 32-bit due to XNU BootArgs structure.
// The reason for this allocation to be required is because XNU uses static
// mapping for directly passed pointers (see ProtectRtMemoryFromRelocation).
//
BootCompat->KernelState.SysTableRtArea = BASE_4GB;
Status = AllocatePagesFromTop (
EfiRuntimeServicesData,
1,
&BootCompat->KernelState.SysTableRtArea,
GetMemoryMap,
NULL
);
if (EFI_ERROR (Status)) {
DEBUG ((
DEBUG_ERROR,
"OCABC: Failed to allocate system table memory - %r\n",
Status
));
BootCompat->KernelState.SysTableRtArea = 0;
return;
}
//
// Copy UEFI system table to the new location.
//
if (gST->Hdr.HeaderSize > EFI_PAGE_SIZE) {
DEBUG ((
DEBUG_ERROR,
"OCABC: EFI_SYSTEM_TABLE is too large - %u bytes\n",
(UINT32) gST->Hdr.HeaderSize
));
}
CopyMem (
(VOID *)(UINTN) BootCompat->KernelState.SysTableRtArea,
gST,
MIN (gST->Hdr.HeaderSize, EFI_PAGE_SIZE)
);
}
//
// Assign loaded image with custom system table.
//
LoadedImage->SystemTable =
(EFI_SYSTEM_TABLE *)(UINTN) BootCompat->KernelState.SysTableRtArea;
}
VOID
AppleMapPrepareKernelJump (
IN OUT BOOT_COMPAT_CONTEXT *BootCompat,
IN UINTN ImageAddress,
IN BOOLEAN AppleHibernateWake
)
{
UINT8 *KernelEntry;
UINTN SlideAddr;
VOID *MachOImage;
IOHibernateImageHeader *ImageHeader;
if (!AppleHibernateWake) {
//
// Read kernel entry from Mach-O load command and patch it with jump.
//
SlideAddr = ImageAddress - BASE_KERNEL_ADDR;
MachOImage = (VOID*) (SlideAddr + SLIDE_GRANULARITY);
KernelEntry = (UINT8*) MachoRuntimeGetEntryAddress (MachOImage);
if (KernelEntry != 0) {
KernelEntry += SlideAddr;
}
} else {
//
// Read kernel entry from hibernation image and patch it with jump.
// At this stage HIB section is not yet copied from sleep image to it's
// proper memory destination. so we'll patch entry point in sleep image.
//
ImageHeader = (IOHibernateImageHeader *) ImageAddress;
KernelEntry = (UINT8 *) &ImageHeader->fileExtentMap[0]
+ ImageHeader->fileExtentMapSize + ImageHeader->restore1CodeOffset;
}
if (KernelEntry == 0) {
RUNTIME_DEBUG ((DEBUG_ERROR, "KernelEntry must be found!"));
return;
}
//
// Save original kernel entry code.
//
CopyMem (
&BootCompat->KernelState.KernelOrg[0],
KernelEntry,
sizeof (BootCompat->KernelState.KernelOrg)
);
//
// Copy kernel jump code to kernel entry address.
//
CopyMem (
KernelEntry,
&BootCompat->KernelState.KernelJump,
sizeof (BootCompat->KernelState.KernelJump)
);
}
EFI_STATUS
AppleMapPrepareVmState (
IN OUT BOOT_COMPAT_CONTEXT *BootCompat,
IN UINTN MemoryMapSize,
IN UINTN DescriptorSize,
IN UINT32 DescriptorVersion,
IN EFI_MEMORY_DESCRIPTOR *MemoryMap
)
{
EFI_STATUS Status;
//
// Protect RT areas from relocation by marking then MemMapIO.
//
ProtectRtMemoryFromRelocation (
&BootCompat->RtReloc,
MemoryMapSize,
DescriptorSize,
MemoryMap,
BootCompat->KernelState.SysTableRtArea
);
//
// Virtualize RT services with all needed fixes.
//
Status = PerformRtMemoryVirtualMapping (
&BootCompat->KernelState,
MemoryMapSize,
DescriptorSize,
DescriptorVersion,
MemoryMap
);
//
// Copy now virtualized UEFI system table for boot.efi to hand it to the kernel.
//
CopyMem (
(VOID *)(UINTN) BootCompat->KernelState.SysTableRtArea,
gST,
gST->Hdr.HeaderSize
);
return Status;
}
UINTN
EFIAPI
AppleMapPrepareKernelState (
IN UINTN Args,
IN BOOLEAN ModeX64
)
{
BOOT_COMPAT_CONTEXT *BootCompatContext;
BootCompatContext = GetBootCompatContext ();
if (BootCompatContext->ServiceState.AppleHibernateWake) {
AppleMapPrepareForHibernateWake (
BootCompatContext,
Args
);
} else {
AppleMapPrepareForBooting (
BootCompatContext,
(VOID *) Args
);
}
//
// Restore original kernel entry code.
//
CopyMem (
BootCompatContext->KernelState.AsmState.KernelEntry,
&BootCompatContext->KernelState.KernelOrg[0],
sizeof (BootCompatContext->KernelState.KernelOrg)
);
return Args;
}