Merge ~arraybolt3/grub:ubuntu into ~ubuntu-uefi-team/grub/+git/ubuntu:ubuntu

Proposed by Aaron Rainbolt
Status: Needs review
Proposed branch: ~arraybolt3/grub:ubuntu
Merge into: ~ubuntu-uefi-team/grub/+git/ubuntu:ubuntu
Diff against target: 318 lines (+254/-3)
7 files modified
debian/control (+1/-0)
debian/grub-common-run (+33/-0)
debian/grub-common.install.in (+1/-0)
debian/grub-common.service (+1/-3)
debian/patches/enable-grubenv-on-esp.patch (+214/-0)
debian/patches/series (+1/-0)
debian/update-grub (+3/-0)
Reviewer Review Type Date Requested Status
Julian Andres Klode Needs Fixing
Steve Langasek Abstain
Mate Kukri Pending
Review via email: mp+462445@code.launchpad.net

This proposal supersedes a proposal from 2024-03-14.

Description of the change

This patch enables the GRUB environment block to be used even if the /boot partition's filesystem cannot be written to by GRUB.

When /boot is a filesystem that GRUB cannot write to (such as BTRFS) on an EFI system, the current behavior is to disable recordfail support entirely and always show the boot menu. This is a non-ideal situation for some users. This patch works around the issue by detecting when /boot cannot be written to by GRUB and the system is EFI-based, and if both are true it saves and loads the environment block from the EFI System Partition if this is the case.

The way this is done is by detecting unwritable /boot and the use of EFI at grub.cfg generation time. If the conditions are met, grub.cfg is adjusted to search for the FS that is mounted at /boot/efi at boot time. It then saves and loads environment variables to and from (efi filesystem)/EFI/ubuntu/grubenv. Additionally, the recordfail handling in grub-common.service is adjusted to do the same check and adjust the correct grubenv file. Finally, update-grub has been adjusted to always wipe and recreate the grubenv file (in the correct location) so as to cope with changing bootloaders (an edge case described below).

How this code handles some edge cases:

* Multiple EFI System Partitions with RAID - Only the filesystem used by /boot/efi is ever used for grubenv storage. GRUB and grub-common.service should always find and handle the correct file.
* Multiple Ubuntu versions and/or flavors on the same system - This will result in some versions of Ubuntu occasionally "taking control" of the EFI system partition during GRUB bootloader upgrades. In the event the format of grubenv changes between versions of GRUB, this could theoretically lead to boot failure after a GRUB update. To work around this, on every update-grub invocation, the grubenv file is deleted and a blank one generated. Assuming update-grub is called when GRUB is updated (**which I did not verify was the case!!!**), this will ensure that grubenv is always compatible with the GRUB installed at EFI/ubuntu.
* User wants to have their own grubenv and/or grub.cfg - The user can modify EFI/ubuntu/grub.cfg to point to their own custom grub.cfg, which can point at their desired custom grubenv. This code should not override that as it only changes the generation of /boot/grub/grub.cfg, not EFI/ubuntu/grub.cfg.
* GRUB is installed to EFI/BOOT, not EFI/ubuntu - The EFI/ubuntu directory is automatically created by update-grub and grub-common.serivce if needed. It does not require GRUB to be installed there to work.
* Multiple operating systems other than Ubuntu are installed - grubenv is saved to EFI/ubuntu/grubenv, and should not affect any non-Ubuntu flavors except for Ubuntu derivatives (which presumably will also be using this change).
* EFI installation is converted to a legacy BIOS installation - the EFI system partition is only modified to include grubenv if the system if EFI-based. Even if /boot is unwritable by GRUB, grubenv will still be located at /boot/grub/grubenv on a BIOS system, rather than /boot/efi/EFI/ubuntu/grubenv.
* Legacy BIOS installation is converted to an EFI installation - See above - the logic will start reading and writing grubenv to/from /boot/efi/EFI/ubuntu/grubenv if /boot is unwritable and the system becomes EFI-based.

To post a comment you must log in.
Revision history for this message
Aaron Rainbolt (arraybolt3) wrote : Posted in a previous version of this proposal

Shoot, I proposed this against the wrong branch.

Revision history for this message
Aaron Rainbolt (arraybolt3) wrote :

There, fixed.

Revision history for this message
Mate Kukri (mkukri) wrote :

Would it hurt if we always put grubenv on ESP when running on EFI? I think the less magic shell scripts and the less special cases, the better off we are.

I am not sure if I am comfortable with doing this post-FF in Noble. I think we need at least an FF-exception? Or maybe should we do this early in the 24.10 cycle instead?

Revision history for this message
Aaron Rainbolt (arraybolt3) wrote :

My reasoning for wanting it in 24.04 isn't really a great excuse, but I work for an OEM that would really like for this to work in 24.04 for implementing a system restore feature. We have a fallback for if this isn't accepted, but it's not really an ideal one. For that reason I'd be happy to file the FFe. I totally understand if this is just too late for this cycle though.

I suppose always putting grubenv on ESP when running on EFI would work too. I intentionally didn't do that as I was trying to trip as little "gotchas" that may exist as possible by tweaking the code and the behavior as little as I could manage. I'm happy to rework this to simply say "if on an EFI system, put grubenv there instead". That would be significantly less complicated code-wise, though it would affect more scenarios and therefore potentially come with a greater regression risk.

Revision history for this message
Steve Langasek (vorlon) wrote :

> Would it hurt if we always put grubenv on ESP when running on EFI?

One argument against this is that ESPs can't be RAIDed; if you have a system with multiple disks in a redundant configuration, putting the grubenv on the ESP means you then have two grubenvs, and I think if you are booting from the fallback disk there are various subtle cases where you'll regress (e.g. the 'initrdfail' handling in 00_header).

If it were possible instead for grub to provide a variable corresponding to "the ESP I booted from", this would not be an issue.

review: Needs Fixing
Revision history for this message
Aaron Rainbolt (arraybolt3) wrote :

vorlon: Implemented all suggested changes.

Revision history for this message
Steve Langasek (vorlon) wrote :

Thanks. fwiw my review was a bit of a drive-by, my real purpose was to comment on whether we should turn this on for all UEFI systems. So I'll abstain from here and let Julian/Máté take it the rest of the way.

review: Abstain
Revision history for this message
Julian Andres Klode (juliank) wrote :

I left some comments. I'm not sure we can still land this, but if we can get the patch size reduced to alternative lookup path in the loadenv command, this probably is fine.

review: Needs Fixing
Revision history for this message
Aaron Rainbolt (arraybolt3) wrote :

I should be up front - if this involves patching the C code component of the bootloader, I do not think I can do this. My C skills stink, and with a piece of software as security-critical as the bootloader, I don't trust myself to be able to modify GRUB's "kernel" without introducing the next Secure Boot-bypassing CVE on accident. That's part of the reason I did this in the shell scripts - GRUB has all the features to do this, so I tried to just leverage them.

If nothing else, hopefully this was at least an interesting proof-of-concept and we can try to get the actual implementation into 24.10. Still, I'm willing to try to make things work in an ideal way, just... get someone from the security team to grill my code if I do try to mess with the C stuff. :)

Revision history for this message
Aaron Rainbolt (arraybolt3) :
Revision history for this message
Aaron Rainbolt (arraybolt3) wrote :

OK, I think I will go ahead and at least try this, since the proposed solution sounds like theoretically it won't be too difficult to implement even in C, I can be careful, and there's some stuff I can tweak in Calamares to help make this workable.

Revision history for this message
Mate Kukri (mkukri) wrote :

@arraybolt3 I'll look at this in more detail next week (hopefully Monday), I didn't have much time for it yet.

~arraybolt3/grub:ubuntu updated
d76bcc4... by Aaron Rainbolt

Fix missing build dep, enable saving grubenv on ESP

Revision history for this message
Aaron Rainbolt (arraybolt3) wrote :

I have reworked the patch to fix all issues pointed out by juliank, including patching the loadenv.c file to work with fallbacks.

CRITICAL NOTE:
*******************************************
**I HAVE NOT TESTED THIS CODE!!! !!! !!!***
*******************************************
I'm not quite sure how to get grub-efi-amd64 to build unsigned, and have been very busy, so this is sort of a "here's where I'm at, look it over and tell me if I'm on the right path" sort of thing. I'll try and test it later today most likely.

Unmerged commits

d76bcc4... by Aaron Rainbolt

Fix missing build dep, enable saving grubenv on ESP

6dfb508... by Mate Kukri

debian/: Add tests for grub-sort-version

a748151... by Mate Kukri

d/grub-sort-version: Append `-0` to abi strings before passing to python-apt (Fixes LP: #2041827)

c699230... by Mate Kukri

d/grub-sort-version: Update regex to correctly match kernel flavour

GRUB_FLAVOUR_ORDER didn't work because the regex f"[0-9]*-{flavour}$"
never matched against the entire ABI string.

39483fe... by Mate Kukri

d/p/grub-sort-version.patch: Also patch grub-mkconfig to export GRUB_FLAVOUR_ORDER

GRUB_FLAVOUR_ORDER specified in /etc/default/grub was ignored before
this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/debian/control b/debian/control
2index f0a896b..3cc18db 100644
3--- a/debian/control
4+++ b/debian/control
5@@ -7,6 +7,7 @@ Uploaders: Felix Zielcke <fzielcke@z-51.de>, Jordi Mallach <jordi@debian.org>, S
6 Build-Depends: debhelper-compat (= 13),
7 patchutils,
8 python3,
9+ python3-apt,
10 python3-pytest,
11 flex,
12 bison,
13diff --git a/debian/grub-common-run b/debian/grub-common-run
14new file mode 100755
15index 0000000..c7631d2
16--- /dev/null
17+++ b/debian/grub-common-run
18@@ -0,0 +1,33 @@
19+#! /bin/sh
20+set -e
21+
22+if test -f /etc/default/grub; then
23+ . /etc/default/grub
24+fi
25+
26+for x in /etc/default/grub.d/*.cfg; do
27+ if [ -e "${x}" ]; then
28+ . "${x}"
29+ fi
30+done
31+
32+while getopts 'ri' opt_str; do
33+ case "${opt_str}" in
34+ r) reset_grubenv='y';;
35+ i) check_initrdless_boot_fallback='y';;
36+ esac
37+done
38+
39+grubenv_dir="echo "/boot/efi/EFI/$(echo ${GRUB_DISTRIBUTOR} | tr 'A-Z' 'a-z' | cut -d' ' -f1|LC_ALL=C sed 's,[^[:alnum:]_],_,g')")"
40+grubenv_location="$grubenv_dir/grubenv"
41+
42+[ -s "$grubenv_location" ] || rm -f "$grubenv_location"; mkdir -p "$grubenv_dir"
43+[ "$reset_grubenv" = 'y' ] && rm -f "$grubenv_location"; mkdir -p "$grubenv_dir"
44+
45+grub-editenv "$grubenv_location" unset recordfail
46+
47+if [ "$check_initrdless_boot_fallback" = 'y' ]; then
48+ if grub-editenv "$grubenv_location" list | grep -q initrdless_boot_fallback_triggered=1; then
49+ echo "grub: GRUB_FORCE_PARTUUID set, initrdless boot paniced, fallback triggered."
50+ fi
51+fi
52diff --git a/debian/grub-common.install.in b/debian/grub-common.install.in
53index d43de70..fdf6fa2 100644
54--- a/debian/grub-common.install.in
55+++ b/debian/grub-common.install.in
56@@ -4,6 +4,7 @@
57 ../../debian/grub-multi-install usr/lib/grub/
58 ../../debian/grub-sort-version usr/lib/grub
59 ../../debian/canonical-uefi-ca.crt usr/share/grub/
60+../../debian/grub-common-run usr/lib/grub/
61
62 etc/grub.d
63 usr/bin/grub-editenv
64diff --git a/debian/grub-common.service b/debian/grub-common.service
65index e7bdf4b..85011ba 100644
66--- a/debian/grub-common.service
67+++ b/debian/grub-common.service
68@@ -6,9 +6,7 @@ ConditionPathExists=/boot/grub/grub.cfg
69 [Service]
70 Type=oneshot
71 Restart=no
72-ExecStartPre=/bin/sh -c '[ -s /boot/grub/grubenv ] || rm -f /boot/grub/grubenv; mkdir -p /boot/grub'
73-ExecStart=grub-editenv /boot/grub/grubenv unset recordfail
74-ExecStartPost=/bin/sh -c 'if grub-editenv /boot/grub/grubenv list | grep -q initrdless_boot_fallback_triggered=1; then echo "grub: GRUB_FORCE_PARTUUID set, initrdless boot paniced, fallback triggered."; fi'
75+ExecStart=/usr/lib/grub/grub-common-run -i
76 StandardOutput=kmsg
77
78 [Install]
79diff --git a/debian/patches/enable-grubenv-on-esp.patch b/debian/patches/enable-grubenv-on-esp.patch
80new file mode 100644
81index 0000000..1354729
82--- /dev/null
83+++ b/debian/patches/enable-grubenv-on-esp.patch
84@@ -0,0 +1,214 @@
85+--- a/grub-core/commands/loadenv.c
86++++ b/grub-core/commands/loadenv.c
87+@@ -161,17 +161,62 @@ grub_cmd_load_env (grub_extcmd_context_t
88+ grub_file_t file;
89+ grub_envblk_t envblk;
90+ grub_env_whitelist_t whitelist;
91++ enum grub_file_type sigskip = state[1].set
92++ ? GRUB_FILE_TYPE_SKIP_SIGNATURE : GRUB_FILE_TYPE_NONE;
93+
94+ whitelist.len = argc;
95+ whitelist.list = args;
96+
97+ /* state[0] is the -f flag; state[1] is the --skip-sig flag */
98+- file = open_envblk_file ((state[0].set) ? state[0].arg : 0,
99+- GRUB_FILE_TYPE_LOADENV
100+- | (state[1].set
101+- ? GRUB_FILE_TYPE_SKIP_SIGNATURE : GRUB_FILE_TYPE_NONE));
102+- if (! file)
103+- return grub_errno;
104++ if (state[0].set)
105++ {
106++ file = open_envblk_file (state[0].arg,
107++ GRUB_FILE_TYPE_SAVEENV | sigskip);
108++ if (! file)
109++ {
110++ file = open_envblk_file (state[0].arg,
111++ GRUB_FILE_TYPE_LOADENV | sigskip);
112++ if (! file)
113++ return grub_errno;
114++ grub_env_set ("grubenv_readonly", "y");
115++ }
116++ }
117++ else
118++ {
119++ const char *envpath = 0;
120++ char *filename = 0;
121++
122++ envpath = grub_env_get ("envpath");
123++ if (envpath)
124++ {
125++ filename = grub_strdup (envpath);
126++ file = open_envblk_file (filename,
127++ GRUB_FILE_TYPE_SAVEENV | sigskip);
128++ if (! file)
129++ {
130++ file = open_envblk_file (0, GRUB_FILE_TYPE_SAVEENV | sigskip);
131++ if (! file)
132++ {
133++ file = open_envblk_file (0, GRUB_FILE_TYPE_LOADENV | sigskip);
134++ if (! file)
135++ return grub_errno;
136++ grub_env_set ("grubenv_readonly", "y");
137++ }
138++ }
139++ grub_free (filename);
140++ }
141++ else
142++ {
143++ file = open_envblk_file (0, GRUB_FILE_TYPE_SAVEENV | sigskip);
144++ if (! file)
145++ {
146++ file = open_envblk_file (0, GRUB_FILE_TYPE_LOADENV | sigskip);
147++ if (! file)
148++ return grub_errno;
149++ grub_env_set ("grubenv_readonly", "y");
150++ }
151++ }
152++ }
153+
154+ envblk = read_envblk_file (file);
155+ if (! envblk)
156+@@ -392,11 +437,45 @@ grub_cmd_save_env (grub_extcmd_context_t
157+ if (! argc)
158+ return grub_error (GRUB_ERR_BAD_ARGUMENT, "no variable is specified");
159+
160+- file = open_envblk_file ((state[0].set) ? state[0].arg : 0,
161+- GRUB_FILE_TYPE_SAVEENV
162+- | GRUB_FILE_TYPE_SKIP_SIGNATURE);
163+- if (! file)
164+- return grub_errno;
165++ if (state[0].set)
166++ {
167++ file = open_envblk_file (state[0].arg,
168++ GRUB_FILE_TYPE_SAVEENV
169++ | GRUB_FILE_TYPE_SKIP_SIGNATURE);
170++ if (! file)
171++ return grub_errno;
172++ }
173++ else
174++ {
175++ const char *envpath = 0;
176++ char *filename = 0;
177++
178++ envpath = grub_env_get ("envpath");
179++ if (envpath)
180++ {
181++ filename = grub_strdup (envpath);
182++ file = open_envblk_file (filename,
183++ GRUB_FILE_TYPE_SAVEENV
184++ | GRUB_FILE_TYPE_SKIP_SIGNATURE);
185++ if (! file)
186++ {
187++ file = open_envblk_file (0,
188++ GRUB_FILE_TYPE_SAVEENV
189++ | GRUB_FILE_TYPE_SKIP_SIGNATURE);
190++ if (! file)
191++ return grub_errno;
192++ }
193++ grub_free (filename);
194++ }
195++ else
196++ {
197++ file = open_envblk_file (0,
198++ GRUB_FILE_TYPE_SAVEENV
199++ | GRUB_FILE_TYPE_SKIP_SIGNATURE);
200++ if (! file)
201++ return grub_errno;
202++ }
203++ }
204+
205+ if (! file->device->disk)
206+ {
207+--- a/util/grub.d/00_header.in
208++++ b/util/grub.d/00_header.in
209+@@ -23,6 +23,9 @@ datarootdir="@datarootdir@"
210+ grub_lang=`echo $LANG | cut -d . -f 1`
211+ grubdir="`echo "/@bootdirname@/@grubdirname@" | sed 's,//*,/,g'`"
212+ quick_boot="@QUICK_BOOT@"
213++efi_system_partition_disk="$(grub-probe --target=device /boot/efi)"
214++efi_system_partition_uuid="$(grub-probe --device $efi_system_partition_disk --target=fs_uuid 2> /dev/null)"
215++grub_distributor_normalized="$(echo ${GRUB_DISTRIBUTOR} | tr 'A-Z' 'a-z' | cut -d' ' -f1|LC_ALL=C sed 's,[^[:alnum:]_],_,g')";
216+
217+ export TEXTDOMAIN=@PACKAGE@
218+ export TEXTDOMAINDIR="@localedir@"
219+@@ -45,7 +48,12 @@ if [ "x${GRUB_DEFAULT_BUTTON}" = "xsaved
220+ if [ "x${GRUB_TIMEOUT_BUTTON}" = "x" ] ; then GRUB_TIMEOUT_BUTTON="$GRUB_TIMEOUT" ; fi
221+
222+ cat << EOF
223+-if [ -s \$prefix/grubenv ]; then
224++search.fs_uuid $efi_system_partition_uuid efisys
225++set envpath="(\${efisys})/EFI/$grub_distributor_normalized/grubenv"
226++if [ -s \$envpath ]; then
227++ set have_grubenv=true
228++ load_env
229++elif [ -s \$prefix/grubenv ]; then
230+ set have_grubenv=true
231+ load_env
232+ fi
233+@@ -132,41 +140,7 @@ if [ "$quick_boot" = 1 ]; then
234+ cat <<EOF
235+ function recordfail {
236+ set recordfail=1
237+-EOF
238+-
239+- check_writable () {
240+- abstractions="$(grub-probe --target=abstraction "${grubdir}")"
241+- for abstraction in $abstractions; do
242+- case "$abstraction" in
243+- diskfilter | lvm)
244+- cat <<EOF
245+- # GRUB lacks write support for $abstraction, so recordfail support is disabled.
246+-EOF
247+- return 1
248+- ;;
249+- esac
250+- done
251+-
252+- FS="$(grub-probe --target=fs "${grubdir}")"
253+- case "$FS" in
254+- btrfs | cpiofs | newc | odc | romfs | squash4 | tarfs | zfs)
255+- cat <<EOF
256+- # GRUB lacks write support for $FS, so recordfail support is disabled.
257+-EOF
258+- return 1
259+- ;;
260+- esac
261+-
262+- cat <<EOF
263+ if [ -n "\${have_grubenv}" ]; then if [ -z "\${boot_once}" ]; then save_env recordfail; fi; fi
264+-EOF
265+- }
266+-
267+- if ! check_writable; then
268+- recordfail_broken=1
269+- fi
270+-
271+- cat <<EOF
272+ }
273+ EOF
274+ fi
275+@@ -407,17 +381,15 @@ EOF
276+ cat << EOF
277+ fi
278+ fi
279+-EOF
280+-if [ "$recordfail_broken" = 1 ]; then
281+- cat << EOF
282+-if [ \$grub_platform = efi ]; then
283+- set timeout=${GRUB_RECORDFAIL_TIMEOUT:-30}
284+- if [ x\$feature_timeout_style = xy ] ; then
285+- set timeout_style=menu
286++if [ x\$grubenv_readonly = xy ] ; then
287++ if [ \$grub_platform = efi ]; then
288++ set timeout=${GRUB_RECORDFAIL_TIMEOUT:-30}
289++ if [ x\$feature_timeout_style = xy ] ; then
290++ set timeout_style=menu
291++ fi
292+ fi
293+ fi
294+ EOF
295+-fi
296+ }
297+
298+ if [ "x$GRUB_BUTTON_CMOS_ADDRESS" != "x" ]; then
299diff --git a/debian/patches/series b/debian/patches/series
300index 05de63a..7b73833 100644
301--- a/debian/patches/series
302+++ b/debian/patches/series
303@@ -111,3 +111,4 @@ Revert-kern-ieee1275-init-ppc64-Return-allocated-address-.patch
304 Revert-kern-ieee1275-init-ppc64-Decide-by-request-whether.patch
305 Revert-kern-ieee1275-init-ppc64-Introduce-a-request-for-r.patch
306 grub-install-efi-title.patch
307+enable-grubenv-on-esp.patch
308diff --git a/debian/update-grub b/debian/update-grub
309index 0c43327..fa87c35 100644
310--- a/debian/update-grub
311+++ b/debian/update-grub
312@@ -1,3 +1,6 @@
313 #!/bin/sh
314 set -e
315+
316+/usr/lib/grub/grub-common-run -r
317+
318 exec grub-mkconfig -o /boot/grub/grub.cfg "$@"

Subscribers

People subscribed via source and target branches