using System; using System.Collections.Generic; using System.Text; using System.Linq; /** * A class for handling EFI boot entries. Lazy access with cache. * Notice: Data is not updated after the first read. */ public class EfiBootEntries { /** * Information about an EFI boot entry. */ public class BootEntryData { public UInt32 Attributes; public string Label; public class DevicePathNode { public byte Type, SubType; public byte[] Data; public DevicePathNode(byte[] data) { Type = data[0]; SubType = data[1]; Data = data.Skip(4).ToArray(); } public byte[] ToBytes() { var len = Data.Length + 4; return new byte[] { Type, SubType, (byte)(len & 0xff), (byte)(len >> 8) }.Concat(Data).ToArray(); } } public List DevicePathNodes; public byte[] Arguments; public BootEntryData(byte[] data) { Attributes = BitConverter.ToUInt32(data, 0); var pathNodesLength = BitConverter.ToUInt16(data, 4); Label = new string(Efi.BytesToUInt16s(data).Skip(3).TakeWhile(i => i != 0).Select(i => (char)i).ToArray()); var pos = 6 + 2 * (Label.Length + 1); var pathNodesEnd = pos + pathNodesLength; DevicePathNodes = new List(); Arguments = new byte[0]; while (pos + 4 <= pathNodesEnd) { var len = BitConverter.ToUInt16(data, pos + 2); if (len < 4 || pos + len > pathNodesEnd) { return; // throw new Exception("Bad entry."); } var node = new DevicePathNode(data.Skip(pos).Take(len).ToArray()); DevicePathNodes.Add(node); if (node.Type == 0x7f && node.SubType == 0xff) { // End of entire device path. // Apparently some firmwares produce paths with unused nodes at the end. break; } pos += len; } Arguments = data.Skip(pathNodesEnd).ToArray(); } public byte[] ToBytes() { return new byte[0] .Concat(BitConverter.GetBytes((UInt32) Attributes)) .Concat(BitConverter.GetBytes((UInt16) DevicePathNodes.Sum(n => n.Data.Length + 4))) .Concat(Encoding.Unicode.GetBytes(Label + "\0")) .Concat(DevicePathNodes.SelectMany(n => n.ToBytes())) .Concat(Arguments) .ToArray(); } public DevicePathNode FileNameNode { get { var d = DevicePathNodes; return d.Count > 1 && d[d.Count - 1].Type == 0x7F && d[d.Count - 2].Type == 0x04 ? d[d.Count - 2] : null; } } public bool HasFileName { get { return FileNameNode != null; } } public string FileName { get { if (!HasFileName) { return ""; } return new string(Encoding.Unicode.GetChars(FileNameNode.Data).TakeWhile(c => c != '\0').ToArray()); } set { if (!HasFileName) { throw new Exception("Logic error: Setting FileName on a bad boot entry."); } FileNameNode.Data = Encoding.Unicode.GetBytes(value + "\0"); } } } /** * Status of the own boot entry. */ public enum OwnEntryStatus { NotFound, Disabled, EnabledAfterWindows, Enabled } /** * Path to the Windows boot loader. */ public const string WindowsLoaderPath = "\\EFI\\Microsoft\\Boot\\bootmgfw.efi"; /** * Path to the HackBGRT loader. */ public const string OwnLoaderPath = "\\EFI\\HackBGRT\\loader.efi"; private readonly Dictionary cache; private readonly Efi.Variable BootOrder; private readonly Efi.Variable BootCurrent; private readonly List BootOrderInts; private readonly List BootCurrentInts; /** * Constructor. Reads BootOrder and BootCurrent. */ public EfiBootEntries() { cache = new Dictionary(); BootOrder = Efi.GetVariable("BootOrder"); BootCurrent = Efi.GetVariable("BootCurrent"); if (BootOrder.Data == null) { throw new Exception("Could not read BootOrder."); } BootCurrentInts = new List(Efi.BytesToUInt16s(BootCurrent.Data ?? new byte[0])); BootOrderInts = new List(Efi.BytesToUInt16s(BootOrder.Data)); } /** * Get the boot entry with the given number. * * @param num Number of the boot entry. * @return The boot entry. */ public (Efi.Variable, BootEntryData) GetEntry(UInt16 num) { if (!cache.ContainsKey(num)) { var v = Efi.GetVariable(String.Format("Boot{0:X04}", num)); cache[num] = (v, v.Data == null ? null : new BootEntryData(v.Data)); } return cache[num]; } /** * Find entry by file name. * * @param fileName File name of the boot entry. * @return The boot entry. */ public (UInt16, Efi.Variable, BootEntryData) FindEntry(string fileName) { var rest = Enumerable.Range(0, 0xff).Select(i => (UInt16) i); var entryAccessOrder = BootCurrentInts.Concat(BootOrderInts).Concat(rest); foreach (var num in entryAccessOrder) { var (v, e) = GetEntry(num); if (fileName == null ? e == null : (e != null && e.FileName.Equals(fileName, StringComparison.OrdinalIgnoreCase))) { return (num, v, e); } } return (0xffff, null, null); } /** * Get the Windows boot entry. */ public (UInt16, Efi.Variable, BootEntryData) WindowsEntry { get { return FindEntry(WindowsLoaderPath); } } /** * Get the HackBGRT boot entry. */ public (UInt16, Efi.Variable, BootEntryData) OwnEntry { get { return FindEntry(OwnLoaderPath); } } /** * Get a free boot entry. */ public (UInt16, Efi.Variable, BootEntryData) FreeEntry { get { return FindEntry(null); } } /** * Check if the own entry is enabled. */ public OwnEntryStatus GetOwnEntryStatus() { var (ownNum, ownVar, _) = OwnEntry; if (ownVar == null) { return OwnEntryStatus.NotFound; } var (msNum, _, _) = WindowsEntry; var msPos = BootOrderInts.IndexOf(msNum); var ownPos = BootOrderInts.IndexOf(ownNum); if (ownPos < 0) { return OwnEntryStatus.Disabled; } if (ownPos < msPos || msPos < 0) { return OwnEntryStatus.Enabled; } return OwnEntryStatus.EnabledAfterWindows; } /** * Disable the said boot entry from BootOrder. * * @param dryRun Don't actually write to NVRAM. * @return True, if the entry was found in BootOrder. */ public bool DisableOwnEntry(bool dryRun = false) { var (ownNum, ownVar, _) = OwnEntry; if (ownVar == null) { Setup.Log("Own entry not found."); return false; } Setup.Log($"Old boot order: {BootOrder}"); if (!BootOrderInts.Contains(ownNum)) { Setup.Log("Own entry not in BootOrder."); } else { Setup.Log($"Disabling own entry: {ownNum:X04}"); BootOrderInts.Remove(ownNum); BootOrder.Data = BootOrderInts.SelectMany(num => new byte[] { (byte)(num & 0xff), (byte)(num >> 8) }).ToArray(); Efi.SetVariable(BootOrder, dryRun); return true; } return false; } /** * Create the boot entry. * * @param alwaysCopyFromMS If true, do not preserve any existing data. * @param dryRun Don't actually write to NVRAM. */ public void MakeOwnEntry(bool alwaysCopyFromMS, bool dryRun = false) { var (msNum, msVar, msEntry) = WindowsEntry; var (ownNum, ownVar, ownEntry) = OwnEntry; if (ownVar == null) { (ownNum, ownVar, ownEntry) = FreeEntry; if (ownVar == null) { throw new Exception("MakeOwnEntry: No free entry."); } Setup.Log($"Creating own entry {ownNum:X4}."); } else { Setup.Log($"Updating own entry {ownNum:X4}."); } Setup.Log($"Read EFI variable: {msVar}"); Setup.Log($"Read EFI variable: {ownVar}"); // Make a new boot entry using the MS entry as a starting point. if (!alwaysCopyFromMS && ownEntry != null) { // Shim expects the arguments to be a filename or nothing. // But BCDEdit expects some Microsoft-specific data. // Modify the entry so that BCDEdit still recognises it // but the data becomes a valid UCS-2 / UTF-16LE file name. var str = new string(ownEntry.Arguments.Take(12).Select(c => (char) c).ToArray()); if (str == "WINDOWS\0\x01\0\0\0") { ownEntry.Arguments[8] = (byte) 'X'; } else if (str != "WINDOWS\0\x58\0\0\0") { // Not recognized. Clear the arguments. ownEntry.Arguments = new byte[0]; } } else { if (msEntry == null) { throw new Exception("MakeOwnEntry: Windows Boot Manager not found."); } ownEntry = msEntry; ownEntry.Arguments = new byte[0]; ownEntry.Label = "HackBGRT"; ownEntry.FileName = OwnLoaderPath; } ownEntry.Attributes = 1; // LOAD_OPTION_ACTIVE ownVar.Attributes = 7; // EFI_VARIABLE_NON_VOLATILE | EFI_VARIABLE_BOOTSERVICE_ACCESS | EFI_VARIABLE_RUNTIME_ACCESS ownVar.Data = ownEntry.ToBytes(); Efi.SetVariable(ownVar, dryRun); } /** * Enable the own boot entry. * * @param dryRun Don't actually write to NVRAM. */ public void EnableOwnEntry(bool dryRun = false) { var (ownNum, ownVar, _) = OwnEntry; if (ownVar == null) { Setup.Log("Own entry not found."); return; } var (msNum, _, _) = WindowsEntry; var msPos = BootOrderInts.IndexOf(msNum); var ownPos = BootOrderInts.IndexOf(ownNum); var mustAdd = ownPos == -1; var mustMove = 0 <= msPos && msPos <= ownPos; Setup.Log($"Old boot order: {BootOrder}"); if (mustAdd || mustMove) { Setup.Log($"Enabling own entry: {ownNum:X04}"); if (mustMove) { BootOrderInts.RemoveAt(ownPos); } BootOrderInts.Insert(msPos < 0 ? 0 : msPos, ownNum); BootOrder.Data = BootOrderInts.SelectMany(num => new byte[] { (byte)(num & 0xff), (byte)(num >> 8) }).ToArray(); Efi.SetVariable(BootOrder, dryRun); } } /** * Log the boot entries. */ public void LogEntries() { Setup.Log($"LogEntries: {BootOrder}"); Setup.Log($"LogEntries: {BootCurrent}"); // Windows can't enumerate EFI variables, and trying them all is too slow. // BootOrder + BootCurrent + the first 0xff entries should be enough. var seen = new HashSet(); foreach (var num in BootOrderInts.Concat(BootCurrentInts).Concat(Enumerable.Range(0, 0xff).Select(i => (UInt16) i))) { if (seen.Contains(num)) { continue; } seen.Add(num); var (v, e) = GetEntry(num); if (e != null) { Setup.Log($"LogEntries: {v}"); } } } }