Reboot to another OS by setting firmware variables

Post your working scripts, libraries and tools.
lexikos
Posts: 9560
Joined: 30 Sep 2013, 04:07
Contact:

Reboot to another OS by setting firmware variables

Post by lexikos » 24 Jun 2022, 21:05

For example: reboot automatically from Windows 11 to Windows 10 on a dual boot system, without going through a boot menu.

This script is intended to be invoked via task scheduler, passing the name of a UEFI boot entry as a parameter. The name is used to look up the index of the boot entry, which is then stored in the BootNext UEFI variable. If successful, the script calls Shutdown 2 to reboot the system. Upon next boot, the firmware reads (and resets) this variable and uses its value to select a boot entry one time. In other words, it boots from a specific boot option (typically an OS or device) that might not be the usual default.

Requirements:
  • The system must support UEFI.
  • The OS you want to boot must be registered as a boot option in UEFI, with a known/unique descriptive string.
  • Run as administrator and pass the boot entry description as a command line parameter.

Code: Select all

#Requires AutoHotkey v2.0-beta.4

Fail() => ExitApp(A_LastError)

if A_Args.Length < 1
    ExitApp 87
target := A_Args[1]

if !DllCall("OpenProcessToken", "ptr", DllCall("GetCurrentProcess", "ptr")
    , "uint", 0x20 ; TOKEN_ADJUST_PRIVILEGES
    , "ptr*", &hToken:=0)
    Fail

privs := Buffer(16, 0)
if !DllCall("advapi32\LookupPrivilegeValue", "ptr", 0
    , "str", "SeSystemEnvironmentPrivilege"
    , "ptr", privs.ptr + 4) ; &privs.Privileges[0].Luid
    Fail

NumPut('uint', 1, privs)
NumPut('uint', 2, privs, 12) ; SE_PRIVILEGE_ENABLED
DllCall("advapi32\AdjustTokenPrivileges", "ptr", hToken, "int", false
    , "ptr", privs, "uint", privs.size, "ptr", 0, "ptr", 0)
if A_LastError
    Fail

DllCall("CloseHandle", "ptr", hToken)

buf := Buffer(512) ; arbitrary size
while DllCall("GetFirmwareEnvironmentVariable"
        , "str", Format("Boot{:04x}", A_Index-1)
        , "str", "{8be4df61-93ca-11d2-aa0d-00e098032b8c}" ; EFI_GLOBAL_VARIABLE
        , "ptr", buf
        , "uint", buf.size) {
    if StrGet(buf.ptr + 6) = target {
        if !DllCall("SetFirmwareEnvironmentVariable"
            , "str", "BootNext"
            , "str", "{8be4df61-93ca-11d2-aa0d-00e098032b8c}"
            , "ushort*", A_Index-1
            , "uint", 2)
            Fail
        Shutdown 2
        ExitApp 0
    }
}

ExitApp 2
Example command line:

Code: Select all

"C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" "X:\Example\RebootTo.ahk" "Windows 10"
Notes:
  • Windows tends to create boot entries named "Windows Boot Manager"; this version of the script currently can't differentiate duplicate entries and will just select the first one it sees. In my case, the firmware supports manually adding and renaming boot entries, so I have entries named "Windows 10" and "Windows 11".
  • Any boot option present in the firmware should work; I have tested rebooting into Linux Mint.
  • Errors are reported by exit code, since it is intended to be executed via task scheduler.
  • Shutdown 2 can be removed if you just want to make a selection for the next boot, without rebooting right now.
This script displays a list of available boot options:

Code: Select all

#Requires AutoHotkey v2.0-beta.4

if !A_IsAdmin {
    if MsgBox("This script must be run as admin. Run as admin now?",, "y/n") = "no"
        ExitApp
    Run '*RunAs "' A_AhkPath '" "' A_ScriptFullPath '"'
    ExitApp
}

if !DllCall("OpenProcessToken", "ptr", DllCall("GetCurrentProcess", "ptr")
    , "uint", 0x28 ; TOKEN_QUERY | TOKEN_ADJUST_PRIVILEGES
    , "ptr*", &hToken:=0)
    throw OSError()

privs := Buffer(16, 0)
NumPut('uint', 1, privs)
if !DllCall("advapi32\LookupPrivilegeValue", "ptr", 0, "str", "SeSystemEnvironmentPrivilege"
    , "ptr", privs.ptr + 4) ; &privs.Privileges[0].Luid
    throw OSError()

NumPut('uint', SE_PRIVILEGE_ENABLED := 2, privs, 12)
if !DllCall("advapi32\AdjustTokenPrivileges", "ptr", hToken, "int", false, "ptr", privs, "uint", privs.size, "ptr", 0, "ptr", 0)
    || A_LastError
    throw OSError()

DllCall("CloseHandle", "ptr", hToken)

order := Buffer(64) ; arbitrary - 64 allows 32 boot entries
n := DllCall("GetFirmwareEnvironmentVariable"
    , "str", "BootOrder"
    , "str", "{8be4df61-93ca-11d2-aa0d-00e098032b8c}"
    , "ptr", order
    , "uint", order.size)

boots := ""
buf := Buffer(512) ; arbitrary size
Loop n//2 {
    if DllCall("GetFirmwareEnvironmentVariable"
        , "str", Format("Boot{:04x}", NumGet(order, (A_Index-1)*2, "ushort"))
        , "str", "{8be4df61-93ca-11d2-aa0d-00e098032b8c}"
        , "ptr", buf
        , "uint", buf.size)
        boots .= StrGet(buf.ptr + 6) "`n"
}
MsgBox boots
If someone wants to modify the script to work with duplicate entries, this may be helpful: the BootCurrent EFI variable contains the index of the current boot entry.

Code: Select all

; (prerequisite: run as admin and enable SeSystemEnvironmentPrivilege)
DllCall("GetFirmwareEnvironmentVariable"
    , "str", "BootCurrent"
    , "str", "{8be4df61-93ca-11d2-aa0d-00e098032b8c}"
    , "ushort*", &current:=0
    , "uint", 2)

Return to “Scripts and Functions (v2)”