Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save graingert/38d834a24a760d664b3f903ed48d6dca to your computer and use it in GitHub Desktop.

Select an option

Save graingert/38d834a24a760d664b3f903ed48d6dca to your computer and use it in GitHub Desktop.
GRUB → Windows reboot via EFI BootNext (BitLocker-safe)

GRUB → Windows reboot via EFI BootNext (BitLocker-safe)

Moved

To https://github.com/graingert/efibootnext

Boot Windows from a GRUB menu entry without breaking BitLocker TPM measurements.

Problem

Chainloading \EFI\Microsoft\Boot\bootmgfw.efi from GRUB breaks TPM PCR measurements, causing BitLocker to prompt for a recovery key every time.

Solution

Instead of chainloading, boot a minimal Linux kernel/initramfs that:

  1. Reads a bootnext= parameter from the kernel command line
  2. Mounts efivarfs and calls efibootmgr --bootnext to set the UEFI BootNext variable
  3. Immediately reboots

The firmware then boots Windows natively with correct TPM state. BitLocker is happy.

The premount script runs before the LUKS prompt, so you never have to enter your Linux disk encryption password just to boot Windows.

The GRUB menu entry is auto-generated by 50_efibootnext during update-grub — it detects Windows via os-prober, matches the EFI path to a boot entry via efibootmgr, and finds the /boot partition via GRUB's prepare_grub_to_access_device, so no manual configuration is needed.

Files

File Install to
efibootnext-hook /etc/initramfs-tools/hooks/efibootnext
efibootnext-premount /etc/initramfs-tools/scripts/init-premount/efibootnext
50_efibootnext /etc/grub.d/50_efibootnext

Install

sudo apt install efibootmgr os-prober
sudo make install
sudo update-initramfs -u
sudo update-grub

To uninstall:

sudo make uninstall
sudo update-initramfs -u
sudo update-grub

Verify

Check efibootmgr is in the initramfs:

lsinitramfs /boot/initrd.img | grep efibootmgr

Check the GRUB menu entry exists:

grep -E "^menuentry|^submenu" /boot/grub/grub.cfg

How it works

update-grub
 → 50_efibootnext runs
   → detects Windows via os-prober
   → finds matching EFI boot entry (e.g. Boot0007) via efibootmgr
   → finds /boot device via GRUB's prepare_grub_to_access_device
   → generates menuentry with bootnext=0007

GRUB
 → boots linux kernel + initramfs with bootnext=XXXX
   → init-premount/efibootnext runs (before LUKS prompt)
     → mounts efivarfs
     → efibootmgr --bootnext XXXX
     → reboot -f
       → firmware reads BootNext, boots Windows natively
         → TPM PCRs are correct, BitLocker unlocks automatically

Duplicate GRUB entries

30_os-prober also detects Windows and creates a chainload menu entry. You will see two Windows entries in GRUB: one from 30_os-prober (chainload, breaks BitLocker) and one from 50_efibootnext (BootNext, BitLocker-safe). Use the "(EFI BootNext)" entry. To remove the duplicate, either set GRUB_DISABLE_OS_PROBER=true in /etc/default/grub (disables both scripts) or add the Windows partition UUID to GRUB_OS_PROBER_SKIP_LIST (both scripts respect this).

Design notes

The premount script parses /proc/cmdline using the same for x in $(cat /proc/cmdline) + case pattern used by initramfs-tools itself in /usr/share/initramfs-tools/init:

# shellcheck disable=SC2013
for x in $(cat /proc/cmdline); do
	case $x in
	root=*)
		ROOT=${x#root=}
		;;
	...
	esac
done

This word-splits on spaces, which is fine because bootnext=XXXX never contains spaces.

Requirements

  • Ubuntu with GRUB2 and EFI
  • efibootmgr, os-prober, and initramfs-tools packages
  • busybox in the initramfs (provides reboot, mount, mountpoint — included by default on Ubuntu)
  • UEFI firmware (not legacy BIOS)

License

Public domain / CC0

#!/bin/sh
set -e
prefix="/usr"
exec_prefix="/usr"
datarootdir="/usr/share"
. "$pkgdatadir/grub-mkconfig_lib"
export TEXTDOMAIN=grub
export TEXTDOMAINDIR="${datarootdir}/locale"
if ! command -v efibootmgr > /dev/null 2>&1; then
echo "efibootnext: efibootmgr not found, skipping" >&2
exit 0
fi
if ! command -v os-prober > /dev/null 2>&1; then
echo "efibootnext: os-prober not found, skipping" >&2
exit 0
fi
if [ "x${GRUB_DISABLE_OS_PROBER}" = "xtrue" ]; then
echo "efibootnext: GRUB_DISABLE_OS_PROBER is true, skipping" >&2
exit 0
fi
if ! EFIBOOTMGR_OUTPUT="$(efibootmgr -v)"; then
echo "efibootnext: efibootmgr -v failed, skipping" >&2
exit 0
fi
OSPROBED="$(os-prober | tr ' ' '^' | paste -s -d ' ')"
if [ -z "${OSPROBED}" ]; then
echo "efibootnext: os-prober found no other OS, skipping" >&2
exit 0
fi
for OS in ${OSPROBED}; do
DEVICE="$(echo "${OS}" | cut -d ':' -f 1)"
LONGNAME="$(echo "${OS}" | cut -d ':' -f 2 | tr '^' ' ')"
LABEL="$(echo "${OS}" | cut -d ':' -f 3 | tr '^' ' ')"
BOOT="$(echo "${OS}" | cut -d ':' -f 4)"
case ${BOOT} in
efi) ;;
*) continue ;;
esac
if UUID="$(${grub_probe} --target=fs_uuid --device "${DEVICE%@*}")"; then
EXPUUID="$UUID"
if [ x"${DEVICE#*@}" != x ]; then
EXPUUID="${EXPUUID}@${DEVICE#*@}"
fi
if [ "x${GRUB_OS_PROBER_SKIP_LIST}" != "x" ] && [ "x$(echo "${GRUB_OS_PROBER_SKIP_LIST}" | grep -i -e '\b'"${EXPUUID}"'\b')" != "x" ]; then
echo "efibootnext: skipped ${LONGNAME} on ${DEVICE} by user request" >&2
continue
fi
fi
EFIPATH="${DEVICE#*@}"
# Convert forward slashes to backslashes to match efibootmgr output
EFIPATH_BS="$(echo "${EFIPATH}" | tr '/' '\\')"
BOOTNEXT=""
while IFS= read -r line; do
case "$line" in
Boot[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]\**)
case "$line" in
*"File(${EFIPATH_BS})"*)
BOOTNEXT="${line#Boot}"
BOOTNEXT="${BOOTNEXT%%\**}"
break
;;
esac
;;
esac
done <<EFIEOF
${EFIBOOTMGR_OUTPUT}
EFIEOF
if [ -z "$BOOTNEXT" ]; then
echo "efibootnext: no EFI boot entry found for ${EFIPATH}, skipping" >&2
continue
fi
echo "Adding ${LONGNAME} (EFI BootNext $BOOTNEXT) entry" >&2
cat << MENUEOF
menuentry '$(echo "${LONGNAME} (EFI BootNext)" | grub_quote)' --class $(echo "${LABEL}" | LC_ALL=C sed 's,[[:digit:]]*$,,' | cut -d' ' -f1 | tr 'A-Z' 'a-z' | LC_ALL=C sed 's,[^[:alnum:]_],_,g') --class os \$menuentry_id_option 'efibootnext-$BOOTNEXT' {
MENUEOF
prepare_grub_to_access_device ${GRUB_DEVICE_BOOT} | grub_add_tab
cat << MENUEOF
linux /vmlinuz bootnext=$BOOTNEXT
initrd /initrd.img
}
MENUEOF
done
#!/bin/sh
set -e
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in prereqs) prereqs; exit 0;; esac
. /usr/share/initramfs-tools/hook-functions
[ "${verbose}" = "y" ] && echo "I: efibootnext: copying efibootmgr"
copy_exec "$(command -v efibootmgr)"
#!/bin/sh
set -e
PREREQ=""
prereqs() { echo "$PREREQ"; }
case "$1" in prereqs) prereqs; exit 0;; esac
BOOTNEXT=""
for param in $(cat /proc/cmdline); do
case "$param" in
bootnext=*) BOOTNEXT="${param#bootnext=}" ;;
esac
done
[ -n "$BOOTNEXT" ] || exit 0
case "$BOOTNEXT" in
[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) ;;
*) echo "efibootnext: invalid bootnext value: $BOOTNEXT" >&2; exit 1 ;;
esac
if ! mountpoint -q /sys/firmware/efi/efivars; then
mount -t efivarfs efivarfs /sys/firmware/efi/efivars
fi
if ! efibootmgr --bootnext "$BOOTNEXT"; then
echo "efibootnext: failed to set BootNext to $BOOTNEXT, continuing normal boot" >&2
exit 0
fi
reboot -f
PREFIX ?= /etc
GRUB_DIR = $(PREFIX)/grub.d
INITRAMFS_HOOKS_DIR = $(PREFIX)/initramfs-tools/hooks
INITRAMFS_SCRIPTS_DIR = $(PREFIX)/initramfs-tools/scripts/init-premount
install:
install -m 755 50_efibootnext $(DESTDIR)$(GRUB_DIR)/50_efibootnext
install -m 755 efibootnext-hook $(DESTDIR)$(INITRAMFS_HOOKS_DIR)/efibootnext
install -m 755 efibootnext-premount $(DESTDIR)$(INITRAMFS_SCRIPTS_DIR)/efibootnext
uninstall:
rm -f $(DESTDIR)$(GRUB_DIR)/50_efibootnext
rm -f $(DESTDIR)$(INITRAMFS_HOOKS_DIR)/efibootnext
rm -f $(DESTDIR)$(INITRAMFS_SCRIPTS_DIR)/efibootnext
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment