/* SPDX-License-Identifier: LGPL-2.1-or-later */

#include <sys/kexec.h>
#include <unistd.h>

#include "sd-bus.h"

#include "bootspec.h"
#include "bus-error.h"
#include "bus-locator.h"
#include "efivars.h"
#include "fd-util.h"
#include "log.h"
#include "parse-util.h"
#include "path-util.h"
#include "process-util.h"
#include "reboot-util.h"
#include "runtime-scope.h"
#include "strv.h"
#include "systemctl.h"
#include "systemctl-logind.h"
#include "systemctl-start-special.h"
#include "systemctl-start-unit.h"
#include "systemctl-trivial-method.h"
#include "systemctl-util.h"

static int load_kexec_kernel(void) {
        int r;

        if (kexec_loaded()) {
                if (arg_kernel_cmdline)
                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                               "--kernel-cmdline= specified but kexec kernel already loaded");
                log_debug("Kexec kernel already loaded.");
                return 0;
        }

        _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL;
        r = boot_config_load_auto(&config, NULL, NULL);
        if (r == -ENOKEY)
                /* The call doesn't log about ENOKEY, let's do so here. */
                return log_error_errno(r,
                                       "No kexec kernel loaded and autodetection failed.\n%s",
                                       is_efi_boot()
                                       ? "Cannot automatically load kernel: ESP mount point not found."
                                       : "Automatic loading works only on systems booted with EFI.");
        if (r < 0)
                return r;

        r = boot_config_select_special_entries(&config, /* skip_efivars= */ false);
        if (r < 0)
                return r;

        const BootEntry *e = boot_config_default_entry(&config);
        if (!e)
                return log_error_errno(SYNTHETIC_ERRNO(ENOENT),
                                       "No boot loader entry suitable as default, refusing to guess.");

        log_debug("Found default boot loader entry in file \"%s\"", e->path);

        if (!e->kernel)
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
                                       "Boot entry does not refer to Linux kernel, which is not supported currently.");
        if (strv_length(e->initrd) > 1)
                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP),
                                       "Boot entry specifies multiple initrds, which is not supported currently.");

        _cleanup_free_ char *kernel = NULL;
        kernel = path_join(e->root, e->kernel);
        if (!kernel)
                return log_oom();

        _cleanup_free_ char *initrd = NULL;
        if (!strv_isempty(e->initrd)) {
                initrd = path_join(e->root, e->initrd[0]);
                if (!initrd)
                        return log_oom();
        }

        _cleanup_free_ char *options = strv_join(e->options, " ");
        if (!options)
                return log_oom();

        if (!isempty(arg_kernel_cmdline) && !strextend_with_separator(&options, " ", arg_kernel_cmdline))
                return log_oom();

        log_full(arg_quiet ? LOG_DEBUG : LOG_INFO,
                 "%s %s kernel=\"%s\" cmdline=\"%s\"%s%s%s",
                 arg_dry_run ? "Would call" : "Calling",
                 HAVE_KEXEC_FILE_LOAD_SYSCALL ? "kexec_file_load()" : "kexec",
                 kernel,
                 options,
                 initrd ? " initrd=\"" : "", strempty(initrd), initrd ? "\"" : "");
        if (arg_dry_run)
                return 0;

#if HAVE_KEXEC_FILE_LOAD_SYSCALL
        _cleanup_close_ int kernel_fd = open(kernel, O_RDONLY|O_CLOEXEC);
        if (kernel_fd < 0)
                return log_error_errno(errno, "Failed to open kernel '%s': %m", kernel);

        _cleanup_close_ int initrd_fd = -EBADF;
        if (initrd) {
                initrd_fd = open(initrd, O_RDONLY|O_CLOEXEC);
                if (initrd_fd < 0)
                        return log_error_errno(errno, "Failed to open initrd '%s': %m", initrd);
        }

        unsigned long flags = initrd ? 0 : KEXEC_FILE_NO_INITRAMFS;

        if (kexec_file_load(kernel_fd, initrd_fd, strlen(options) + 1, options, flags) >= 0)
                return 0;

        int saved_errno = errno;

        if (IN_SET(saved_errno, ENOEXEC, EINVAL)) {
                /* The kernel didn't recognize the image format. Try decompressing or extracting the
                 * kernel (e.g. compressed Image, ZBOOT PE, or UKI) and loading again.
                 *
                 * Most architectures' kexec_file_load() returns ENOEXEC when no image loader matches
                 * (the default behavior of kexec_image_probe_default()), but arm64's image_probe()
                 * returns EINVAL for an unrecognized magic. Accept both, otherwise `systemctl kexec`
                 * of a UKI never reaches the extraction path on arm64. */
                log_debug_errno(saved_errno, "Kernel rejected image, trying decompression/extraction: %m");

                _cleanup_close_ int extracted_kernel_fd = -EBADF, extracted_initrd_fd = -EBADF;
                r = kexec_maybe_decompress_kernel(
                                kernel, kernel_fd, &extracted_kernel_fd,
                                initrd_fd >= 0 ? NULL : &extracted_initrd_fd);
                if (r < 0)
                        log_debug_errno(r, "Failed to decompress/extract kernel image, ignoring: %m");
                else if (r > 0) {
                        int final_initrd_fd = initrd_fd >= 0 ? initrd_fd : extracted_initrd_fd;
                        unsigned long final_flags = final_initrd_fd >= 0 ? 0 : KEXEC_FILE_NO_INITRAMFS;

                        if (kexec_file_load(extracted_kernel_fd, final_initrd_fd, strlen(options) + 1, options, final_flags) >= 0)
                                return 0;

                        saved_errno = errno;
                }
        }

        log_debug_errno(saved_errno, "kexec_file_load() failed, falling back to " KEXEC " binary: %m");
#endif

        /* Fall back to kexec binary for architectures without kexec_file_load() or when the
         * syscall fails (e.g. the kernel's kexec handler doesn't support this image format
         * but kexec-tools might via the older kexec_load() code path). */
        if (access(KEXEC, X_OK) < 0)
                return log_error_errno(errno, KEXEC " is not available: %m");

        r = pidref_safe_fork(
                        "(kexec)",
                        FORK_WAIT|FORK_RESET_SIGNALS|FORK_DEATHSIG_SIGTERM|FORK_RLIMIT_NOFILE_SAFE|FORK_LOG,
                        /* ret= */ NULL);
        if (r < 0)
                return r;
        if (r == 0) {
                const char* const args[] = {
                        KEXEC,
                        "--load", kernel,
                        "--append", options,
                        initrd ? "--initrd" : NULL, initrd,
                        NULL
                };

                /* Child */
                execv(args[0], (char * const *) args);
                _exit(EXIT_FAILURE);
        }

        return 0;
}

static int set_exit_code(uint8_t code) {
        _cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
        sd_bus *bus;
        int r;

        r = acquire_bus(BUS_MANAGER, &bus);
        if (r < 0)
                return r;

        r = bus_call_method(bus, bus_systemd_mgr, "SetExitCode", &error, NULL, "y", code);
        if (r < 0)
                return log_error_errno(r, "Failed to set exit code: %s", bus_error_message(&error, r));

        return 0;
}

int verb_start_special(int argc, char *argv[], uintptr_t data, void *userdata) {
        bool termination_action; /* An action that terminates the system, can be performed also by signal. */
        enum action a;
        int r;

        assert(argv);

        a = verb_to_action(argv[0]);

        r = logind_check_inhibitors(a);
        if (r < 0)
                return r;

        if (arg_force >= 2) {
                r = must_be_root();
                if (r < 0)
                        return r;
        }

        termination_action = IN_SET(a, ACTION_HALT, ACTION_POWEROFF, ACTION_REBOOT);

        if (termination_action) {
                r = prepare_firmware_setup();
                if (r < 0)
                        return r;

                r = prepare_boot_loader_menu();
                if (r < 0)
                        return r;

                r = prepare_boot_loader_entry();
                if (r < 0)
                        return r;
        }

        if (arg_reboot_argument && IN_SET(a, ACTION_HALT, ACTION_POWEROFF, ACTION_REBOOT, ACTION_KEXEC)) {
                /* If we are going through an action that involves systemd-shutdown, let's set the reboot
                 * parameter, even if it's not a regular reboot. After all we nowadays send the string to
                 * our supervisor via sd_notify() too. */
                r = update_reboot_parameter_and_warn(arg_reboot_argument, /* keep= */ false);
                if (r < 0)
                        return r;
        }

        if (a == ACTION_KEXEC) {
                r = load_kexec_kernel();
                if (r < 0 && arg_force >= 1)
                        log_notice("Failed to load kexec kernel, continuing without.");
                else if (r < 0)
                        return r;

        } else if (a == ACTION_EXIT && argc > 1) {
                uint8_t code;

                /* If the exit code is not given on the command line, don't reset it to zero: just keep it as
                 * it might have been set previously. */

                r = safe_atou8(argv[1], &code);
                if (r < 0)
                        return log_error_errno(r, "Invalid exit code.");

                r = set_exit_code(code);
                if (r < 0)
                        return r;
        }

        if (termination_action && arg_force >= 2)
                return halt_now(a);

        if (arg_force >= 1 &&
            (termination_action || IN_SET(a, ACTION_KEXEC, ACTION_EXIT)))
                r = verb_trivial_method(argc, argv, data, userdata);
        else {
                /* First try logind, to allow authentication with polkit */
                switch (a) {

                case ACTION_POWEROFF:
                case ACTION_REBOOT:
                case ACTION_KEXEC:
                case ACTION_HALT:
                case ACTION_SOFT_REBOOT:
                        if (arg_when == 0) {
                                r = logind_reboot(a);
                                if (r >= 0 || IN_SET(r, -EACCES, -EOPNOTSUPP, -EINPROGRESS))
                                        /* The latter indicates that the requested operation requires auth,
                                         * is not supported or already in progress, in which cases we ignore the error. */
                                        return r;
                        } else {
                                r = logind_schedule_shutdown(a);
                                if (r != -ENOSYS)
                                        return r;
                        }

                        /* On all other errors, try low-level operation. In order to minimize the difference
                         * between operation with and without logind, we explicitly enable non-blocking mode
                         * for this, as logind's shutdown operations are always non-blocking. */
                        arg_no_block = true;
                        break;

                case ACTION_SUSPEND:
                case ACTION_HIBERNATE:
                case ACTION_HYBRID_SLEEP:
                case ACTION_SUSPEND_THEN_HIBERNATE:

                        /* For sleep operations, do not automatically fall back to low-level operation for
                         * errors other than logind not available. There's a high chance that logind did
                         * some extra sanity check and that didn't pass. */
                        r = logind_reboot(a);
                        if (r >= 0 || (r != -ENOSYS && arg_force == 0))
                                return r;

                        arg_no_block = true;
                        break;

                case ACTION_SLEEP:
                        return logind_reboot(a);

                case ACTION_EXIT:
                        /* Since exit is so close in behaviour to power-off/reboot, let's also make
                         * it asynchronous, in order to not confuse the user needlessly with unexpected
                         * behaviour. */
                        arg_no_block = true;
                        break;

                default:
                        ;
                }

                r = verb_start(argc, argv, data, userdata);
        }

        if (termination_action && arg_force < 2 &&
            IN_SET(r, -ENOENT, -ETIMEDOUT))
                log_notice("It is possible to perform action directly, see discussion of --force --force in man:systemctl(1).");

        return r;
}

int verb_start_system_special(int argc, char *argv[], uintptr_t data, void *userdata) {
        /* Like start_special above, but raises an error when running in user mode */

        if (arg_runtime_scope != RUNTIME_SCOPE_SYSTEM)
                return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
                                       "Bad action for %s mode.",
                                       runtime_scope_cmdline_option_to_string(arg_runtime_scope));

        return verb_start_special(argc, argv, data, userdata);
}
