Mike Beaton 6bb481ed62 OpenLinuxBoot: Fix booting with TuneD in Fedora 41
In GRUB2+blscfg mode:
 - Allow grub vars in 'initrd' as well as in 'options'
 - Allow multiple initrd files on one 'initrd' line
 - Initialise empty $tuned_params and $tuned_initrd grub vars if no
   values present, on an optional flag enabled by default (since
   we want to make booting major distros easy)

In GRUB2+blscfg mode (seem to be allowed now, not required for fix):
 - Allow multiple 'initrd' lines
 - Allow multiple 'options' lines

Add variant of OcParseVars which can parse as value-only tokens.

Signed-off-by: Mike Beaton <mjsbeaton@gmail.com>
2024-11-18 23:28:33 +00:00

1553 lines
37 KiB
C

/** @file
Boot Loader Spec / Grub2 blscfg module loader entry parser.
Copyright (c) 2021, Mike Beaton. All rights reserved.<BR>
SPDX-License-Identifier: BSD-3-Clause
**/
#include "LinuxBootInternal.h"
#include <Uefi.h>
#include <Library/BaseLib.h>
#include <Library/DevicePathLib.h>
#include <Library/MemoryAllocationLib.h>
#include <Library/OcBootManagementLib.h>
#include <Library/OcDebugLogLib.h>
#include <Library/OcFileLib.h>
#include <Library/OcFlexArrayLib.h>
#include <Library/OcStringLib.h>
#include <Library/PrintLib.h>
#include <Library/SortLib.h>
#include <Library/UefiBootServicesTableLib.h>
#include <Protocol/OcBootEntry.h>
//
// Vars which require blank values if not present after GRUB2+blscfg var parsing, in order
// to fix up the fact that TuneD does not always initialise them.
//
STATIC CHAR8 *mTuneDVars[] = {
"tuned_params",
"tuned_initrd"
};
//
// Root.
//
#define ROOT_DIR L"\\"
//
// Required where the BTRFS subvolume is /boot, as this looks like a
// normal directory within EFI. Note that scanning / and then /boot
// is how blscfg behaves by default too.
//
#define BOOT_DIR L"\\boot"
//
// Don't add missing root= option unless this directory is present at root.
//
#define OSTREE_DIR L"\\ostree"
//
// No leading slash so they can be relative to root or
// additional scan dir.
//
#define LOADER_ENTRIES_DIR L"loader\\entries"
#define GRUB2_GRUB_CFG L"grub2\\grub.cfg"
#define GRUB2_GRUBENV L"grub2\\grubenv"
#define GRUB2_GRUBENV_SIZE SIZE_1KB
#define BLSPEC_SUFFIX_CONF L".conf"
#define BLSPEC_PREFIX_AUTO L"auto-"
//
// Put a limit on entry name length, since the base part of the filename
// is used for the id, i.e. may be stored in BOOT#### entry in NVRAM.
// We then re-use the same filename length limit for vmlinuz and initrd files.
// Since we are using pure EFISTUB loading, initrd file paths have to be passed
// in kernel args (which have an unknown max. length of 256-4096 bytes).
//
#define MAX_LOADER_ENTRY_NAME_LEN (127)
#define MAX_LOADER_ENTRY_FILE_INFO_SIZE ( \
SIZE_OF_EFI_FILE_INFO + \
(MAX_LOADER_ENTRY_NAME_LEN + L_STR_LEN (BLSPEC_SUFFIX_CONF) + 1) * sizeof (CHAR16) \
)
//
// Might as well put an upper limit for some kind of sanity check.
// Typical files are ~350 bytes.
//
#define MAX_LOADER_ENTRY_FILE_SIZE SIZE_4KB
//
// grub2/grub.cfg was found, we treat this as enough to show that /loader/entries
// we have found are a GRUB2+blscfg setup.
//
STATIC
BOOLEAN
mIsGrub2;
/*
loader entry processing states
*/
typedef enum ENTRY_PARSE_STATE_ {
ENTRY_LEADING_SPACE,
ENTRY_COMMENT,
ENTRY_KEY,
ENTRY_KEY_VALUE_SPACE,
ENTRY_VALUE
} ENTRY_PARSE_STATE;
//
// First match, therefore Ubuntu should come after similar variants,
// and probably very short strings should come last in case they
// occur elsewhere in another kernel version string.
// Note: Should be kept in sync with Flavours.md.
//
STATIC
CHAR8 *
mLinuxVariants[] = {
"Arch",
"Astra",
"CentOS",
"Debian",
"Deepin",
"elementaryOS",
"Endless",
"Gentoo",
"Fedora",
"KDEneon",
"Kali",
"Mageia",
"Manjaro",
"Mint",
"openSUSE",
"Oracle",
"PopOS",
"RHEL",
"Rocky",
"Solus",
"Lubuntu",
"UbuntuMATE",
"Xubuntu",
"Ubuntu",
"Void",
"Zorin",
"MX"
};
STATIC
CHAR8 *
ExtractVariantFrom (
IN CHAR8 *String
)
{
UINTN Index;
CHAR8 *Variant;
Variant = NULL;
for (Index = 0; Index < ARRAY_SIZE (mLinuxVariants); Index++) {
if (OcAsciiStriStr (String, mLinuxVariants[Index]) != NULL) {
Variant = mLinuxVariants[Index];
break;
}
}
return Variant;
}
//
// Skips comment lines, and lines with key but no value
// EFI_LOAD_ERROR - File has invalid chars
//
STATIC
EFI_STATUS
GetLoaderEntryLine (
IN CONST CHAR16 *FileName,
IN OUT CHAR8 *Content,
IN OUT UINTN *Pos,
OUT CHAR8 **Key,
OUT CHAR8 **Value
)
{
ENTRY_PARSE_STATE State;
CHAR8 *LastSpace;
CHAR8 Ch;
BOOLEAN IsComplete;
*Key = NULL;
*Value = NULL;
State = ENTRY_LEADING_SPACE;
IsComplete = FALSE;
do {
Ch = Content[*Pos];
if (!((Ch == '\0') || (Ch == '\t') || (Ch == '\n') || ((Ch >= 20) && (Ch < 128)))) {
DEBUG ((DEBUG_WARN, "LNX: Invalid char 0x%x in %s\n", Ch, FileName));
return EFI_INVALID_PARAMETER;
}
switch (State) {
case ENTRY_LEADING_SPACE:
if ((Ch == '\n') || (Ch == '\0')) {
//
// Skip empty line
//
} else if ((Ch == ' ') || (Ch == '\t')) {
} else if (Ch == '#') {
State = ENTRY_COMMENT;
} else {
*Key = &Content[*Pos];
State = ENTRY_KEY;
}
break;
case ENTRY_COMMENT:
if (Ch == '\n') {
State = ENTRY_LEADING_SPACE;
}
break;
case ENTRY_KEY:
if ((Ch == '\n') || (Ch == '\0')) {
//
// No value, skip line
//
} else if ((Ch == ' ') || (Ch == '\t')) {
Content[*Pos] = '\0';
State = ENTRY_KEY_VALUE_SPACE;
}
break;
case ENTRY_KEY_VALUE_SPACE:
if ((Ch == '\n') || (Ch == '\0')) {
//
// No value, skip line
//
} else if ((Ch == ' ') || (Ch == '\t')) {
} else {
*Value = &Content[*Pos];
State = ENTRY_VALUE;
LastSpace = NULL;
}
break;
case ENTRY_VALUE:
if ((Ch == '\n') || (Ch == '\0')) {
if (LastSpace != NULL) {
*LastSpace = '\0';
} else {
Content[*Pos] = '\0';
}
IsComplete = TRUE;
} else if ((Ch == ' ') || (Ch == '\t')) {
LastSpace = &Content[*Pos];
} else {
LastSpace = NULL;
}
break;
default:
ASSERT (FALSE);
return EFI_INVALID_PARAMETER;
}
if (Ch != '\0') {
++(*Pos);
}
} while (Ch != '\0' && !IsComplete);
if (!IsComplete) {
return EFI_NOT_FOUND;
}
return EFI_SUCCESS;
}
LOADER_ENTRY *
InternalAllocateLoaderEntry (
VOID
)
{
LOADER_ENTRY *Entry;
Entry = OcFlexArrayAddItem (gLoaderEntries);
if (Entry != NULL) {
Entry->Options = OcFlexArrayInit (sizeof (CHAR8 *), OcFlexArrayFreePointerItem);
if (Entry->Options == NULL) {
InternalFreeLoaderEntry (Entry);
Entry = NULL;
}
}
if (Entry != NULL) {
Entry->Initrds = OcFlexArrayInit (sizeof (CHAR8 *), OcFlexArrayFreePointerItem);
if (Entry->Initrds == NULL) {
InternalFreeLoaderEntry (Entry);
Entry = NULL;
}
}
return Entry;
}
VOID
InternalFreeLoaderEntry (
LOADER_ENTRY *Entry
)
{
ASSERT (Entry != NULL);
if (Entry == NULL) {
return;
}
if (Entry->Title) {
FreePool (Entry->Title);
}
if (Entry->Version) {
FreePool (Entry->Version);
}
if (Entry->Linux) {
FreePool (Entry->Linux);
}
OcFlexArrayFree (&Entry->Initrds);
OcFlexArrayFree (&Entry->Options);
if (Entry->OcId != NULL) {
FreePool (Entry->OcId);
}
if (Entry->OcFlavour != NULL) {
FreePool (Entry->OcFlavour);
}
}
STATIC
EFI_STATUS
EntryCopySingleValue (
IN CONST BOOLEAN Grub2,
IN OUT CHAR8 **Target,
IN CONST CHAR8 *Value
)
{
if (!Grub2 || (*Target == NULL)) {
if (*Target != NULL) {
FreePool (*Target);
}
*Target = AllocateCopyPool (AsciiStrSize (Value), Value);
if (*Target == NULL) {
return EFI_OUT_OF_RESOURCES;
}
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
EntryCopyMultipleValue (
IN CONST BOOLEAN Grub2,
IN OC_FLEX_ARRAY *Array,
IN CONST CHAR8 *Value
)
{
VOID **NewItem;
if (Grub2 && (Array->Items != NULL)) {
return EFI_SUCCESS;
}
NewItem = OcFlexArrayAddItem (Array);
if (NewItem == NULL) {
return EFI_OUT_OF_RESOURCES;
}
*NewItem = AllocateCopyPool (AsciiStrSize (Value), Value);
if (*NewItem == NULL) {
return EFI_OUT_OF_RESOURCES;
}
return EFI_SUCCESS;
}
EFI_STATUS
InternalProcessLoaderEntryFile (
IN CONST CHAR16 *FileName,
IN OUT CHAR8 *Content,
OUT LOADER_ENTRY *Entry,
IN CONST BOOLEAN Grub2
)
{
EFI_STATUS Status;
UINTN Pos;
CHAR8 *Key;
CHAR8 *Value;
ASSERT (Content != NULL);
Status = EFI_SUCCESS;
Pos = 0;
//
// Grub2 blscfg module uses first only (even for options,
// which should allow more than one according to BL Spec).
// systemd-boot uses multiple (concatenated with a space)
// for options, and last only for everything which should
// have only one.
// Both use multiple for initrd.
//
while (TRUE) {
Status = GetLoaderEntryLine (FileName, Content, &Pos, &Key, &Value);
if (Status == EFI_NOT_FOUND) {
break;
}
if (EFI_ERROR (Status)) {
return Status;
}
if (AsciiStrCmp (Key, "title") == 0) {
Status = EntryCopySingleValue (Grub2, &Entry->Title, Value);
} else if (AsciiStrCmp (Key, "version") == 0) {
Status = EntryCopySingleValue (Grub2, &Entry->Version, Value);
} else if (AsciiStrCmp (Key, "linux") == 0) {
Status = EntryCopySingleValue (Grub2, &Entry->Linux, Value);
} else if (AsciiStrCmp (Key, "options") == 0) {
Status = EntryCopyMultipleValue (Grub2, Entry->Options, Value);
} else if (AsciiStrCmp (Key, "initrd") == 0) {
Status = EntryCopyMultipleValue (FALSE, Entry->Initrds, Value);
}
if (EFI_ERROR (Status)) {
return Status;
}
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
ExpandReplaceOptions (
IN OUT LOADER_ENTRY *Entry
)
{
EFI_STATUS Status;
CHAR8 **Options;
CHAR8 *NewOptions;
GRUB_VAR *DefaultOptionsVar;
if (Entry->Options->Count > 0) {
Status = InternalExpandGrubVarsForArray (Entry->Options);
if (EFI_ERROR (Status)) {
return Status;
}
} else {
//
// This is what grub2 blscfg does if there are no options.
//
DefaultOptionsVar = InternalGetGrubVar ("default_kernelopts");
if (DefaultOptionsVar != NULL) {
if (DefaultOptionsVar->Errors != 0) {
DEBUG ((DEBUG_WARN, "LNX: Unusable grub var $%a - 0x%x\n", "default_kernelopts", DefaultOptionsVar->Errors));
return EFI_INVALID_PARAMETER;
}
DEBUG ((DEBUG_INFO, "LNX: Using $%a\n", "default_kernelopts"));
//
// Blscfg expands $default_kernelopts, and we expand it even
// if !HasVars since we need the string to be realloced.
//
Status = InternalExpandGrubVars (DefaultOptionsVar->Value, &NewOptions);
if (EFI_ERROR (Status)) {
return Status;
}
Options = OcFlexArrayAddItem (Entry->Options);
if (Options == NULL) {
FreePool (NewOptions);
return EFI_OUT_OF_RESOURCES;
}
*Options = NewOptions;
}
}
return EFI_SUCCESS;
}
//
// Expand grub vars, and expand multiple initrds per line to multiple initrd lines.
// Do this before checking for files, etc.
//
STATIC
EFI_STATUS
ExpandInitrds (
IN OUT LOADER_ENTRY *Entry
)
{
EFI_STATUS Status;
UINTN OptionsIndex;
CHAR8 **Options;
OC_FLEX_ARRAY *ExpandedInitrds;
OC_FLEX_ARRAY *SplitInitrds;
UINTN SplitInitrdsIndex;
if (Entry->Initrds->Count == 0) {
return EFI_SUCCESS;
}
Status = InternalExpandGrubVarsForArray (Entry->Initrds);
if (EFI_ERROR (Status)) {
return Status;
}
ExpandedInitrds = OcFlexArrayInit (sizeof (CHAR8 *), OcFlexArrayFreePointerItem);
for (OptionsIndex = 0; OptionsIndex < Entry->Initrds->Count; OptionsIndex++) {
Options = OcFlexArrayItemAt (Entry->Initrds, OptionsIndex);
Status = OcParseVars (*Options, &SplitInitrds, OcStringFormatAscii, TRUE);
if (EFI_ERROR (Status)) {
break;
}
for (SplitInitrdsIndex = 0; SplitInitrdsIndex < SplitInitrds->Count; SplitInitrdsIndex++) {
Status = EntryCopyMultipleValue (FALSE, ExpandedInitrds, OcParsedVarsItemAt (SplitInitrds, SplitInitrdsIndex)->Ascii.Value);
if (EFI_ERROR (Status)) {
break;
}
}
OcFlexArrayFree (&SplitInitrds);
if (EFI_ERROR (Status)) {
break;
}
}
if (EFI_ERROR (Status)) {
OcFlexArrayFree (&ExpandedInitrds);
return Status;
}
ASSERT (ExpandedInitrds->Count >= Entry->Initrds->Count);
OcFlexArrayFree (&Entry->Initrds);
Entry->Initrds = ExpandedInitrds;
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
DoFilterLoaderEntry (
EFI_FILE_HANDLE Directory,
EFI_FILE_INFO *FileInfo,
UINTN FileInfoSize,
VOID *Context OPTIONAL
)
{
if ((FileInfo->Attribute & EFI_FILE_DIRECTORY) != 0) {
return EFI_NOT_FOUND;
}
//
// Skip ".*" and case sensitive "auto-*" files,
// follows systemd-boot logic.
//
if ((FileInfo->FileName[0] == L'.') ||
(StrnCmp (FileInfo->FileName, BLSPEC_PREFIX_AUTO, L_STR_LEN (BLSPEC_PREFIX_AUTO)) == 0))
{
return EFI_NOT_FOUND;
}
if (!OcUnicodeEndsWith (FileInfo->FileName, BLSPEC_SUFFIX_CONF, TRUE)) {
return EFI_NOT_FOUND;
}
return EFI_SUCCESS;
}
//
// Works for vmlinuz-{version} and {machine-id}-{version}.conf.
// If blspec/blscfg style then we want .conf files with the same machine-id
// to count as alternate kernels of the same install; but there might be
// different installs, and the same kernel might be on different installs;
// so {machine-id} is good as the shared id (and it would not be sufficient
// to just use {version} when generating a non-shared id).
// If autodetect, we assume there is only one install in the directory,
// so {vmlinuz} is good as the shared id.
//
EFI_STATUS
InternalIdVersionFromFileName (
IN OUT LOADER_ENTRY *Entry,
IN CHAR16 *FileName
)
{
EFI_STATUS Status;
UINTN NumCopied;
CHAR16 *Split;
CHAR16 *IdEnd;
CHAR16 *VersionStart;
CHAR16 *VersionEnd;
CHAR8 *OcId;
CHAR8 *Version;
ASSERT (Entry != NULL);
ASSERT (FileName != NULL);
Split = NULL;
Split = OcStrChr (FileName, L'-');
VersionEnd = &FileName[StrLen (FileName)];
if (OcUnicodeEndsWith (FileName, BLSPEC_SUFFIX_CONF, TRUE)) {
VersionEnd -= L_STR_LEN (BLSPEC_SUFFIX_CONF);
}
//
// Will not happen with sane filenames.
//
if ((Split != NULL) && ((Split == FileName) || (VersionEnd == Split + 1))) {
Split = NULL;
}
if (Split == NULL) {
VersionStart = FileName;
IdEnd = VersionEnd;
} else {
VersionStart = Split + 1;
if ((gLinuxBootFlags & LINUX_BOOT_USE_LATEST) != 0) {
IdEnd = Split;
} else {
IdEnd = VersionEnd;
}
}
OcId = AllocatePool (IdEnd - FileName + 1);
if (OcId == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Status = UnicodeStrnToAsciiStrS (FileName, IdEnd - FileName, OcId, IdEnd - FileName + 1, &NumCopied);
ASSERT_EFI_ERROR (Status);
ASSERT (NumCopied == (UINTN)(IdEnd - FileName));
Entry->OcId = OcId;
if (Entry->Version == NULL) {
Version = AllocatePool (VersionEnd - VersionStart + 1);
if (Version == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Status = UnicodeStrnToAsciiStrS (VersionStart, VersionEnd - VersionStart, Version, VersionEnd - VersionStart + 1, &NumCopied);
ASSERT_EFI_ERROR (Status);
ASSERT (NumCopied == (UINTN)(VersionEnd - VersionStart));
Entry->Version = Version;
}
return EFI_SUCCESS;
}
//
// We do not need to warn about missing files: OC warns about
// missing bootable file (vmlinuz) and kernel stops and warns
// about missing initrd. However, some linuxes (e.g. Endless)
// make the filepaths specified in an entry relative to /boot,
// not relative to / as it should be, so we have to check which
// it is. Only for this reason, we end up warning here if we
// can't find it at all.
//
STATIC
EFI_STATUS
FindLoaderFile (
IN CONST EFI_FILE_HANDLE Directory,
IN CONST CHAR16 *DirName,
IN OUT CHAR8 **FileName
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *File;
UINTN FileNameLen;
UINTN DirNameLen;
CHAR16 *Path;
UINTN MaxPathSize;
ASSERT (DirName != NULL);
ASSERT (FileName != NULL);
ASSERT (*FileName != NULL);
FileNameLen = AsciiStrLen (*FileName);
DirNameLen = StrLen (DirName);
MaxPathSize = (DirNameLen + FileNameLen + 1) * sizeof (CHAR16);
Path = AllocatePool (MaxPathSize);
if (Path == NULL) {
return EFI_OUT_OF_RESOURCES;
}
UnicodeSPrintAsciiFormat (Path, MaxPathSize, "%a", *FileName);
UnicodeUefiSlashes (Path);
Status = OcSafeFileOpen (Directory, &File, Path, EFI_FILE_MODE_READ, 0);
if (!EFI_ERROR (Status)) {
File->Close (File);
FreePool (Path);
return Status;
}
if (DirNameLen <= 1) {
DEBUG ((DEBUG_WARN, "LNX: %a not found - %r\n", *FileName, Status));
FreePool (Path);
return Status;
}
UnicodeSPrintAsciiFormat (Path, MaxPathSize, "%s%a", DirName, *FileName);
UnicodeUefiSlashes (Path);
Status = OcSafeFileOpen (Directory, &File, Path, EFI_FILE_MODE_READ, 0);
if (!EFI_ERROR (Status)) {
//
// Found at 'wrong' location - re-use allocated path
//
File->Close (File);
AsciiSPrint ((CHAR8 *)Path, MaxPathSize, "%s%a", DirName, *FileName);
AsciiUnixSlashes ((CHAR8 *)Path);
FreePool (*FileName);
*FileName = (CHAR8 *)Path;
return EFI_SUCCESS;
}
DEBUG ((DEBUG_WARN, "LNX: %a not found (searched %s and %s) - %r\n", *FileName, ROOT_DIR, DirName, Status));
FreePool (Path);
return Status;
}
STATIC
BOOLEAN
HasRootOption (
IN OC_FLEX_ARRAY *Options
)
{
EFI_STATUS Status;
UINTN Index;
CHAR8 **Option;
CHAR8 *OptionCopy;
OC_FLEX_ARRAY *ParsedVars;
BOOLEAN HasRoot;
ASSERT (Options != NULL);
//
// TRUE on errors: do not attempt to add root.
//
for (Index = 0; Index < Options->Count; Index++) {
Option = OcFlexArrayItemAt (Options, Index);
ASSERT (Option != NULL);
ASSERT (*Option != NULL);
OptionCopy = AllocateCopyPool (AsciiStrSize (*Option), *Option);
if (OptionCopy == NULL) {
return TRUE;
}
Status = OcParseVars (OptionCopy, &ParsedVars, OcStringFormatAscii, FALSE);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_WARN, "LNX: Error parsing Options[%u]=<%a> - %r\n", Index, *Option, Status));
FreePool (OptionCopy);
return TRUE;
}
HasRoot = OcHasParsedVar (ParsedVars, "root", OcStringFormatAscii);
FreePool (ParsedVars);
FreePool (OptionCopy);
if (HasRoot) {
return TRUE;
}
}
return FALSE;
}
STATIC
BOOLEAN
HasOstreeDir (
EFI_FILE_HANDLE Directory
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *OstreeFile;
Status = OcSafeFileOpen (Directory, &OstreeFile, OSTREE_DIR, EFI_FILE_MODE_READ, 0);
if (EFI_ERROR (Status)) {
return FALSE;
}
Status = OcEnsureDirectoryFile (OstreeFile, TRUE);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_WARN, "LNX: %s found but not a %a - %r\n", OSTREE_DIR, "directory", Status));
}
OstreeFile->Close (OstreeFile);
return !EFI_ERROR (Status);
}
STATIC
EFI_STATUS
DoProcessLoaderEntry (
EFI_FILE_HANDLE Directory,
EFI_FILE_INFO *FileInfo,
UINTN FileInfoSize,
VOID *Context OPTIONAL
)
{
EFI_STATUS Status;
CHAR8 *Content;
LOADER_ENTRY *Entry;
CHAR16 SaveChar;
CHAR16 *DirName;
CHAR8 **Initrd;
UINTN Index;
ASSERT (Context != NULL);
DirName = Context;
if (FileInfoSize > MAX_LOADER_ENTRY_FILE_INFO_SIZE) {
SaveChar = FileInfo->FileName[MAX_LOADER_ENTRY_NAME_LEN];
FileInfo->FileName[MAX_LOADER_ENTRY_NAME_LEN] = CHAR_NULL;
DEBUG ((DEBUG_WARN, "LNX: Entry filename overlong: %s...\n", FileInfo->FileName));
FileInfo->FileName[MAX_LOADER_ENTRY_NAME_LEN] = SaveChar;
return EFI_NOT_FOUND;
}
if (FileInfo->FileSize > MAX_LOADER_ENTRY_FILE_SIZE) {
DEBUG ((DEBUG_WARN, "LNX: Entry file size overlong: %s\n", FileInfo->FileName));
return EFI_NOT_FOUND;
}
DEBUG ((
(gLinuxBootFlags & LINUX_BOOT_LOG_VERBOSE) == 0 ? DEBUG_VERBOSE : DEBUG_INFO,
"LNX: Reading %s...\n",
FileInfo->FileName
));
Content = OcReadFileFromDirectory (Directory, FileInfo->FileName, NULL, 0);
if (Content == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Entry = InternalAllocateLoaderEntry ();
if (Entry == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Status = InternalProcessLoaderEntryFile (FileInfo->FileName, Content, Entry, mIsGrub2);
if (EFI_ERROR (Status)) {
OcFlexArrayDiscardItem (gLoaderEntries, TRUE);
return Status;
}
if (Entry->Linux == NULL) {
DEBUG ((
(gLinuxBootFlags & LINUX_BOOT_LOG_VERBOSE) == 0 ? DEBUG_VERBOSE : DEBUG_INFO,
"LNX: No linux line, ignoring\n"
));
OcFlexArrayDiscardItem (gLoaderEntries, TRUE);
return EFI_NOT_FOUND;
}
if (mIsGrub2) {
Status = ExpandReplaceOptions (Entry);
if (!EFI_ERROR (Status)) {
Status = ExpandInitrds (Entry);
}
if (EFI_ERROR (Status)) {
OcFlexArrayDiscardItem (gLoaderEntries, TRUE);
return Status;
}
}
//
// Check all files exist, see comment on FindLoaderFile.
//
Status = FindLoaderFile (Directory, DirName, &Entry->Linux);
if (!EFI_ERROR (Status)) {
for (Index = 0; Index < Entry->Initrds->Count; Index++) {
Initrd = OcFlexArrayItemAt (Entry->Initrds, Index);
Status = FindLoaderFile (Directory, DirName, Initrd);
if (EFI_ERROR (Status)) {
break;
}
}
}
if (EFI_ERROR (Status)) {
OcFlexArrayDiscardItem (gLoaderEntries, TRUE);
return Status;
}
//
// Need to understand other reasons to apply this fix (if any),
// so not automatically applying unless we recognise the layout.
//
if ( ((gLinuxBootFlags & LINUX_BOOT_ALLOW_CONF_AUTO_ROOT) != 0)
&& !HasRootOption (Entry->Options))
{
if (!HasOstreeDir (Directory)) {
DEBUG ((DEBUG_WARN, "LNX: Missing root option, %s %afound - %afixing\n", OSTREE_DIR, "not ", "not "));
} else {
DEBUG ((DEBUG_INFO, "LNX: Missing root option, %s %afound - %afixing\n", OSTREE_DIR, "", ""));
Status = InsertRootOption (Entry->Options);
if (EFI_ERROR (Status)) {
OcFlexArrayDiscardItem (gLoaderEntries, TRUE);
return Status;
}
}
}
//
// Id, and version if not already set within .conf data, from filename.
//
Status = InternalIdVersionFromFileName (Entry, FileInfo->FileName);
if (EFI_ERROR (Status)) {
OcFlexArrayDiscardItem (gLoaderEntries, TRUE);
return Status;
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
ProcessLoaderEntry (
EFI_FILE_HANDLE Directory,
EFI_FILE_INFO *FileInfo,
UINTN FileInfoSize,
VOID *Context OPTIONAL
)
{
EFI_STATUS Status;
Status = DoFilterLoaderEntry (Directory, FileInfo, FileInfoSize, Context);
if (EFI_ERROR (Status)) {
return Status;
}
Status = DoProcessLoaderEntry (Directory, FileInfo, FileInfoSize, Context);
if (EFI_ERROR (Status)) {
DEBUG ((DEBUG_WARN, "LNX: Error processing %s - %r\n", FileInfo->FileName, Status));
//
// Continue to use earlier or later .conf files which we can process -
// more chance of a way to boot into Linux for end user.
//
return EFI_NOT_FOUND;
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
FixTuneDVars (
VOID
)
{
EFI_STATUS Status;
UINTN Index;
STATIC_ASSERT (ARRAY_SIZE (mTuneDVars) > 0, "No TuneD vars to set");
Status = EFI_SUCCESS;
for (Index = 0; Index < ARRAY_SIZE (mTuneDVars); Index++) {
if (InternalGetGrubVar (mTuneDVars[Index]) == NULL) {
Status = InternalSetGrubVar (mTuneDVars[Index], "", VAR_ERR_NONE);
if (EFI_ERROR (Status)) {
break;
}
}
}
return Status;
}
STATIC
EFI_STATUS
ScanLoaderEntriesAtDirectory (
IN EFI_FILE_PROTOCOL *RootDirectory,
IN CHAR16 *DirName,
OUT OC_PICKER_ENTRY **Entries,
OUT UINTN *NumEntries
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *EntriesDirectory;
CHAR8 *GrubCfg;
CHAR8 *GrubEnv;
GRUB_VAR *EarlyInitrdVar;
Status = OcSafeFileOpen (RootDirectory, &EntriesDirectory, LOADER_ENTRIES_DIR, EFI_FILE_MODE_READ, 0);
if (EFI_ERROR (Status)) {
return Status;
}
Status = EFI_SUCCESS;
GrubEnv = NULL;
mIsGrub2 = FALSE;
gLoaderEntries = NULL;
//
// Treat /loader/entries as being GRUB2+blscfg style if /grub2/grub.cfg exists.
//
GrubCfg = OcReadFileFromDirectory (RootDirectory, GRUB2_GRUB_CFG, NULL, 0);
if (GrubCfg == NULL) {
DEBUG ((DEBUG_INFO, "LNX: %s not found\n", GRUB2_GRUB_CFG));
} else {
mIsGrub2 = TRUE;
Status = InternalInitGrubVars ();
if (!EFI_ERROR (Status)) {
//
// Read grubenv first, since vars in grub.cfg should overwrite it.
//
GrubEnv = OcReadFileFromDirectory (RootDirectory, GRUB2_GRUBENV, NULL, GRUB2_GRUBENV_SIZE);
if (GrubEnv == NULL) {
DEBUG ((DEBUG_WARN, "LNX: %s not found\n", GRUB2_GRUBENV));
} else {
DEBUG ((
(gLinuxBootFlags & LINUX_BOOT_LOG_VERBOSE) == 0 ? DEBUG_VERBOSE : DEBUG_INFO,
"LNX: Reading %s\n",
GRUB2_GRUBENV
));
Status = InternalProcessGrubEnv (GrubEnv, GRUB2_GRUBENV_SIZE);
}
if (!EFI_ERROR (Status)) {
DEBUG ((
(gLinuxBootFlags & LINUX_BOOT_LOG_VERBOSE) == 0 ? DEBUG_VERBOSE : DEBUG_INFO,
"LNX: Reading %s\n",
GRUB2_GRUB_CFG
));
Status = InternalProcessGrubCfg (GrubCfg);
}
if ( !EFI_ERROR (Status)
&& ((gLinuxBootFlags & LINUX_BOOT_FIX_TUNED) != 0))
{
DEBUG ((
(gLinuxBootFlags & LINUX_BOOT_LOG_VERBOSE) == 0 ? DEBUG_VERBOSE : DEBUG_INFO,
"LNX: Fix TuneD vars\n"
));
Status = FixTuneDVars ();
}
}
}
//
// If we are grub2 and $early_initrd exists, then warn and halt (blscfg logic is to use it).
// Would not be hard to implement in ExpandInitrds if required. This is a space separated list
// of filenames to use first as initrds.
//
if (!EFI_ERROR (Status)) {
if (mIsGrub2) {
EarlyInitrdVar = InternalGetGrubVar ("early_initrd");
if ((EarlyInitrdVar != NULL) &&
(EarlyInitrdVar->Value != NULL) &&
(EarlyInitrdVar->Value[0] != '\0')
)
{
DEBUG ((DEBUG_INFO, "LNX: grub var $%a is present but currently unsupported - aborting\n", "early_initrd"));
Status = EFI_INVALID_PARAMETER;
}
}
}
if (!EFI_ERROR (Status)) {
gLoaderEntries = OcFlexArrayInit (sizeof (LOADER_ENTRY), (OC_FLEX_ARRAY_FREE_ITEM)InternalFreeLoaderEntry);
if (gLoaderEntries == NULL) {
Status = EFI_OUT_OF_RESOURCES;
} else {
Status = OcScanDirectory (EntriesDirectory, ProcessLoaderEntry, DirName);
}
if (!EFI_ERROR (Status)) {
ASSERT (gLoaderEntries->Count > 0);
Status = InternalConvertLoaderEntriesToBootEntries (
RootDirectory,
Entries,
NumEntries
);
}
OcFlexArrayFree (&gLoaderEntries);
}
InternalFreeGrubVars ();
if (GrubEnv != NULL) {
FreePool (GrubEnv);
}
if (GrubCfg != NULL) {
FreePool (GrubCfg);
}
EntriesDirectory->Close (EntriesDirectory);
return Status;
}
STATIC
EFI_STATUS
DoScanLoaderEntries (
IN EFI_FILE_PROTOCOL *Directory,
IN CHAR16 *DirName,
OUT OC_PICKER_ENTRY **Entries,
OUT UINTN *NumEntries
)
{
EFI_STATUS Status;
Status = ScanLoaderEntriesAtDirectory (Directory, DirName, Entries, NumEntries);
DEBUG ((
(EFI_ERROR (Status) && Status != EFI_NOT_FOUND) ? DEBUG_WARN : DEBUG_INFO,
"LNX: ScanLoaderEntries %s - %r\n",
DirName,
Status
));
return Status;
}
EFI_STATUS
InternalScanLoaderEntries (
IN EFI_FILE_PROTOCOL *RootDirectory,
OUT OC_PICKER_ENTRY **Entries,
OUT UINTN *NumEntries
)
{
EFI_STATUS Status;
EFI_FILE_PROTOCOL *AdditionalScanDirectory;
Status = DoScanLoaderEntries (RootDirectory, ROOT_DIR, Entries, NumEntries);
if (EFI_ERROR (Status)) {
Status = OcSafeFileOpen (RootDirectory, &AdditionalScanDirectory, BOOT_DIR, EFI_FILE_MODE_READ, 0);
if (!EFI_ERROR (Status)) {
Status = DoScanLoaderEntries (AdditionalScanDirectory, BOOT_DIR, Entries, NumEntries);
AdditionalScanDirectory->Close (AdditionalScanDirectory);
}
}
return Status;
}
STATIC
EFI_STATUS
DoConvertLoaderEntriesToBootEntries (
OUT OC_PICKER_ENTRY **Entries,
OUT UINTN *NumEntries
)
{
EFI_STATUS Status;
UINTN Index;
UINTN OptionsIndex;
LOADER_ENTRY *Entry;
OC_FLEX_ARRAY *PickerEntries;
OC_PICKER_ENTRY *PickerEntry;
OC_ASCII_STRING_BUFFER *StringBuffer;
CHAR8 **Options;
UINTN OptionsLength;
StringBuffer = NULL;
PickerEntries = OcFlexArrayInit (sizeof (OC_PICKER_ENTRY), (OC_FLEX_ARRAY_FREE_ITEM)InternalFreePickerEntry);
if (PickerEntries == NULL) {
return EFI_OUT_OF_RESOURCES;
}
Status = EFI_SUCCESS;
for (Index = 0; Index < gLoaderEntries->Count; Index++) {
Entry = OcFlexArrayItemAt (gLoaderEntries, Index);
PickerEntry = OcFlexArrayAddItem (PickerEntries);
if (PickerEntry == NULL) {
Status = EFI_OUT_OF_RESOURCES;
break;
}
PickerEntry->Path = AllocateCopyPool (AsciiStrSize (Entry->Linux), Entry->Linux);
if (PickerEntry->Path == NULL) {
Status = EFI_OUT_OF_RESOURCES;
break;
}
//
// Arguments.
//
StringBuffer = OcAsciiStringBufferInit ();
if (StringBuffer == NULL) {
Status = EFI_OUT_OF_RESOURCES;
break;
}
for (OptionsIndex = 0; OptionsIndex < Entry->Initrds->Count; OptionsIndex++) {
Options = OcFlexArrayItemAt (Entry->Initrds, OptionsIndex);
Status = OcAsciiStringBufferAppend (StringBuffer, "initrd=");
if (EFI_ERROR (Status)) {
break;
}
Status = OcAsciiStringBufferAppend (StringBuffer, *Options);
if (EFI_ERROR (Status)) {
break;
}
Status = OcAsciiStringBufferAppend (StringBuffer, " ");
if (EFI_ERROR (Status)) {
break;
}
}
if (EFI_ERROR (Status)) {
break;
}
for (OptionsIndex = 0; OptionsIndex < Entry->Options->Count; OptionsIndex++) {
Options = OcFlexArrayItemAt (Entry->Options, OptionsIndex);
OptionsLength = AsciiStrLen (*Options);
ASSERT (OptionsLength != 0);
if (OptionsLength == 0) {
continue;
}
if ((*Options)[OptionsLength - 1] == ' ') {
--OptionsLength;
}
Status = OcAsciiStringBufferAppendN (StringBuffer, *Options, OptionsLength);
if (EFI_ERROR (Status)) {
break;
}
if (OptionsIndex < Entry->Options->Count - 1) {
Status = OcAsciiStringBufferAppend (StringBuffer, " ");
if (EFI_ERROR (Status)) {
break;
}
}
}
if (EFI_ERROR (Status)) {
break;
}
PickerEntry->Arguments = OcAsciiStringBufferFreeContainer (&StringBuffer);
//
// Name.
//
StringBuffer = OcAsciiStringBufferInit ();
if (StringBuffer == NULL) {
Status = EFI_OUT_OF_RESOURCES;
break;
}
if ((gLinuxBootFlags & LINUX_BOOT_ADD_DEBUG_INFO) != 0) {
//
// Show file system type and enough of PARTUUID to help distinguish one partition from another.
//
Status = OcAsciiStringBufferSPrint (StringBuffer, "%a-%08x", gFileSystemType, gPartuuid.Data1);
if (EFI_ERROR (Status)) {
break;
}
OcAsciiToLower (&StringBuffer->String[StringBuffer->StringLength] - sizeof (gPartuuid.Data1) * 2);
Status = OcAsciiStringBufferAppend (StringBuffer, ": ");
if (EFI_ERROR (Status)) {
break;
}
}
Status = OcAsciiStringBufferAppend (StringBuffer, Entry->Title);
if (EFI_ERROR (Status)) {
break;
}
PickerEntry->Name = OcAsciiStringBufferFreeContainer (&StringBuffer);
ASSERT (Entry->OcFlavour != NULL);
PickerEntry->Flavour = AllocateCopyPool (AsciiStrSize (Entry->OcFlavour), Entry->OcFlavour);
if (PickerEntry->Flavour == NULL) {
Status = EFI_OUT_OF_RESOURCES;
break;
}
ASSERT (Entry->OcId != NULL);
PickerEntry->Id = AllocateCopyPool (AsciiStrSize (Entry->OcId), Entry->OcId);
if (PickerEntry->Id == NULL) {
Status = EFI_OUT_OF_RESOURCES;
break;
}
PickerEntry->Auxiliary = Entry->OcAuxiliary;
PickerEntry->RealPath = TRUE;
PickerEntry->TextMode = FALSE;
PickerEntry->Tool = FALSE;
}
if (EFI_ERROR (Status)) {
if (StringBuffer != NULL) {
OcAsciiStringBufferFree (&StringBuffer);
}
OcFlexArrayFree (&PickerEntries);
} else {
ASSERT (StringBuffer == NULL);
OcFlexArrayFreeContainer (&PickerEntries, (VOID **)Entries, NumEntries);
}
return Status;
}
STATIC
EFI_STATUS
AppendVersion (
LOADER_ENTRY *Entry
)
{
EFI_STATUS Status;
OC_ASCII_STRING_BUFFER *StringBuffer;
ASSERT (Entry->Title != NULL);
if ((Entry->Version == NULL) || (AsciiStrStr (Entry->Title, Entry->Version) != NULL)) {
return EFI_SUCCESS;
}
StringBuffer = OcAsciiStringBufferInit ();
Status = OcAsciiStringBufferSPrint (StringBuffer, "%a (%a)", Entry->Title, Entry->Version);
if (!EFI_ERROR (Status)) {
FreePool (Entry->Title);
Entry->Title = OcAsciiStringBufferFreeContainer (&StringBuffer);
}
return Status;
}
STATIC
EFI_STATUS
AppendVersions (
VOID
)
{
EFI_STATUS Status;
UINTN Index;
LOADER_ENTRY *Entry;
if (gPickerContext->HideAuxiliary && ((gLinuxBootFlags & LINUX_BOOT_USE_LATEST) != 0)) {
return EFI_SUCCESS;
}
for (Index = 0; Index < gLoaderEntries->Count; Index++) {
Entry = OcFlexArrayItemAt (gLoaderEntries, Index);
Status = AppendVersion (Entry);
if (EFI_ERROR (Status)) {
return Status;
}
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
DisambiguateDuplicates (
VOID
)
{
UINTN Index;
UINTN MatchIndex;
LOADER_ENTRY *Entry;
LOADER_ENTRY *MatchEntry;
//
// This check should not be strictly necessary, since there shouldn't
// (normally) be any duplicate ids if this flag is not set.
//
if ((gLinuxBootFlags & LINUX_BOOT_USE_LATEST) == 0) {
return EFI_SUCCESS;
}
for (Index = 0; Index < gLoaderEntries->Count - 1; Index++) {
Entry = OcFlexArrayItemAt (gLoaderEntries, Index);
if (Entry->DuplicateIdScanned) {
continue;
}
for (MatchIndex = Index + 1; MatchIndex < gLoaderEntries->Count; MatchIndex++) {
MatchEntry = OcFlexArrayItemAt (gLoaderEntries, MatchIndex);
if (!MatchEntry->DuplicateIdScanned) {
if (AsciiStrCmp (Entry->OcId, MatchEntry->OcId) == 0) {
//
// Everything but the first id duplicate becomes auxiliary.
//
MatchEntry->OcAuxiliary = TRUE;
MatchEntry->DuplicateIdScanned = TRUE;
}
}
}
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
EntryApplyDefaults (
IN EFI_FILE_PROTOCOL *RootDirectory,
IN LOADER_ENTRY *Entry
)
{
CHAR8 *Variant;
UINTN Length;
UINTN NumPrinted;
Variant = NULL;
if (Entry->Title != NULL) {
Variant = ExtractVariantFrom (Entry->Title);
}
ASSERT (Entry->OcFlavour == NULL);
if (Variant != NULL) {
Length = AsciiStrLen (Variant) + L_STR_LEN ("Linux") + 1;
Entry->OcFlavour = AllocatePool (Length + 1);
if (Entry->OcFlavour == NULL) {
return EFI_OUT_OF_RESOURCES;
}
NumPrinted = AsciiSPrint (Entry->OcFlavour, Length + 1, "%a:%a", Variant, "Linux");
ASSERT (NumPrinted == Length);
} else {
Variant = "Linux";
Entry->OcFlavour = AllocateCopyPool (AsciiStrSize (Variant), Variant);
if (Entry->OcFlavour == NULL) {
return EFI_OUT_OF_RESOURCES;
}
}
if (Entry->Title == NULL) {
Entry->Title = AllocateCopyPool (AsciiStrSize (Variant), Variant);
if (Entry->Title == NULL) {
return EFI_OUT_OF_RESOURCES;
}
}
return EFI_SUCCESS;
}
STATIC
EFI_STATUS
ApplyDefaults (
IN EFI_FILE_PROTOCOL *RootDirectory
)
{
EFI_STATUS Status;
UINTN Index;
LOADER_ENTRY *Entry;
Status = EFI_SUCCESS;
for (Index = 0; Index < gLoaderEntries->Count; Index++) {
Entry = OcFlexArrayItemAt (gLoaderEntries, Index);
Status = EntryApplyDefaults (RootDirectory, Entry);
if (EFI_ERROR (Status)) {
break;
}
}
return Status;
}
//
// Also use for loader entries generated by autodetect.
//
EFI_STATUS
InternalConvertLoaderEntriesToBootEntries (
IN EFI_FILE_PROTOCOL *RootDirectory,
OUT OC_PICKER_ENTRY **Entries,
OUT UINTN *NumEntries
)
{
EFI_STATUS Status;
Status = EFI_SUCCESS;
//
// Sort entries by version descending.
//
PerformQuickSort (gLoaderEntries->Items, gLoaderEntries->Count, gLoaderEntries->ItemSize, InternalReverseVersionCompare);
//
// Autodetect good title and flavour, if needed.
//
Status = ApplyDefaults (RootDirectory);
//
// Always append version to titles when !HideAuxiliary.
// We previously implemented a pure-BLSpec process of disambiguating by appending version
// only if the title is ambiguous with others on the same partition, but it is more natural
// to do it always.
//
if (!EFI_ERROR (Status)) {
Status = AppendVersions ();
}
//
// Make duplicates by id after the first into auxiliary entries.
//
if (!EFI_ERROR (Status)) {
Status = DisambiguateDuplicates ();
}
if (!EFI_ERROR (Status)) {
Status = DoConvertLoaderEntriesToBootEntries (
Entries,
NumEntries
);
}
return Status;
}