/** @file
Save, load and delete emulated NVRAM from file storage.
Copyright (c) 2019-2022, vit9696, mikebeaton. All rights reserved.
SPDX-License-Identifier: BSD-3-Clause
**/
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define BASE64_CHUNK_SIZE (52)
#define NVRAM_PLIST_MAX_SIZE (BASE_1MB)
typedef struct {
UINT8 *DataBuffer;
UINTN DataBufferSize;
CHAR8 *Base64Buffer;
UINTN Base64BufferSize;
OC_ASCII_STRING_BUFFER *StringBuffer;
GUID SectionGuid;
OC_NVRAM_LEGACY_ENTRY *SchemaEntry;
EFI_STATUS Status;
} NVRAM_SAVE_CONTEXT;
/**
Version check for NVRAM file. Not the same as protocol revision.
**/
#define OC_NVRAM_STORAGE_VERSION 1
/**
Structure declaration for NVRAM file.
**/
#define OC_NVRAM_STORAGE_MAP_FIELDS(_, __) \
OC_MAP (OC_STRING, OC_ASSOC, _, __)
OC_DECLARE (OC_NVRAM_STORAGE_MAP)
#define OC_NVRAM_STORAGE_FIELDS(_, __) \
_(UINT32 , Version , , 0 , () ) \
_(OC_NVRAM_STORAGE_MAP , Add , , OC_CONSTR (OC_NVRAM_STORAGE_MAP, _, __) , OC_DESTR (OC_NVRAM_STORAGE_MAP))
OC_DECLARE (OC_NVRAM_STORAGE)
OC_MAP_STRUCTORS (OC_NVRAM_STORAGE_MAP)
OC_STRUCTORS (OC_NVRAM_STORAGE, ())
/**
Schema definition for NVRAM file.
**/
STATIC
OC_SCHEMA
mNvramStorageEntrySchema = OC_SCHEMA_MDATA (NULL);
STATIC
OC_SCHEMA
mNvramStorageAddSchema = OC_SCHEMA_MAP (NULL, &mNvramStorageEntrySchema);
STATIC
OC_SCHEMA
mNvramStorageNodesSchema[] = {
OC_SCHEMA_MAP_IN ("Add", OC_NVRAM_STORAGE, Add, &mNvramStorageAddSchema),
OC_SCHEMA_INTEGER_IN ("Version", OC_NVRAM_STORAGE, Version),
};
STATIC
OC_SCHEMA_INFO
mNvramStorageRootSchema = {
.Dict = { mNvramStorageNodesSchema, ARRAY_SIZE (mNvramStorageNodesSchema) }
};
STATIC
OC_STORAGE_CONTEXT
*mStorageContext = NULL;
STATIC
OC_NVRAM_LEGACY_MAP
*mLegacyMap = NULL;
STATIC
EFI_STATUS
LocateNvramDir (
OUT EFI_FILE_PROTOCOL **NvramDir
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *Root;
if ((mStorageContext == NULL) || (mLegacyMap == NULL)) {
return EFI_NOT_READY;
}
if (mStorageContext->FileSystem == NULL) {
return EFI_NOT_FOUND;
}
Status = mStorageContext->FileSystem->OpenVolume (mStorageContext->FileSystem, &Root);
if (EFI_ERROR (Status)) {
return EFI_NOT_FOUND;
}
Status = OcSafeFileOpen (
Root,
NvramDir,
OPEN_CORE_NVRAM_ROOT_PATH,
EFI_FILE_MODE_READ | EFI_FILE_MODE_WRITE | EFI_FILE_MODE_CREATE,
EFI_FILE_DIRECTORY
);
return Status;
}
STATIC
EFI_STATUS
EFIAPI
LoadNvram (
IN OC_STORAGE_CONTEXT *StorageContext,
IN OC_NVRAM_LEGACY_MAP *LegacyMap,
IN BOOLEAN LegacyOverwrite
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *NvramDir;
UINT8 *FileBuffer;
UINT32 FileSize;
BOOLEAN IsValid;
OC_NVRAM_STORAGE NvramStorage;
UINT32 GuidIndex;
UINT32 VariableIndex;
GUID VariableGuid;
OC_ASSOC *VariableMap;
OC_NVRAM_LEGACY_ENTRY *SchemaEntry;
if ((mStorageContext != NULL) || (mLegacyMap != NULL)) {
return EFI_ALREADY_STARTED;
}
if ((StorageContext == NULL) || (LegacyMap == NULL)) {
return EFI_INVALID_PARAMETER;
}
mStorageContext = StorageContext;
mLegacyMap = LegacyMap;
Status = LocateNvramDir (&NvramDir);
if (EFI_ERROR (Status)) {
return Status;
}
FileBuffer = OcReadFileFromDirectory (NvramDir, OPEN_CORE_NVRAM_FILENAME, &FileSize, NVRAM_PLIST_MAX_SIZE);
if (FileBuffer == NULL) {
FileBuffer = OcReadFileFromDirectory (NvramDir, OPEN_CORE_NVRAM_FALLBACK_FILENAME, &FileSize, NVRAM_PLIST_MAX_SIZE);
}
NvramDir->Close (NvramDir);
if (FileBuffer == NULL) {
return EFI_NOT_FOUND;
}
OC_NVRAM_STORAGE_CONSTRUCT (&NvramStorage, sizeof (NvramStorage));
IsValid = ParseSerialized (&NvramStorage, &mNvramStorageRootSchema, FileBuffer, FileSize, NULL);
FreePool (FileBuffer);
if (!IsValid) {
OC_NVRAM_STORAGE_DESTRUCT (&NvramStorage, sizeof (NvramStorage));
return EFI_UNSUPPORTED;
}
if (NvramStorage.Version != OC_NVRAM_STORAGE_VERSION) {
OC_NVRAM_STORAGE_DESTRUCT (&NvramStorage, sizeof (NvramStorage));
return EFI_UNSUPPORTED;
}
for (GuidIndex = 0; GuidIndex < NvramStorage.Add.Count; ++GuidIndex) {
Status = OcProcessVariableGuid (
OC_BLOB_GET (NvramStorage.Add.Keys[GuidIndex]),
&VariableGuid,
mLegacyMap,
&SchemaEntry
);
if (EFI_ERROR (Status)) {
continue;
}
VariableMap = NvramStorage.Add.Values[GuidIndex];
//
// Note 1: LegacyOverwrite remains useful here, even though we know we are writing to
// emulated NVRAM which 'starts off empty'; both for any variables set by the emulated
// NVRAM driver itself, and for those set by any part of OpenDuet when that is in use.
//
// Note 2: If we obey WriteFlash here, then when it is TRUE the SaveNvram method fails
// to save anything to nvram.plist, since everything is marked volatile. As we are in a
// context where emulated NVRAM must be present, we always write non-volatile here.
// (This issue was only not relevant prior to implementation of the emulated NVRAM
// protocol because the previous and current scripts for saving NVRAM variables from
// within macOS do not check whether the variables they are saving are non-volatile.)
//
for (VariableIndex = 0; VariableIndex < VariableMap->Count; ++VariableIndex) {
OcSetNvramVariable (
OC_BLOB_GET (VariableMap->Keys[VariableIndex]),
&VariableGuid,
OPEN_CORE_NVRAM_NV_ATTR, ///< Was NvramConfig->WriteFlash ? OPEN_CORE_NVRAM_NV_ATTR : OPEN_CORE_NVRAM_ATTR
VariableMap->Values[VariableIndex]->Size,
OC_BLOB_GET (VariableMap->Values[VariableIndex]),
SchemaEntry,
LegacyOverwrite
);
}
}
OC_NVRAM_STORAGE_DESTRUCT (&NvramStorage, sizeof (NvramStorage));
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
DeleteFile (
IN EFI_FILE_PROTOCOL *Directory,
IN CONST CHAR16 *FileName
)
{
EFI_STATUS Status;
Status = OcDeleteFile (Directory, FileName);
if (EFI_ERROR (Status)) {
if (Status == EFI_NOT_FOUND) {
Status = EFI_SUCCESS;
}
}
return Status;
}
//
// Serialize one section at a time, NVRAM scan per section.
//
STATIC
OC_PROCESS_VARIABLE_RESULT
EFIAPI
SerializeSectionVariables (
IN EFI_GUID *Guid,
IN CHAR16 *Name,
IN VOID *Context
)
{
EFI_STATUS Status;
NVRAM_SAVE_CONTEXT *SaveContext;
UINT32 Attributes;
UINTN DataSize;
UINTN Base64Size;
UINTN Base64Pos;
ASSERT (Context != NULL);
SaveContext = Context;
if (!CompareGuid (Guid, &SaveContext->SectionGuid)) {
return OcProcessVariableContinue;
}
if (!OcVariableIsAllowedBySchemaEntry (SaveContext->SchemaEntry, Guid, Name, OcStringFormatUnicode)) {
return OcProcessVariableContinue;
}
do {
DataSize = SaveContext->DataBufferSize;
Status = gRT->GetVariable (
Name,
Guid,
&Attributes,
&DataSize,
SaveContext->DataBuffer
);
if (Status == EFI_BUFFER_TOO_SMALL) {
while (DataSize > SaveContext->DataBufferSize) {
if (BaseOverflowMulUN (SaveContext->DataBufferSize, 2, &SaveContext->DataBufferSize)) {
SaveContext->Status = EFI_OUT_OF_RESOURCES;
return OcProcessVariableAbort;
}
}
FreePool (SaveContext->DataBuffer);
SaveContext->DataBuffer = AllocatePool (SaveContext->DataBufferSize);
if (SaveContext->DataBuffer == NULL) {
SaveContext->Status = EFI_OUT_OF_RESOURCES;
return OcProcessVariableAbort;
}
}
} while (Status == EFI_BUFFER_TOO_SMALL);
if (EFI_ERROR (Status)) {
SaveContext->Status = Status;
return OcProcessVariableAbort;
}
//
// Only save non-volatile variables; also, match launchd script and only save
// variables which it can save, i.e. runtime accessible.
//
if ( ((Attributes & EFI_VARIABLE_RUNTIME_ACCESS) == 0)
|| ((Attributes & EFI_VARIABLE_NON_VOLATILE) == 0))
{
DEBUG ((DEBUG_VERBOSE, "NVRAM %g:%s skipped w/ attributes 0x%X\n", Guid, Name, Attributes));
return OcProcessVariableContinue;
}
Base64Size = 0;
Base64Encode (SaveContext->DataBuffer, DataSize, NULL, &Base64Size);
if (Base64Size > SaveContext->Base64BufferSize) {
while (Base64Size > SaveContext->Base64BufferSize) {
if (BaseOverflowMulUN (SaveContext->Base64BufferSize, 2, &SaveContext->Base64BufferSize)) {
SaveContext->Status = EFI_OUT_OF_RESOURCES;
return OcProcessVariableAbort;
}
}
FreePool (SaveContext->Base64Buffer);
SaveContext->Base64Buffer = AllocatePool (SaveContext->Base64BufferSize);
if (SaveContext->Base64Buffer == NULL) {
SaveContext->Status = EFI_OUT_OF_RESOURCES;
return OcProcessVariableAbort;
}
}
Base64Encode (SaveContext->DataBuffer, DataSize, SaveContext->Base64Buffer, &Base64Size);
//
// %c works around BasePrintLibSPrintMarker converting \n to \r\n.
//
Status = OcAsciiStringBufferSPrint (
SaveContext->StringBuffer,
"\t\t\t%s%c"
"\t\t\t%c",
Name,
'\n',
'\n'
);
if (EFI_ERROR (Status)) {
SaveContext->Status = Status;
return OcProcessVariableAbort;
}
for (Base64Pos = 0; Base64Pos < (Base64Size - 1); Base64Pos += BASE64_CHUNK_SIZE) {
Status = OcAsciiStringBufferAppend (
SaveContext->StringBuffer,
"\t\t\t"
);
if (EFI_ERROR (Status)) {
SaveContext->Status = Status;
return OcProcessVariableAbort;
}
Status = OcAsciiStringBufferAppendN (
SaveContext->StringBuffer,
&SaveContext->Base64Buffer[Base64Pos],
BASE64_CHUNK_SIZE
);
if (EFI_ERROR (Status)) {
SaveContext->Status = Status;
return OcProcessVariableAbort;
}
Status = OcAsciiStringBufferAppend (
SaveContext->StringBuffer,
"\n"
);
if (EFI_ERROR (Status)) {
SaveContext->Status = Status;
return OcProcessVariableAbort;
}
}
Status = OcAsciiStringBufferAppend (
SaveContext->StringBuffer,
"\t\t\t\n"
);
if (EFI_ERROR (Status)) {
SaveContext->Status = Status;
return OcProcessVariableAbort;
}
return OcProcessVariableContinue;
}
STATIC
EFI_STATUS
EFIAPI
SaveNvram (
VOID
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *NvramDir;
UINT32 GuidIndex;
NVRAM_SAVE_CONTEXT Context;
Status = LocateNvramDir (&NvramDir);
if (EFI_ERROR (Status)) {
return Status;
}
Context.Status = EFI_SUCCESS;
Context.DataBufferSize = BASE_1KB;
Context.DataBuffer = AllocatePool (Context.DataBufferSize);
if (Context.DataBuffer == NULL) {
NvramDir->Close (NvramDir);
return EFI_OUT_OF_RESOURCES;
}
Context.Base64BufferSize = BASE_1KB;
Context.Base64Buffer = AllocatePool (Context.Base64BufferSize);
if (Context.Base64Buffer == NULL) {
NvramDir->Close (NvramDir);
FreePool (Context.DataBuffer);
return EFI_OUT_OF_RESOURCES;
}
Context.StringBuffer = OcAsciiStringBufferInit ();
if (Context.StringBuffer == NULL) {
NvramDir->Close (NvramDir);
FreePool (Context.DataBuffer);
FreePool (Context.Base64Buffer);
return EFI_OUT_OF_RESOURCES;
}
Status = OcAsciiStringBufferAppend (
Context.StringBuffer,
"\n"
"\n"
"\n"
"\n"
"\tAdd\n"
"\t\n"
);
if (EFI_ERROR (Status)) {
NvramDir->Close (NvramDir);
FreePool (Context.DataBuffer);
FreePool (Context.Base64Buffer);
OcAsciiStringBufferFree (&Context.StringBuffer);
return Status;
}
for (GuidIndex = 0; GuidIndex < mLegacyMap->Count; ++GuidIndex) {
Status = OcProcessVariableGuid (
OC_BLOB_GET (mLegacyMap->Keys[GuidIndex]),
&Context.SectionGuid,
mLegacyMap,
&Context.SchemaEntry
);
if (EFI_ERROR (Status)) {
Status = EFI_SUCCESS;
continue;
}
Status = OcAsciiStringBufferSPrint (
Context.StringBuffer,
"\t\t%g%c"
"\t\t%c",
&Context.SectionGuid,
'\n',
'\n'
);
if (EFI_ERROR (Status)) {
break;
}
OcScanVariables (SerializeSectionVariables, &Context);
Status = Context.Status;
if (EFI_ERROR (Status)) {
break;
}
Status = OcAsciiStringBufferAppend (
Context.StringBuffer,
"\t\t\n"
);
if (EFI_ERROR (Status)) {
break;
}
}
FreePool (Context.DataBuffer);
FreePool (Context.Base64Buffer);
if (!EFI_ERROR (Status)) {
Status = OcAsciiStringBufferSPrint (
Context.StringBuffer,
"\t%c"
"\tVersion%c"
"\t%u%c"
"%c"
"%c",
'\n',
'\n',
OC_NVRAM_STORAGE_VERSION,
'\n',
'\n',
'\n'
);
}
if (EFI_ERROR (Status)) {
NvramDir->Close (NvramDir);
OcAsciiStringBufferFree (&Context.StringBuffer);
return Status;
}
DeleteFile (NvramDir, OPEN_CORE_NVRAM_FILENAME);
STATIC_ASSERT (NVRAM_PLIST_MAX_SIZE <= MAX_UINT32, "NVRAM_PLIST_MAX_SIZE must be less than or equal to UINT32_MAX");
if (Context.StringBuffer->StringLength > NVRAM_PLIST_MAX_SIZE) {
Status = EFI_OUT_OF_RESOURCES;
} else {
Status = OcSetFileData (
NvramDir,
OPEN_CORE_NVRAM_FILENAME,
Context.StringBuffer->String,
(UINT32)Context.StringBuffer->StringLength
);
}
OcAsciiStringBufferFree (&Context.StringBuffer);
NvramDir->Close (NvramDir);
return Status;
}
STATIC
EFI_STATUS
EFIAPI
ResetNvram (
VOID
)
{
EFI_STATUS Status;
EFI_STATUS AltStatus;
EFI_FILE_PROTOCOL *NvramDir;
Status = LocateNvramDir (&NvramDir);
if (EFI_ERROR (Status)) {
return Status;
}
Status = DeleteFile (NvramDir, OPEN_CORE_NVRAM_FILENAME);
AltStatus = DeleteFile (NvramDir, OPEN_CORE_NVRAM_FALLBACK_FILENAME);
NvramDir->Close (NvramDir);
return EFI_ERROR (Status) ? Status : AltStatus;
}
//
// If Luanchd.command is installed this should correctly handle reboots during full or partial OTA updates.
// When installing from USB we will likely go to the wrong OS after first reboot: the one in nvram.fallback,
// if present (which might or might not be the drive we installed to); otherwise the empty NVRAM default OS
// (likewise). Apart from needing to manually select that, at some point, the install should otherwise be correct.
//
STATIC
EFI_STATUS
EFIAPI
SwitchToFallback (
VOID
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *NvramDir;
UINT8 *FileBuffer;
UINT32 FileSize;
Status = LocateNvramDir (&NvramDir);
if (EFI_ERROR (Status)) {
return Status;
}
//
// Given that this approach is designed to avoid continually displaying 'macOS Installer' option,
// we want to switch back to empty NVRAM defaults even if nvram.fallback does not exist, so we
// do not check that.
//
FileBuffer = OcReadFileFromDirectory (NvramDir, OPEN_CORE_NVRAM_FILENAME, &FileSize, NVRAM_PLIST_MAX_SIZE);
if (FileBuffer == NULL) {
NvramDir->Close (NvramDir);
return EFI_ALREADY_STARTED;
}
DeleteFile (NvramDir, OPEN_CORE_NVRAM_USED_FILENAME);
DeleteFile (NvramDir, OPEN_CORE_NVRAM_FILENAME);
Status = OcSetFileData (
NvramDir,
OPEN_CORE_NVRAM_USED_FILENAME,
FileBuffer,
FileSize
);
NvramDir->Close (NvramDir);
FreePool (FileBuffer);
return Status;
}
STATIC
OC_VARIABLE_RUNTIME_PROTOCOL
mOcVariableRuntimeProtocol = {
OC_VARIABLE_RUNTIME_PROTOCOL_REVISION,
LoadNvram,
SaveNvram,
ResetNvram,
SwitchToFallback
};
EFI_STATUS
EFIAPI
OcVariableRuntimeLibConstructor (
IN EFI_HANDLE ImageHandle,
IN EFI_SYSTEM_TABLE *SystemTable
)
{
return gBS->InstallMultipleProtocolInterfaces (
&ImageHandle,
&gOcVariableRuntimeProtocolGuid,
&mOcVariableRuntimeProtocol,
NULL
);
}