On Btrfs, GRUB cannot update the environment block (grubenv) because
file based writes via blocklists are incompatible with Btrfs COW (Copy
On Write) design. Although GRUB’s filesystem drivers are read only,
environment updates rely on raw block access to fixed locations, which
is not safe on Btrfs due to its dynamic block relocation.

To address this, we introduce support for storing the GRUB environment
block in a fixed block within Btrfs reserved bootloader area, an unused
region in the device header intended for bootloader use.

This patch adds fs_envblk helpers to grub-editenv for accessing the
reserved area directly. Variables that require runtime write access
during boot, such as next_entry, will be mirrored into this external
block. Other variables will remain stored in the file based grubenv so
they can keep per snapshot.

The embedding logic is also updated to mark the reserved area as used to
avoid conflicts with embedded core images.

This enables support for runtime environment updates on Btrfs root
volumes, allowing tools like grub-reboot to boot an entry once.

Signed-off-by: Michael Chang <mch...@suse.com>
---
 grub-core/fs/btrfs.c     |   3 +-
 include/grub/fs.h        |   2 +
 util/grub-editenv.c      | 337 ++++++++++++++++++++++++++++++++++++++-
 util/grub.d/00_header.in |  20 ++-
 4 files changed, 357 insertions(+), 5 deletions(-)

diff --git a/grub-core/fs/btrfs.c b/grub-core/fs/btrfs.c
index 7bf8d922f..ceacc1151 100644
--- a/grub-core/fs/btrfs.c
+++ b/grub-core/fs/btrfs.c
@@ -2341,7 +2341,7 @@ struct embed_region {
 
 static const struct {
   struct embed_region available;
-  struct embed_region used[6];
+  struct embed_region used[7];
 } btrfs_head = {
   .available = {0, GRUB_DISK_KiB_TO_SECTORS (1024)}, /* The first 1 MiB. */
   .used = {
@@ -2349,6 +2349,7 @@ static const struct {
     {GRUB_DISK_KiB_TO_SECTORS (64) - 1, 1},                        /* Overflow 
guard. */
     {GRUB_DISK_KiB_TO_SECTORS (64), GRUB_DISK_KiB_TO_SECTORS (4)}, /* 4 KiB 
superblock. */
     {GRUB_DISK_KiB_TO_SECTORS (68), 1},                            /* Overflow 
guard. */
+    {GRUB_DISK_KiB_TO_SECTORS (ENV_BTRFS_OFFSET) - 1, 3},          /* 
Environment Block. */
     {GRUB_DISK_KiB_TO_SECTORS (1024) - 1, 1},                      /* Overflow 
guard. */
     {0, 0}                                                         /* Array 
terminator. */
   }
diff --git a/include/grub/fs.h b/include/grub/fs.h
index df4c93b16..9c8206133 100644
--- a/include/grub/fs.h
+++ b/include/grub/fs.h
@@ -132,4 +132,6 @@ grub_fs_unregister (grub_fs_t fs)
 
 grub_fs_t EXPORT_FUNC(grub_fs_probe) (grub_device_t device);
 
+#define ENV_BTRFS_OFFSET (256)
+
 #endif /* ! GRUB_FS_HEADER */
diff --git a/util/grub-editenv.c b/util/grub-editenv.c
index db6f187cc..31103df1d 100644
--- a/util/grub-editenv.c
+++ b/util/grub-editenv.c
@@ -23,8 +23,11 @@
 #include <grub/util/misc.h>
 #include <grub/lib/envblk.h>
 #include <grub/i18n.h>
-#include <grub/emu/hostfile.h>
+#include <grub/emu/hostdisk.h>
 #include <grub/util/install.h>
+#include <grub/emu/getroot.h>
+#include <grub/fs.h>
+#include <grub/crypto.h>
 
 #include <stdio.h>
 #include <unistd.h>
@@ -120,6 +123,142 @@ block, use `rm %s'."),
   NULL, help_filter, NULL
 };
 
+struct fs_envblk_spec {
+  const char *fs_name;
+  int offset;
+  int size;
+} fs_envblk_spec[] = {
+  { "btrfs", ENV_BTRFS_OFFSET * 1024, GRUB_DISK_SECTOR_SIZE },
+  { NULL, 0, 0 }
+};
+
+struct fs_envblk {
+  struct fs_envblk_spec *spec;
+  const char *dev;
+};
+
+typedef struct fs_envblk_spec *fs_envblk_spec_t;
+typedef struct fs_envblk *fs_envblk_t;
+
+fs_envblk_t fs_envblk = NULL;
+
+static int
+read_envblk_fs (const char *varname, const char *value, void *hook_data)
+{
+  grub_envblk_t *p_envblk = (grub_envblk_t *)hook_data;
+
+  if (!p_envblk || !fs_envblk)
+    return 0;
+
+  if (strcmp (varname, "env_block") == 0)
+    {
+      int off, sz;
+      char *p;
+
+      off = strtol (value, &p, 10);
+      if (*p == '+')
+       sz = strtol (p+1, &p, 10);
+      else
+       return 0;
+
+      if (*p == '\0')
+       {
+         FILE *fp;
+         char *buf;
+
+         off <<= GRUB_DISK_SECTOR_BITS;
+         sz <<= GRUB_DISK_SECTOR_BITS;
+
+         fp = grub_util_fopen (fs_envblk->dev, "rb");
+         if (! fp)
+           grub_util_error (_("cannot open `%s': %s"), fs_envblk->dev,
+                               strerror (errno));
+
+
+         if (fseek (fp, off, SEEK_SET) < 0)
+           grub_util_error (_("cannot seek `%s': %s"), fs_envblk->dev,
+                               strerror (errno));
+
+         buf = xmalloc (sz);
+         if ((fread (buf, 1, sz, fp)) != sz)
+           grub_util_error (_("cannot read `%s': %s"), fs_envblk->dev,
+                               strerror (errno));
+
+         fclose (fp);
+
+         *p_envblk = grub_envblk_open (buf, sz);
+       }
+    }
+
+  return 0;
+}
+
+static void
+create_envblk_fs (void)
+{
+  FILE *fp;
+  char *buf;
+  const char *device;
+  int offset, size;
+
+  if (!fs_envblk)
+    return;
+
+  device = fs_envblk->dev;
+  offset = fs_envblk->spec->offset;
+  size = fs_envblk->spec->size;
+
+  fp = grub_util_fopen (device, "r+b");
+  if (! fp)
+    grub_util_error (_("cannot open `%s': %s"), device, strerror (errno));
+
+  buf = xmalloc (size);
+  memcpy (buf, GRUB_ENVBLK_SIGNATURE, sizeof (GRUB_ENVBLK_SIGNATURE) - 1);
+  memset (buf + sizeof (GRUB_ENVBLK_SIGNATURE) - 1, '#', size - sizeof 
(GRUB_ENVBLK_SIGNATURE) + 1);
+
+  if (fseek (fp, offset, SEEK_SET) < 0)
+    grub_util_error (_("cannot seek `%s': %s"), device, strerror (errno));
+
+  if (fwrite (buf, 1, size, fp) != size)
+    grub_util_error (_("cannot write to `%s': %s"), device, strerror (errno));
+
+  grub_util_file_sync (fp);
+  free (buf);
+  fclose (fp);
+}
+
+static grub_envblk_t
+open_envblk_fs (grub_envblk_t envblk)
+{
+  grub_envblk_t envblk_fs = NULL;
+  char *val;
+  int offset, size;
+
+  if (!fs_envblk)
+    return NULL;
+
+  offset = fs_envblk->spec->offset;
+  size = fs_envblk->spec->size;
+
+  grub_envblk_iterate (envblk, &envblk_fs, read_envblk_fs);
+
+  if (envblk_fs && grub_envblk_size (envblk_fs) == size)
+    return envblk_fs;
+
+  create_envblk_fs ();
+
+  offset = offset >> GRUB_DISK_SECTOR_BITS;
+  size =  (size + GRUB_DISK_SECTOR_SIZE - 1) >> GRUB_DISK_SECTOR_BITS;
+
+  val = xasprintf ("%d+%d", offset, size);
+  if (! grub_envblk_set (envblk, "env_block", val))
+    grub_util_error ("%s", _("environment block too small"));
+  grub_envblk_iterate (envblk, &envblk_fs, read_envblk_fs);
+  free (val);
+
+  return envblk_fs;
+}
+
 static grub_envblk_t
 open_envblk_file (const char *name)
 {
@@ -182,10 +321,17 @@ static void
 list_variables (const char *name)
 {
   grub_envblk_t envblk;
+  grub_envblk_t envblk_fs = NULL;
 
   envblk = open_envblk_file (name);
+  grub_envblk_iterate (envblk, &envblk_fs, read_envblk_fs);
   grub_envblk_iterate (envblk, NULL, print_var);
   grub_envblk_close (envblk);
+  if (envblk_fs)
+    {
+      grub_envblk_iterate (envblk_fs, NULL, print_var);
+      grub_envblk_close (envblk_fs);
+    }
 }
 
 static void
@@ -208,6 +354,38 @@ write_envblk (const char *name, grub_envblk_t envblk)
   fclose (fp);
 }
 
+static void
+write_envblk_fs (grub_envblk_t envblk)
+{
+  FILE *fp;
+  const char *device;
+  int offset, size;
+
+  if (!fs_envblk)
+    return;
+
+  device = fs_envblk->dev;
+  offset = fs_envblk->spec->offset;
+  size = fs_envblk->spec->size;
+
+  if (grub_envblk_size (envblk) > size)
+    grub_util_error ("%s", _("environment block too small"));
+
+  fp = grub_util_fopen (device, "r+b");
+
+  if (! fp)
+    grub_util_error (_("cannot open `%s': %s"), device, strerror (errno));
+
+  if (fseek (fp, offset, SEEK_SET) < 0)
+    grub_util_error (_("cannot seek `%s': %s"), device, strerror (errno));
+
+  if (fwrite (grub_envblk_buffer (envblk), 1, grub_envblk_size (envblk), fp) 
!= grub_envblk_size (envblk))
+    grub_util_error (_("cannot write to `%s': %s"), device, strerror (errno));
+
+  grub_util_file_sync (fp);
+  fclose (fp);
+}
+
 static void
 set_variables (const char *name, int argc, char *argv[])
 {
@@ -224,8 +402,27 @@ set_variables (const char *name, int argc, char *argv[])
 
       *(p++) = 0;
 
-      if (! grub_envblk_set (envblk, argv[0], p))
-        grub_util_error ("%s", _("environment block too small"));
+      if ((strcmp (argv[0], "next_entry") == 0 ||
+         strcmp (argv[0], "health_checker_flag") == 0) && fs_envblk)
+       {
+         grub_envblk_t envblk_fs;
+         envblk_fs = open_envblk_fs (envblk);
+         if (!envblk_fs)
+           grub_util_error ("%s", _("can't open fs environment block"));
+         if (! grub_envblk_set (envblk_fs, argv[0], p))
+           grub_util_error ("%s", _("environment block too small"));
+         write_envblk_fs (envblk_fs);
+         grub_envblk_close (envblk_fs);
+       }
+      else if (strcmp (argv[0], "env_block") == 0)
+       {
+         grub_util_warn ("can't set env_block as it's read-only");
+       }
+      else
+       {
+         if (! grub_envblk_set (envblk, argv[0], p))
+           grub_util_error ("%s", _("environment block too small"));
+       }
 
       argc--;
       argv++;
@@ -239,20 +436,151 @@ static void
 unset_variables (const char *name, int argc, char *argv[])
 {
   grub_envblk_t envblk;
+  grub_envblk_t envblk_fs;
 
   envblk = open_envblk_file (name);
+
+  envblk_fs = NULL;
+  if (fs_envblk)
+    envblk_fs = open_envblk_fs (envblk);
+
   while (argc)
     {
       grub_envblk_delete (envblk, argv[0]);
 
+      if (envblk_fs)
+       grub_envblk_delete (envblk_fs, argv[0]);
+
       argc--;
       argv++;
     }
 
   write_envblk (name, envblk);
   grub_envblk_close (envblk);
+
+  if (envblk_fs)
+    {
+      write_envblk_fs (envblk_fs);
+      grub_envblk_close (envblk_fs);
+    }
+}
+
+int have_abstraction = 0;
+static void
+probe_abstraction (grub_disk_t disk)
+{
+  if (disk->partition == NULL)
+    grub_util_info ("no partition map found for %s", disk->name);
+
+  if (disk->dev->id == GRUB_DISK_DEVICE_DISKFILTER_ID ||
+      disk->dev->id == GRUB_DISK_DEVICE_CRYPTODISK_ID)
+    {
+      have_abstraction = 1;
+    }
 }
 
+static fs_envblk_t
+probe_fs_envblk (fs_envblk_spec_t spec)
+{
+  char **grub_devices;
+  char **curdev, **curdrive;
+  size_t ndev = 0;
+  char **grub_drives;
+  grub_device_t grub_dev = NULL;
+  grub_fs_t grub_fs;
+  const char *fs_envblk_device;
+
+#ifdef __s390x__
+  return NULL;
+#endif
+
+  grub_util_biosdisk_init (DEFAULT_DEVICE_MAP);
+  grub_init_all ();
+  grub_gcry_init_all ();
+
+  grub_lvm_fini ();
+  grub_mdraid09_fini ();
+  grub_mdraid1x_fini ();
+  grub_diskfilter_fini ();
+  grub_diskfilter_init ();
+  grub_mdraid09_init ();
+  grub_mdraid1x_init ();
+  grub_lvm_init ();
+
+  grub_devices = grub_guess_root_devices (DEFAULT_DIRECTORY);
+
+  if (!grub_devices || !grub_devices[0])
+    grub_util_error (_("cannot find a device for %s (is /dev mounted?)"), 
DEFAULT_DIRECTORY);
+
+  fs_envblk_device = grub_devices[0];
+
+  for (curdev = grub_devices; *curdev; curdev++)
+    {
+      grub_util_pull_device (*curdev);
+      ndev++;
+    }
+
+  grub_drives = xcalloc ((ndev + 1), sizeof (grub_drives[0]));
+
+  for (curdev = grub_devices, curdrive = grub_drives; *curdev; curdev++,
+       curdrive++)
+    {
+      *curdrive = grub_util_get_grub_dev (*curdev);
+      if (! *curdrive)
+       grub_util_error (_("cannot find a GRUB drive for %s.  Check your 
device.map"),
+                        *curdev);
+    }
+  *curdrive = 0;
+
+  grub_dev = grub_device_open (grub_drives[0]);
+  if (! grub_dev)
+    grub_util_error ("%s", grub_errmsg);
+
+  grub_fs = grub_fs_probe (grub_dev);
+  if (! grub_fs)
+    grub_util_error ("%s", grub_errmsg);
+
+  if (grub_dev->disk)
+    {
+      probe_abstraction (grub_dev->disk);
+    }
+  for (curdrive = grub_drives + 1; *curdrive; curdrive++)
+    {
+      grub_device_t dev = grub_device_open (*curdrive);
+      if (!dev)
+       continue;
+      if (dev->disk)
+       probe_abstraction (dev->disk);
+      grub_device_close (dev);
+    }
+
+  free (grub_drives);
+  grub_device_close (grub_dev);
+  grub_gcry_fini_all ();
+  grub_fini_all ();
+  grub_util_biosdisk_fini ();
+
+  fs_envblk_spec_t p;
+
+  for (p = spec; p->fs_name; p++)
+    {
+      if (strcmp (grub_fs->name, p->fs_name) == 0 && !have_abstraction)
+       {
+         if (p->offset % GRUB_DISK_SECTOR_SIZE == 0 &&
+             p->size % GRUB_DISK_SECTOR_SIZE == 0)
+           {
+             fs_envblk = xmalloc (sizeof (fs_envblk_t));
+             fs_envblk->spec = p;
+             fs_envblk->dev = strdup(fs_envblk_device);
+             return fs_envblk;
+           }
+       }
+    }
+
+  return NULL;
+}
+
+
 int
 main (int argc, char *argv[])
 {
@@ -284,6 +612,9 @@ main (int argc, char *argv[])
       command  = argv[curindex++];
     }
 
+  if (strcmp (filename, DEFAULT_ENVBLK_PATH) == 0)
+    fs_envblk = probe_fs_envblk (fs_envblk_spec);
+
   if (strcmp (command, "create") == 0)
     grub_util_create_envblk_file (filename);
   else if (strcmp (command, "list") == 0)
diff --git a/util/grub.d/00_header.in b/util/grub.d/00_header.in
index f86b69bad..0d3928df6 100644
--- a/util/grub.d/00_header.in
+++ b/util/grub.d/00_header.in
@@ -46,6 +46,13 @@ cat << EOF
 if [ -s \$prefix/grubenv ]; then
   load_env
 fi
+
+if [ "\${env_block}" ] ; then
+  set env_block="(\${root})\${env_block}"
+  export env_block
+  load_env -f "\${env_block}"
+fi
+
 EOF
 if [ "x$GRUB_BUTTON_CMOS_ADDRESS" != "x" ]; then
     cat <<EOF
@@ -55,6 +62,9 @@ elif [ "\${next_entry}" ] ; then
    set default="\${next_entry}"
    set next_entry=
    save_env next_entry
+   if [ "\${env_block}" ] ; then
+     save_env -f "\${env_block}" next_entry
+   fi
    set boot_once=true
 else
    set default="${GRUB_DEFAULT}"
@@ -66,6 +76,9 @@ if [ "\${next_entry}" ] ; then
    set default="\${next_entry}"
    set next_entry=
    save_env next_entry
+   if [ "\${env_block}" ] ; then
+     save_env -f "\${env_block}" next_entry
+   fi
    set boot_once=true
 else
    set default="${GRUB_DEFAULT}"
@@ -93,7 +106,12 @@ fi
 function savedefault {
   if [ -z "\${boot_once}" ]; then
     saved_entry="\${chosen}"
-    save_env saved_entry
+    if [ "\${env_block}" ] ; then
+      save_env -f "\${env_block}" saved_entry
+    else
+      save_env saved_entry
+    fi
+
   fi
 }
 
-- 
2.50.0


_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

Reply via email to