Skip to content

Instantly share code, notes, and snippets.

@Darkhogg
Last active March 18, 2024 10:04
Show Gist options
  • Star 63 You must be signed in to star a gist
  • Fork 6 You must be signed in to fork a gist
  • Save Darkhogg/82a651f40f835196df3b1bd1362f5b8c to your computer and use it in GitHub Desktop.
Save Darkhogg/82a651f40f835196df3b1bd1362f5b8c to your computer and use it in GitHub Desktop.
"Reboot to {OS}" scripts for rEFInd Next Boot selection

Reboot to {OS}

This a collection of notes and files used in my quest to create "Reboot to Windows" and "Reboot to Linux" scripts (and desktop shortcuts) for Linux and Windows respectively that automatically reboot my system and instruct rEFInd to auto-select the appropriate OS entry.

General Information

The key for achieving this is to modify the EFI Variable PreviousBoot with GUID 36d08fa7-cf0b-42f5-8f14-68df73ed3740, which rEFInd uses to store the last entry selected in the menu and, if using the + default entry, will be used to select the default OS. By doing this, we trick rEFInd into booting the OS we choose without having to be physically there to press the keyboard.

This variable seems to use the following format:

  • 4 bytes, 07 00 00 00 (although Windows ignores this)
  • The text string of the entry, in UTF-16 Little Endian (no BOM)
  • 4 bytes, 20 00 00 00 (effectively: a space and a NUL character)

The variable doesn't need to contain the full text of the entry, either: Any substring will match. I don't know what rEFInd does in case of multiple matches; I believe it stops after the first. It's up to you to put everything in there or just a substring.

Select Next Boot OS from Linux

Linux exposes all EFI variables via efivarfs in the directory /sys/firmware/efi/efivars/, with file names {NAME}-{GUID}. Specifically, the relevant variable is at /sys/firmware/efi/efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740. These files contain the value of the variable in NVRAM and can be modified (by root only). Most of them will have the immutable flag set, to prevent errors, so you must call chattr -i /path/to/efivar before attempting to modify them.

This is enough to edit the default rEFInd entry: Write to the efivar file with the format specified in the previous point and the name of the entry you want selected and you're done. See the linux.refind-next-boot.py file below for a ready to use script that will set the value to this variable to its first command line argument with the appropriate format.

If you place that script (renamed to refind-next-boot) in your $PATH and give it the appropriate file permissions, you can just run:

sudo refind-next-boot 'Microsoft'
systemctl reboot

Those two commands can be conviniently placed in a script or desktop launcher so that you can reboot to Windows directly. You might want to add yourself to the sudoers file so that you can run that command with no password, in wich case remember to adequately secure the script: Set root as its owner and group and set permissions to 0755 or more restrictive.

And this is it. That was the easy part.

Select Next Boot OS from Windows

Ok, this is where it gets tricky. Windows has no way of giving you access to the EFI variables other than using the Windows API, specifically via GetFirmwareEnvironmentVariable/SetFirmwareEnvironmentVariable. These functions bot receive the name of the variable, its GUID surrounded by curly braces, a buffer to read/write from/to, respectively, and the length of the buffer or the data.

To call those two functions, the running process needs elevated privileges and a modification to the user access token, which aparently is a thing in Windows. All of this is only available via the Windows API, of course, so you'll need to write some C/C++ code.

Below is a script program that works essentially like the python script but for Windows. It needs to be compiled, which I painfully did using Visual Studio, a experience I wouldn't want to repeat. It works the same: Just call it with the name of the entry you want to boot or a substring of that set. Afterwards, you are free to power off or shut down your system using whatever method and rEFInd will just select the correct entry.

Of course, Windows being Windows, creating a desktop shortcut that has an icon and is just double-click-and-forget is a bit more tricky than the Linux equivalent. First, you'll need to place the compiled program someplace and set it to run as administrator (right click, Propertied, Compatibility, check Run as administrator). After that, in that same folder, create a .bat file that calls our program and restarts:

refind-next-boot "linux"
shutdown -t 0 -r

Now create a shortcut to that .bat file, place it in your desktop, give it a proper icon and name and voilà, a "Reboot to Linux" button! It will bother you with a few console windows and a UAC dialog, yes, but it's better than nothing.

#!/usr/bin/env python3
import subprocess
import sys
EFIVAR_NAME = 'PreviousBoot'
EFIVAR_GUID = '36d08fa7-cf0b-42f5-8f14-68df73ed3740'
EFIVAR_PREFIX = '/sys/firmware/efi/efivars'
PREFIX = b'\x07\x00\x00\x00'
SUFFIX = b'\x20\x00\x00\x00'
if len(sys.argv) != 2:
print('error: must pass exactly one argument', file=sys.stderr)
sys.exit(1)
text = sys.argv[1]
filename = '{}/{}-{}'.format(EFIVAR_PREFIX, EFIVAR_NAME, EFIVAR_GUID)
retcode = subprocess.call(['chattr', '-i', filename])
if retcode != 0:
sys.exit(42 + retcode)
with open(filename, 'wb') as f:
content = PREFIX + bytes(text, 'utf-16-le') + SUFFIX
f.write(content)
#include <windows.h>
#include <strsafe.h>
#include <iostream>
const LPCTSTR STR_VARNAME = L"PreviousBoot";
const LPCTSTR STR_VARGUID = L"{36d08fa7-cf0b-42f5-8f14-68df73ed3740}";
void ErrorExit() {
DWORD error = GetLastError();
LPTSTR errorText = nullptr;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &errorText, 0, nullptr);
std::wcerr << L"Error " << error << L": " << errorText << std::endl;
LocalFree(errorText);
ExitProcess(error);
}
int main(int argc, char** argv) {
if (argc != 2) {
std::wcerr << L"Error: Must have exactly one command line argument" << std::endl;
ExitProcess(1);
}
/* get the privileges necessary */
HANDLE hToken;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken)) {
ErrorExit();
}
TOKEN_PRIVILEGES tkp;
LookupPrivilegeValue(nullptr, SE_SYSTEM_ENVIRONMENT_NAME, &tkp.Privileges[0].Luid);
tkp.PrivilegeCount = 1;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, nullptr, 0);
if (GetLastError() != ERROR_SUCCESS) {
ErrorExit();
}
/* construct the efivar content */
char* sStr = argv[1];
DWORD nStrSize = strlen(sStr);
DWORD nVarSize = 4 + (2 * nStrSize);
BYTE* lpVarData = (BYTE*)LocalAlloc(LPTR, nVarSize);
lpVarData[nVarSize - 4] = 0x20;
for (DWORD i = 0; i < nStrSize; i++) {
lpVarData[(2 * i)] = sStr[i];
lpVarData[1 + (2 * i)] = 0x00;
}
/* write the efivar contents to the efivar */
DWORD dwSetResult = SetFirmwareEnvironmentVariable(STR_VARNAME, STR_VARGUID, lpVarData, nVarSize);
if (!dwSetResult) {
ErrorExit();
}
}
@skannerdarkly
Copy link

It worked for me! Thanks for the great codes. I was able to compile without much of an issue after downloading Visual Studio community (for free). Created a simple .bat file:
refind-next-boot.exe "vmlinux"
(sudo is not a windows command) and I was off and running.

@ashehata
Copy link

Thanks! I was able to get this to work by just using "printf "<HEX_BYTES>" > " and it works perfectly. Now my question is do you think it's possible to also set a temporary timeout of 0 for that next OS since we already know we want it?

@jamezrin
Copy link

jamezrin commented Jan 5, 2019

Instead of using modifying the efivar it's possible to set use_nvram to false in the refind configuration and the previous boot will be stored in vars/PreviousBoot. I'm not sure if it's better to make refind use the efivars or not, but that's another alternative.

By the way, thanks for this guide, you did an awesome job.

@Darkhogg
Copy link
Author

Darkhogg commented Mar 5, 2019

@skannerdarkly glad to hear that! I did the same plus a few indirections to avoid the UAC dialog, creating a service and a few other things.
@ashehata I don't think so, unless you edit the refind config when running this AND after starting the system, and I wish you the best of luck modifying the ESP from Windows... Or maybe contribute to rEFInd, I don't think it would be very hard to do, but their code is in SourceForge and a bit messy, so yeah...
@jamezrin OH, I didn't know that, I will investigate. Is it a new feature? I don't recall reading about that when I did this...

@cyperous
Copy link

cyperous commented Apr 2, 2019

I can't get this to work in Windows 10, I tried first compiling in linux with the following command.

x86_64-w64-mingw32-g++ -municode -static -o refind-next-boot.exe refind-next-boot.cpp but I had to change main to wmain

It compiled without issue but after getting no errors when running rewind-next-boot "ubuntu" I restarted only to find out that I was back in windows like the program did nothing.

I then tried installing VS on a different machine and changing the character set to Unicode and this required no changes to the code. copy the compiled exe to the machine I was using and still nothing (restarted back into windows).

I even went a step further and made a binary copy of the efivars from linux and modified the code in windows to actully write that same binary file to the efivars using this program and even verified the value using GetFirmwareEnvironmentVariable and same thing restarted into Windows again. Right now the only way to get back to Ubuntu is to have direct access to the machine :-( sad face.

If anyone has any ideas why this won't work I'm up for suggestions at this point I've spent to much time screwing around with this. I'm giving up.

@cyperous
Copy link

cyperous commented Apr 3, 2019

I found a different solutions that doesn't involve using nvram and setting environment and uses the refind configuration file.

I added a file called default.conf to the EFIs refind folder and included the file in refind.conf

then I wrote an app that modifies the default.conf file to set the default boot selection.

Here is the link to the GitHub project refind-next-boot

@luckysoul777
Copy link

luckysoul777 commented Aug 3, 2019

I stumbled on this thread while I was looking for an rEFInd next boot only solution. As a non-developer, any solution that involves compiling a code is beyond my capability. Now, I have come up with a work-around and decided to share the idea in case another noob like me is still searching for a solution.

In the refind.conf file, if one uses the following statement, he can boot to the OS of his choice within the specified time.

default_selection OS_of_your_choice current_time current_time_plus_one_minute

For example,
default_selection Windows 06:00 06:01
will boot Windows between 6 and 6:01AM.
By limiting this window to only 1 minute before I reboot, I basically achieve the objective of next boot only!

Don't forget to insert
default_selection your_default_OS
BEFORE the previous statement so that when the time windowed statement is invalid, rEFInd knows what to do than defaulting to last OS booted. The time windowed statement must be the LAST statement, or it will not execute even if you are within the valid time frame.

This is a work-around and not a solution. For example, you cannot reboot from Windows to Windows. After you reboot, it goes back to your default OS. If you want to boot to Windows again, you would need to adjust the time frame in the default_selection statement.
As inelegant as it is, it does offer you the capability to boot to Windows remotely when you need to and most importantly not get stuck in Windows when you need to switch to other OS but you don't have easy access to ESP and you are not sitting in front of the machine to click another OS!

A minor inconvenience is if rebooting at exact same time again tomorrow, it will boot to Windows regardless of it is my intention or not. However, since there are 1440 minutes a day, the probability for this to happen is very low. Personally I leave it alone. If I every get anal retentive, I will just write a batch file that would execute at boot time to restore a refind.conf template file to rid of this windowed statement.

Hope this helps those who don't know how to compile a code but need to reboot his dual-boot machine remotely from time to time.

@meatcar
Copy link

meatcar commented Sep 30, 2019

Great idea! I re-wrote the C++ bit as a C#/PowerShell script to avoid the compilation step. You can find the gist here. It should help some of the issues people are encountering, @Darkhogg feel free to pull it into this gist, modifiy it, etc.

@Darkhogg
Copy link
Author

@meatcar Hmm... That's mostly C#, isn't it? Could that be compiled into an executable without the PowerShell bit, and without using Visual Studio, maybe?

@meatcar
Copy link

meatcar commented Oct 1, 2019

@Darkhogg Right, the idea is that it doesn't need the compilation step, and can be interpreted as-is by .NET CLR magic. That way the semantics stay the same as wiith the python script (Just Run It). Less friction for people who don't have Visual Studio set up (me) 😄. That said, for readability, and if you really want to compile it, I updated it to have the C# bit out as a standalone file. I think you'd need to add a main method to have it run standalone.

@michaeldillabough
Copy link

Hi Darkhogg, Your script here and meatcar's rewrite are looking like the perfect solution for me. One question I have is
Does this only work for a Windows 10 / Ubunto setup or is it agnostic of the operating system? For example I will be dual booting two Windows 10 images.

Thanks!

@Darkhogg
Copy link
Author

@michaeldillabough It should be agnostic to the target OS, yes, as long as the string you use to select it matches with what refind expects to see.

@johnmarshall515
Copy link

Any way to get working in mac to boot into windows and on windows to boot into mac? Cannot use bootcamp utility.

@Darkhogg
Copy link
Author

@johnmarshall515 From Windows to Mac you can use the same method outlined here but changing the target string to one that matches. I have no idea how to set this up on a Mac, my life is completely Apple-free.

@johnmarshall515
Copy link

Ok thank you

@albertvaka
Copy link

To compile it using the command line on Windows (instead of creating a VS project), open the "Developer Command Prompt for VS" then type:

cl /DUNICODE  /D_UNICODE windows.refind-next-boot.cpp Advapi32.lib

@albertvaka
Copy link

Also thanks a lot @Darkhogg for writing this, you are amazing!

@diegodorado
Copy link

diegodorado commented Sep 21, 2020

This is great!
I have ported the python script to bash. This way I can add it to my sudoers binaries and I can reboot without typing my password.
I use this to bind it to a command in i3
Here is the bash script

#!/bin/bash
chattr -i /sys/firmware/efi/efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740
echo -ne "\x07\x00Microsoft\x20\x00" | iconv -f UTF-8 -t UTF-16LE > /sys/firmware/efi/efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740
systemctl reboot

Then add this script to your sudoers.d directory with sudo visudo /etc/sudoers.d/reboot (or wathever name you want) for passwordless sudo execution (you still have to execute with sudo, but you won't get a password prompt )

youruser ALL=(ALL) NOPASSWD: /path/to/reboot-windows-bash-script

@evan0621
Copy link

evan0621 commented Apr 8, 2021

chattr: No such file or directory while trying to stat /sys/firmware/efi/efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740

I got this from Ubuntu 18.04

@Darkhogg
Copy link
Author

Darkhogg commented Apr 9, 2021

@evan0621 Are you using rEFInd? Ubuntu installs GRUB2 if I'm not mistaken, in which case this solution won't help you at all as it targets rEFInd specifically.

@Hizoka76
Copy link

I guess it's simpler with the command efibootmgr:

# Next reboot on Windows
BootNumber=$(efibootmgr | sed -n "/Windows/ s/Boot\(....\).*/\1/p")

# If the number was finded
if [[ ${BootNumber} ]]
then
    # Update the next reboot and get the return of BootNext
    NextBoot=$(sudo efibootmgr -n ${BootNumber} | sed -n "/BootNext/ s/.* //p")

    # If the BootNext is OK
    if [[ ${BootNumber} != ${NextBoot} ]]
    then
        sudo reboot
        # Or better for KDE : qdbus org.kde.ksmserver /KSMServer org.kde.KSMServerInterface.logout 0 1 1

    # If the BootNext isn't OK
    else
        echo "An error has occurred, the next boot will not be ${BootNumber} !${RAZ}"
    fi
fi

@stefan-golinschi
Copy link

...and similar. I dont know C++ but it would be of great use if I was able to get it working.

To compile this in Windows 10, I used the workaround from here: https://stackoverflow.com/questions/13977388, that is to replace this line:
#include <Windows.h>
with this one:

#ifndef UNICODE
#define UNICODE
#define UNICODE_WAS_UNDEFINED
#endif

#include <Windows.h>

#ifdef UNICODE_WAS_UNDEFINED
#undef UNICODE
#endif

This workaround is great when you are not using an IDE, and compiling from CLI (I'm using minGW via mSYS2).

@CtrlValCanc
Copy link

CtrlValCanc commented Jun 22, 2023

Is it possible that in EndeavourOs I don't have this entry?
/sys/firmware/efi/efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740

Also, I compiled the Windows one like that: g++ -o output.exe yourfile.cpp, I did the bat but it's not working, it doesn't say anything when I click the bat but it reboots normally to Windows.
I just thought that in Refind I don't choose EndeavourOS, but I choose systemd. What should I do?

@birgersp
Copy link

birgersp commented Jan 13, 2024

Is it possible that in EndeavourOs I don't have this entry? /sys/firmware/efi/efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740

Also, I compiled the Windows one like that: g++ -o output.exe yourfile.cpp, I did the bat but it's not working, it doesn't say anything when I click the bat but it reboots normally to Windows. I just thought that in Refind I don't choose EndeavourOS, but I choose systemd. What should I do?

Pop Os (22.04) user here, I don't have any files prefixed with "PreviousBoot" in /efi/sys/firmware/efivars

EDIT: For my desire to easily boot into windows from Pop Os, I implemented a somewhat different solution. I added a script named "reboot-to-windows" that will set the "defaultion_selection" in the refind config to "Microsoft" and then do a reboot. Additionally, I added a crontab that undoes the setting, so if I interrup the refind boot menu to boot back into Pop Os, the default entry on the next boot will be "previous" boot, which is Pop Os. Somewhat hacky but works quite well.

The script to set the set and un-set "default_selection Microsoft" in the refind config is uploaded here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment