/** @file Boot Loader Spec / Grub2 blscfg module loader entry parser. Copyright (c) 2021, Mike Beaton. All rights reserved.
SPDX-License-Identifier: BSD-3-Clause **/ #include "LinuxBootInternal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // // 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; }