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