Hello,
We have some fedora maintainers and users who are eager to have this patch (
https://bugzilla.redhat.com/show_bug.cgi?id=2372973), so we would really
appreciate if it could get review asap. SUSE have been carrying it for
about a year
https://build.opensuse.org/projects/Base:System/packages/grub2/files/grub2-grubenv-in-btrfs-header.patch

thank you so much!
marta

On Fri, Jul 11, 2025 at 6:24 AM Michael Chang via Grub-devel <
grub-devel@gnu.org> wrote:

> 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
>
_______________________________________________
Grub-devel mailing list
Grub-devel@gnu.org
https://lists.gnu.org/mailman/listinfo/grub-devel

Reply via email to