From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Raymund Will Date: Mon, 24 Oct 2022 14:33:50 -0400 Subject: [PATCH] loader: Add support for grub-emu to kexec Linux menu entries The GRUB emulator is used as a debugging utility but it could also be used as a user-space bootloader if there is support to boot an operating system. The Linux kernel is already able to (re)boot another kernel via the kexec boot mechanism. So the grub-emu tool could rely on this feature and have linux and initrd commands that are used to pass a kernel, initramfs image and command line parameters to kexec for booting a selected menu entry. By default the systemctl kexec option is used so systemd can shutdown all of the running services before doing a reboot using kexec. But if this is not present, it can fall back to executing the kexec user-space tool directly. The ability to force a kexec-reboot when systemctl kexec fails must only be used in controlled environments to avoid possible filesystem corruption and data loss. Signed-off-by: Raymund Will Signed-off-by: John Jolly Signed-off-by: Javier Martinez Canillas Signed-off-by: Robbie Harwood Reviewed-by: Daniel Kiper (cherry picked from commit e364307f6acc2f631b4c1fefda0791b9ce1f205f) [rharwood: conflicts around makefile and grub_exit return code] --- grub-core/Makefile.core.def | 3 - grub-core/kern/emu/main.c | 4 + grub-core/kern/emu/misc.c | 18 ++++- grub-core/loader/emu/linux.c | 178 +++++++++++++++++++++++++++++++++++++++++++ include/grub/emu/exec.h | 4 +- include/grub/emu/hostfile.h | 3 +- include/grub/emu/misc.h | 3 + docs/grub.texi | 30 ++++++-- grub-core/Makefile.am | 1 + 9 files changed, 230 insertions(+), 14 deletions(-) create mode 100644 grub-core/loader/emu/linux.c diff --git a/grub-core/Makefile.core.def b/grub-core/Makefile.core.def index 741a033978..f21da23213 100644 --- a/grub-core/Makefile.core.def +++ b/grub-core/Makefile.core.def @@ -1864,11 +1864,8 @@ module = { riscv32 = loader/riscv/linux.c; riscv64 = loader/riscv/linux.c; emu = loader/emu/linux.c; - common = loader/linux.c; common = lib/cmdline.c; - enable = noemu; - efi = loader/efi/linux.c; }; diff --git a/grub-core/kern/emu/main.c b/grub-core/kern/emu/main.c index 12277c34d2..68e2b283bb 100644 --- a/grub-core/kern/emu/main.c +++ b/grub-core/kern/emu/main.c @@ -107,6 +107,7 @@ static struct argp_option options[] = { N_("use GRUB files in the directory DIR [default=%s]"), 0}, {"verbose", 'v', 0, 0, N_("print verbose messages."), 0}, {"hold", 'H', N_("SECS"), OPTION_ARG_OPTIONAL, N_("wait until a debugger will attach"), 0}, + {"kexec", 'X', 0, 0, N_("use kexec to boot Linux kernels via systemctl (pass twice to enable dangerous fallback to non-systemctl)."), 0}, { 0, 0, 0, 0, 0, 0 } }; @@ -164,6 +165,9 @@ argp_parser (int key, char *arg, struct argp_state *state) case 'v': verbosity++; break; + case 'X': + grub_util_set_kexecute (); + break; case ARGP_KEY_ARG: { diff --git a/grub-core/kern/emu/misc.c b/grub-core/kern/emu/misc.c index d278c2921f..02d27c3440 100644 --- a/grub-core/kern/emu/misc.c +++ b/grub-core/kern/emu/misc.c @@ -39,6 +39,7 @@ #include int verbosity; +int kexecute; void grub_util_warn (const char *fmt, ...) @@ -82,7 +83,7 @@ grub_util_error (const char *fmt, ...) vfprintf (stderr, fmt, ap); va_end (ap); fprintf (stderr, ".\n"); - exit (1); + grub_exit (1); } void * @@ -154,6 +155,9 @@ void __attribute__ ((noreturn)) grub_exit (int rc) { +#if defined (GRUB_KERNEL) + grub_reboot (); +#endif exit (rc < 0 ? 1 : rc); } #endif @@ -215,3 +219,15 @@ grub_util_load_image (const char *path, char *buf) fclose (fp); } + +void +grub_util_set_kexecute (void) +{ + kexecute++; +} + +int +grub_util_get_kexecute (void) +{ + return kexecute; +} diff --git a/grub-core/loader/emu/linux.c b/grub-core/loader/emu/linux.c new file mode 100644 index 0000000000..0cf378a376 --- /dev/null +++ b/grub-core/loader/emu/linux.c @@ -0,0 +1,178 @@ +/* + * GRUB -- GRand Unified Bootloader + * Copyright (C) 2022 Free Software Foundation, Inc. + * + * GRUB is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * GRUB is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with GRUB. If not, see . + */ + +#include +#include +#include +#include + +#include +#include +#include + +GRUB_MOD_LICENSE ("GPLv3+"); + +static grub_dl_t my_mod; + +static char *kernel_path; +static char *initrd_path; +static char *boot_cmdline; + +static grub_err_t +grub_linux_boot (void) +{ + grub_err_t rc = GRUB_ERR_NONE; + char *initrd_param; + const char *kexec[] = {"kexec", "-la", kernel_path, boot_cmdline, NULL, NULL}; + const char *systemctl[] = {"systemctl", "kexec", NULL}; + int kexecute = grub_util_get_kexecute (); + + if (initrd_path) + { + initrd_param = grub_xasprintf ("--initrd=%s", initrd_path); + kexec[3] = initrd_param; + kexec[4] = boot_cmdline; + } + else + initrd_param = grub_xasprintf ("%s", ""); + + grub_dprintf ("linux", "%serforming 'kexec -la %s %s %s'\n", + (kexecute) ? "P" : "Not p", + kernel_path, initrd_param, boot_cmdline); + + if (kexecute) + rc = grub_util_exec (kexec); + + grub_free (initrd_param); + + if (rc != GRUB_ERR_NONE) + { + grub_error (rc, N_("error trying to perform kexec load operation")); + grub_sleep (3); + return rc; + } + + if (kexecute < 1) + grub_fatal (N_("use '"PACKAGE"-emu --kexec' to force a system restart")); + + grub_dprintf ("linux", "Performing 'systemctl kexec' (%s) ", + (kexecute==1) ? "do-or-die" : "just-in-case"); + rc = grub_util_exec (systemctl); + + if (kexecute == 1) + grub_fatal (N_("error trying to perform 'systemctl kexec': %d"), rc); + + /* + * WARNING: forcible reset should only be used in read-only environments. + * grub-emu cannot check for these - users beware. + */ + grub_dprintf ("linux", "Performing 'kexec -ex'"); + kexec[1] = "-ex"; + kexec[2] = NULL; + rc = grub_util_exec (kexec); + if (rc != GRUB_ERR_NONE) + grub_fatal (N_("error trying to directly perform 'kexec -ex': %d"), rc); + + return rc; +} + +static grub_err_t +grub_linux_unload (void) +{ + /* Unloading: we're no longer in use. */ + grub_dl_unref (my_mod); + grub_free (boot_cmdline); + boot_cmdline = NULL; + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_cmd_linux (grub_command_t cmd __attribute__ ((unused)), int argc, + char *argv[]) +{ + int i; + char *tempstr; + + /* Mark ourselves as in-use. */ + grub_dl_ref (my_mod); + + if (argc == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("filename expected")); + + if (!grub_util_is_regular (argv[0])) + return grub_error (GRUB_ERR_FILE_NOT_FOUND, + N_("cannot find kernel file %s"), argv[0]); + + grub_free (kernel_path); + kernel_path = grub_xasprintf ("%s", argv[0]); + + grub_free (boot_cmdline); + boot_cmdline = NULL; + + if (argc > 1) + { + boot_cmdline = grub_xasprintf ("--command-line=%s", argv[1]); + for (i = 2; i < argc; i++) + { + tempstr = grub_xasprintf ("%s %s", boot_cmdline, argv[i]); + grub_free (boot_cmdline); + boot_cmdline = tempstr; + } + } + + grub_loader_set (grub_linux_boot, grub_linux_unload, 0); + + return GRUB_ERR_NONE; +} + +static grub_err_t +grub_cmd_initrd (grub_command_t cmd __attribute__ ((unused)), int argc, + char *argv[]) +{ + if (argc == 0) + return grub_error (GRUB_ERR_BAD_ARGUMENT, N_("filename expected")); + + if (!grub_util_is_regular (argv[0])) + return grub_error (GRUB_ERR_FILE_NOT_FOUND, + N_("Cannot find initrd file %s"), argv[0]); + + grub_free (initrd_path); + initrd_path = grub_xasprintf ("%s", argv[0]); + + /* We are done - mark ourselves as on longer in use. */ + grub_dl_unref (my_mod); + + return GRUB_ERR_NONE; +} + +static grub_command_t cmd_linux, cmd_initrd; + +GRUB_MOD_INIT (linux) +{ + cmd_linux = grub_register_command ("linux", grub_cmd_linux, 0, + N_("Load Linux.")); + cmd_initrd = grub_register_command ("initrd", grub_cmd_initrd, 0, + N_("Load initrd.")); + my_mod = mod; +} + +GRUB_MOD_FINI (linux) +{ + grub_unregister_command (cmd_linux); + grub_unregister_command (cmd_initrd); +} diff --git a/include/grub/emu/exec.h b/include/grub/emu/exec.h index d1073ef86a..1b61b4a2e5 100644 --- a/include/grub/emu/exec.h +++ b/include/grub/emu/exec.h @@ -23,6 +23,8 @@ #include #include +#include + pid_t grub_util_exec_pipe (const char *const *argv, int *fd); pid_t @@ -32,7 +34,7 @@ int grub_util_exec_redirect_all (const char *const *argv, const char *stdin_file, const char *stdout_file, const char *stderr_file); int -grub_util_exec (const char *const *argv); +EXPORT_FUNC(grub_util_exec) (const char *const *argv); int grub_util_exec_redirect (const char *const *argv, const char *stdin_file, const char *stdout_file); diff --git a/include/grub/emu/hostfile.h b/include/grub/emu/hostfile.h index cfb1e2b566..a61568e36e 100644 --- a/include/grub/emu/hostfile.h +++ b/include/grub/emu/hostfile.h @@ -22,6 +22,7 @@ #include #include #include +#include #include int @@ -29,7 +30,7 @@ grub_util_is_directory (const char *path); int grub_util_is_special_file (const char *path); int -grub_util_is_regular (const char *path); +EXPORT_FUNC(grub_util_is_regular) (const char *path); char * grub_util_path_concat (size_t n, ...); diff --git a/include/grub/emu/misc.h b/include/grub/emu/misc.h index ff9c48a649..01056954b9 100644 --- a/include/grub/emu/misc.h +++ b/include/grub/emu/misc.h @@ -57,6 +57,9 @@ void EXPORT_FUNC(grub_util_warn) (const char *fmt, ...) __attribute__ ((format ( void EXPORT_FUNC(grub_util_info) (const char *fmt, ...) __attribute__ ((format (GNU_PRINTF, 1, 2))); void EXPORT_FUNC(grub_util_error) (const char *fmt, ...) __attribute__ ((format (GNU_PRINTF, 1, 2), noreturn)); +void EXPORT_FUNC(grub_util_set_kexecute) (void); +int EXPORT_FUNC(grub_util_get_kexecute) (void) WARN_UNUSED_RESULT; + grub_uint64_t EXPORT_FUNC (grub_util_get_cpu_time_ms) (void); #ifdef HAVE_DEVICE_MAPPER diff --git a/docs/grub.texi b/docs/grub.texi index a4da9c2a1b..1750b72ee9 100644 --- a/docs/grub.texi +++ b/docs/grub.texi @@ -923,17 +923,17 @@ magic. @node General boot methods @section How to boot operating systems -GRUB has two distinct boot methods. One of the two is to load an -operating system directly, and the other is to chain-load another boot -loader which then will load an operating system actually. Generally -speaking, the former is more desirable, because you don't need to -install or maintain other boot loaders and GRUB is flexible enough to -load an operating system from an arbitrary disk/partition. However, -the latter is sometimes required, since GRUB doesn't support all the -existing operating systems natively. +GRUB has three distinct boot methods: loading an operating system +directly, using kexec from userspace, and chainloading another +bootloader. Generally speaking, the first two are more desirable +because you don't need to install or maintain other boot loaders and +GRUB is flexible enough to load an operating system from an arbitrary +disk/partition. However, chainloading is sometimes required, as GRUB +doesn't support all existing operating systems natively. @menu * Loading an operating system directly:: +* Kexec:: * Chain-loading:: @end menu @@ -959,6 +959,20 @@ use more complicated instructions. @xref{DOS/Windows}, for more information. +@node Kexec +@subsection Kexec with grub2-emu + +GRUB can be run in userspace by invoking the grub2-emu tool. It will +read all configuration scripts as if booting directly (see @xref{Loading +an operating system directly}). With the @code{--kexec} flag, and +kexec(8) support from the operating system, the @command{linux} command +will directly boot the target image. For systems that lack working +systemctl(1) support for kexec, passing the @code{--kexec} flag twice +will fallback to invoking kexec(8) directly; note however that this +fallback may be unsafe outside read-only environments, as it does not +invoke shutdown machinery. + + @node Chain-loading @subsection Chain-loading an OS diff --git a/grub-core/Makefile.am b/grub-core/Makefile.am index c2e8a82bce..dd49939aaa 100644 --- a/grub-core/Makefile.am +++ b/grub-core/Makefile.am @@ -309,6 +309,7 @@ KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/emu/net.h KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/emu/hostdisk.h KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/emu/hostfile.h KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/extcmd.h +KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/emu/exec.h if COND_GRUB_EMU_SDL KERNEL_HEADER_FILES += $(top_srcdir)/include/grub/sdl.h endif