diff options
Diffstat (limited to 'src/kernel-install')
-rwxr-xr-x | src/kernel-install/50-depmod.install | 53 | ||||
-rwxr-xr-x | src/kernel-install/60-ukify.install.in | 265 | ||||
-rwxr-xr-x | src/kernel-install/90-loaderentry.install.in | 210 | ||||
-rwxr-xr-x | src/kernel-install/90-uki-copy.install | 97 | ||||
-rw-r--r-- | src/kernel-install/install.conf | 12 | ||||
-rw-r--r-- | src/kernel-install/kernel-install.c | 1743 | ||||
-rw-r--r-- | src/kernel-install/meson.build | 51 | ||||
-rwxr-xr-x | src/kernel-install/test-kernel-install.sh | 333 |
8 files changed, 2764 insertions, 0 deletions
diff --git a/src/kernel-install/50-depmod.install b/src/kernel-install/50-depmod.install new file mode 100755 index 0000000..88f858f --- /dev/null +++ b/src/kernel-install/50-depmod.install @@ -0,0 +1,53 @@ +#!/bin/sh +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd 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 Lesser General Public License +# along with systemd; If not, see <https://www.gnu.org/licenses/>. + +set -e + +COMMAND="${1:?}" +KERNEL_VERSION="${2:?}" + +[ -w "/lib/modules" ] || exit 0 + +case "$COMMAND" in + add) + [ -d "/lib/modules/$KERNEL_VERSION/kernel" ] || exit 0 + command -v depmod >/dev/null || exit 0 + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "+depmod -a $KERNEL_VERSION" + exec depmod -a "$KERNEL_VERSION" + ;; + remove) + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \ + echo "Removing /lib/modules/${KERNEL_VERSION}/modules.dep and associated files" + exec rm -f \ + "/lib/modules/$KERNEL_VERSION/modules.alias" \ + "/lib/modules/$KERNEL_VERSION/modules.alias.bin" \ + "/lib/modules/$KERNEL_VERSION/modules.builtin.bin" \ + "/lib/modules/$KERNEL_VERSION/modules.builtin.alias.bin" \ + "/lib/modules/$KERNEL_VERSION/modules.dep" \ + "/lib/modules/$KERNEL_VERSION/modules.dep.bin" \ + "/lib/modules/$KERNEL_VERSION/modules.devname" \ + "/lib/modules/$KERNEL_VERSION/modules.softdep" \ + "/lib/modules/$KERNEL_VERSION/modules.symbols" \ + "/lib/modules/$KERNEL_VERSION/modules.symbols.bin" + ;; + *) + exit 0 + ;; +esac diff --git a/src/kernel-install/60-ukify.install.in b/src/kernel-install/60-ukify.install.in new file mode 100755 index 0000000..be1e21b --- /dev/null +++ b/src/kernel-install/60-ukify.install.in @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: LGPL-2.1-or-later +# -*- mode: python-mode -*- +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd 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 Lesser General Public License +# along with systemd; If not, see <https://www.gnu.org/licenses/>. + +# pylint: disable=import-outside-toplevel,consider-using-with,disable=redefined-builtin + +import argparse +import os +import runpy +import shlex +from shutil import which +from pathlib import Path +from typing import Optional + +__version__ = '{{PROJECT_VERSION}} ({{GIT_VERSION}})' + +try: + VERBOSE = int(os.environ['KERNEL_INSTALL_VERBOSE']) > 0 +except (KeyError, ValueError): + VERBOSE = False + +# Override location of ukify and the boot stub for testing and debugging. +UKIFY = os.getenv('KERNEL_INSTALL_UKIFY', which('ukify')) +BOOT_STUB = os.getenv('KERNEL_INSTALL_BOOT_STUB') + + +def shell_join(cmd): + # TODO: drop in favour of shlex.join once shlex.join supports pathlib.Path. + return ' '.join(shlex.quote(str(x)) for x in cmd) + +def log(*args, **kwargs): + if VERBOSE: + print(*args, **kwargs) + +def path_is_readable(p: Path, dir=False) -> None: + """Verify access to a file or directory.""" + try: + p.open().close() + except IsADirectoryError: + if dir: + return + raise + +def mandatory_variable(name): + try: + return os.environ[name] + except KeyError as e: + raise KeyError(f'${name} must be set in the environment') from e + +def parse_args(args=None): + p = argparse.ArgumentParser( + description='kernel-install plugin to build a Unified Kernel Image', + allow_abbrev=False, + usage='60-ukify.install COMMAND KERNEL_VERSION ENTRY_DIR KERNEL_IMAGE INITRD…', + ) + + # Suppress printing of usage synopsis on errors + p.error = lambda message: p.exit(2, f'{p.prog}: error: {message}\n') + + p.add_argument('command', + metavar='COMMAND', + help="The action to perform. Only 'add' is supported.") + p.add_argument('kernel_version', + metavar='KERNEL_VERSION', + help='Kernel version string') + p.add_argument('entry_dir', + metavar='ENTRY_DIR', + type=Path, + nargs='?', + help='Type#1 entry directory (ignored)') + p.add_argument('kernel_image', + metavar='KERNEL_IMAGE', + type=Path, + nargs='?', + help='Kernel binary') + p.add_argument('initrd', + metavar='INITRD…', + type=Path, + nargs='*', + help='Initrd files') + p.add_argument('--version', + action='version', + version=f'systemd {__version__}') + + opts = p.parse_args(args) + + if opts.command == 'add': + opts.staging_area = Path(mandatory_variable('KERNEL_INSTALL_STAGING_AREA')) + path_is_readable(opts.staging_area, dir=True) + + opts.entry_token = mandatory_variable('KERNEL_INSTALL_ENTRY_TOKEN') + opts.machine_id = mandatory_variable('KERNEL_INSTALL_MACHINE_ID') + + return opts + +def we_are_wanted() -> bool: + KERNEL_INSTALL_LAYOUT = os.getenv('KERNEL_INSTALL_LAYOUT') + + if KERNEL_INSTALL_LAYOUT != 'uki': + log(f'{KERNEL_INSTALL_LAYOUT=}, quitting.') + return False + + KERNEL_INSTALL_UKI_GENERATOR = os.getenv('KERNEL_INSTALL_UKI_GENERATOR') or 'ukify' + + if KERNEL_INSTALL_UKI_GENERATOR != 'ukify': + log(f'{KERNEL_INSTALL_UKI_GENERATOR=}, quitting.') + return False + + log('KERNEL_INSTALL_LAYOUT and KERNEL_INSTALL_UKI_GENERATOR are good') + return True + + +def input_file_location( + filename: str, + *search_directories: str) -> Optional[Path]: + + if root := os.getenv('KERNEL_INSTALL_CONF_ROOT'): + search_directories = (root,) + elif not search_directories: + # This is the default search path. + search_directories = ('/etc/kernel', + '/usr/lib/kernel') + + for dir in search_directories: + p = Path(dir) / filename + if p.exists(): + return p + return None + + +def uki_conf_location() -> Optional[Path]: + return input_file_location('uki.conf', + '/etc/kernel') + + +def devicetree_config_location() -> Optional[Path]: + return input_file_location('devicetree') + + +def devicetree_file_location(opts) -> Optional[Path]: + # This mirrors the logic in 90-loaderentry.install. Keep in sync. + configfile = devicetree_config_location() + if configfile is None: + return None + + devicetree = configfile.read_text().strip() + if not devicetree: + raise ValueError(f'{configfile!r} is empty') + + path = input_file_location( + devicetree, + f'/usr/lib/firmware/{opts.kernel_version}/device-tree', + f'/usr/lib/linux-image-{opts.kernel_version}', + f'/usr/lib/modules/{opts.kernel_version}/dtb', + ) + if path is None: + raise FileNotFoundError(f'DeviceTree file {devicetree} not found') + return path + + +def kernel_cmdline_base() -> list[str]: + path = input_file_location('cmdline') + if path: + return path.read_text().split() + + # If we read /proc/cmdline, we need to do some additional filtering. + options = Path('/proc/cmdline').read_text().split() + return [opt for opt in options + if not opt.startswith(('BOOT_IMAGE=', 'initrd='))] + + +def kernel_cmdline(opts) -> str: + options = kernel_cmdline_base() + + # If the boot entries are named after the machine ID, then suffix the kernel + # command line with the machine ID we use, so that the machine ID remains + # stable, even during factory reset, in the initrd (where the system's machine + # ID is not directly accessible yet), and if the root file system is volatile. + if (opts.entry_token == opts.machine_id and + not any(opt.startswith('systemd.machine_id=') for opt in options)): + options += [f'systemd.machine_id={opts.machine_id}'] + + # TODO: we unconditionally set the cmdline here, ignoring the setting in + # the config file. Should we not do that? + + # Prepend a space so that '@' does not get misinterpreted + return ' ' + ' '.join(options) + + +def initrd_list(opts) -> list[Path]: + microcode = sorted(opts.staging_area.glob('microcode*')) + initrd = sorted(opts.staging_area.glob('initrd*')) + + # Order taken from 90-loaderentry.install + return [*microcode, *opts.initrd, *initrd] + + +def call_ukify(opts): + # Punish me harder. + # We want this: + # ukify = importlib.machinery.SourceFileLoader('ukify', UKIFY).load_module() + # but it throws a DeprecationWarning. + # https://stackoverflow.com/questions/67631/how-can-i-import-a-module-dynamically-given-the-full-path + # https://github.com/python/cpython/issues/65635 + # offer "explanations", but to actually load a python file without a .py extension, + # the "solution" is 4+ incomprehensible lines. + # The solution with runpy gives a dictionary, which isn't great, but will do. + ukify = runpy.run_path(UKIFY, run_name='ukify') + + # Create "empty" namespace. We want to override just a few settings, so it + # doesn't make sense to configure everything. We pretend to parse an empty + # argument set to prepopulate the namespace with the defaults. + opts2 = ukify['create_parser']().parse_args(['build']) + + opts2.config = uki_conf_location() + opts2.uname = opts.kernel_version + opts2.linux = opts.kernel_image + opts2.initrd = initrd_list(opts) + # Note that 'uki.efi' is the name required by 90-uki-copy.install. + opts2.output = opts.staging_area / 'uki.efi' + + if devicetree := devicetree_file_location(opts): + opts2.devicetree = devicetree + + opts2.cmdline = kernel_cmdline(opts) + if BOOT_STUB: + opts2.stub = BOOT_STUB + + # opts2.summary = True + + ukify['apply_config'](opts2) + ukify['finalize_options'](opts2) + ukify['check_inputs'](opts2) + ukify['make_uki'](opts2) + + log(f'{opts2.output} has been created') + + +def main(): + opts = parse_args() + if opts.command != 'add': + return + if not we_are_wanted(): + return + + call_ukify(opts) + + +if __name__ == '__main__': + main() diff --git a/src/kernel-install/90-loaderentry.install.in b/src/kernel-install/90-loaderentry.install.in new file mode 100755 index 0000000..a52dd81 --- /dev/null +++ b/src/kernel-install/90-loaderentry.install.in @@ -0,0 +1,210 @@ +#!/bin/sh +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd 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 Lesser General Public License +# along with systemd; If not, see <https://www.gnu.org/licenses/>. + +set -e + +COMMAND="${1:?}" +KERNEL_VERSION="${2:?}" +ENTRY_DIR_ABS="${3:?}" +KERNEL_IMAGE="$4" +INITRD_OPTIONS_SHIFT=4 + +[ "$KERNEL_INSTALL_LAYOUT" = "bls" ] || exit 0 + +MACHINE_ID="${KERNEL_INSTALL_MACHINE_ID:?}" +ENTRY_TOKEN="${KERNEL_INSTALL_ENTRY_TOKEN:?}" +BOOT_ROOT="${KERNEL_INSTALL_BOOT_ROOT:?}" + +[ -n "$BOOT_MNT" ] || BOOT_MNT="$(stat -c %m "$BOOT_ROOT")" +if [ "$BOOT_MNT" = '/' ]; then + ENTRY_DIR="$ENTRY_DIR_ABS" +else + ENTRY_DIR="${ENTRY_DIR_ABS#"$BOOT_MNT"}" +fi + +KERNEL_DEST="$ENTRY_DIR_ABS/linux" +KERNEL_ENTRY="$ENTRY_DIR/linux" +LOADER_ENTRY="$BOOT_ROOT/loader/entries/$ENTRY_TOKEN-$KERNEL_VERSION.conf" + +case "$COMMAND" in + remove) + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \ + echo "Removing ${LOADER_ENTRY%.conf}*.conf" + exec rm -f \ + "$LOADER_ENTRY" \ + "${LOADER_ENTRY%.conf}"*".conf" + ;; + add) + ;; + *) + exit 0 + ;; +esac + +if [ -f /etc/os-release ]; then + # shellcheck source=/dev/null + . /etc/os-release +elif [ -f /usr/lib/os-release ]; then + # shellcheck source=/dev/null + . /usr/lib/os-release +fi + +[ -n "$PRETTY_NAME" ] || PRETTY_NAME="Linux $KERNEL_VERSION" + +SORT_KEY="$IMAGE_ID" +[ -z "$SORT_KEY" ] && SORT_KEY="$ID" + +if [ -n "$KERNEL_INSTALL_CONF_ROOT" ]; then + if [ -f "$KERNEL_INSTALL_CONF_ROOT/cmdline" ]; then + BOOT_OPTIONS="$(tr -s "$IFS" ' ' <"$KERNEL_INSTALL_CONF_ROOT/cmdline")" + fi +elif [ -f /etc/kernel/cmdline ]; then + BOOT_OPTIONS="$(tr -s "$IFS" ' ' </etc/kernel/cmdline)" +elif [ -f /usr/lib/kernel/cmdline ]; then + BOOT_OPTIONS="$(tr -s "$IFS" ' ' </usr/lib/kernel/cmdline)" +else + BOOT_OPTIONS="$(tr -s "$IFS" '\n' </proc/cmdline | grep -ve '^BOOT_IMAGE=' -e '^initrd=' | tr '\n' ' ')" +fi + +BOOT_OPTIONS="${BOOT_OPTIONS% }" + +# If the boot entries are named after the machine ID, then suffix the kernel +# command line with the machine ID we use, so that the machine ID remains +# stable, even during factory reset, in the initrd (where the system's machine +# ID is not directly accessible yet), and if the root file system is volatile. +if [ "$ENTRY_TOKEN" = "$MACHINE_ID" ] && ! echo "$BOOT_OPTIONS" | grep -q "systemd.machine_id="; then + BOOT_OPTIONS="$BOOT_OPTIONS systemd.machine_id=$MACHINE_ID" +fi + +TRIES_FILE="${KERNEL_INSTALL_CONF_ROOT:-/etc/kernel}/tries" + +if [ -f "$TRIES_FILE" ]; then + read -r TRIES <"$TRIES_FILE" + if ! echo "$TRIES" | grep -q '^[0-9][0-9]*$'; then + echo "$TRIES_FILE does not contain an integer." >&2 + exit 1 + fi + LOADER_ENTRY="${LOADER_ENTRY%.conf}+$TRIES.conf" +fi + +if ! [ -d "$ENTRY_DIR_ABS" ]; then + echo "Error: entry directory '$ENTRY_DIR_ABS' does not exist" >&2 + exit 1 +fi + +install -m 0644 "$KERNEL_IMAGE" "$KERNEL_DEST" || { + echo "Error: could not copy '$KERNEL_IMAGE' to '$KERNEL_DEST'." >&2 + exit 1 +} +chown root:root "$KERNEL_DEST" || : + +if [ -n "$KERNEL_INSTALL_CONF_ROOT" ]; then + if [ -f "$KERNEL_INSTALL_CONF_ROOT/devicetree" ]; then + read -r DEVICETREE <"$KERNEL_INSTALL_CONF_ROOT/devicetree" + fi +elif [ -f /etc/kernel/devicetree ]; then + read -r DEVICETREE </etc/kernel/devicetree +elif [ -f /usr/lib/kernel/devicetree ]; then + read -r DEVICETREE </usr/lib/kernel/devicetree +fi +if [ -n "$DEVICETREE" ]; then + for prefix in \ + "$KERNEL_INSTALL_CONF_ROOT" \ + "/usr/lib/firmware/$KERNEL_VERSION/device-tree" \ + "/usr/lib/linux-image-$KERNEL_VERSION" \ + "/usr/lib/modules/$KERNEL_VERSION/dtb" + do + [ -n "$prefix" ] || continue + [ -f "$prefix/$DEVICETREE" ] || continue + DEVICETREE_SRC="$prefix/$DEVICETREE" + break + done + + [ -n "$DEVICETREE_SRC" ] || { + echo "Error: could not find device tree blob '$DEVICETREE'." >&2 + exit 1 + } + + DEVICETREE_DEST="$ENTRY_DIR_ABS/${DEVICETREE##*/}" + DEVICETREE_ENTRY="$ENTRY_DIR/${DEVICETREE##*/}" + + install -m 0644 "$DEVICETREE_SRC" "$DEVICETREE_DEST" || { + echo "Error: could not copy '$DEVICETREE_SRC' to '$DEVICETREE_DEST'." >&2 + exit 1 + } + chown root:root "$DEVICETREE_DEST" || : +fi + +shift "$INITRD_OPTIONS_SHIFT" +# All files listed as arguments, and staged files starting with "initrd" are installed as initrds. +for initrd in "${KERNEL_INSTALL_STAGING_AREA}"/microcode* "${@}" "${KERNEL_INSTALL_STAGING_AREA}"/initrd*; do + [ -f "$initrd" ] || { + case "$initrd" in + "${KERNEL_INSTALL_STAGING_AREA}/initrd*" | "${KERNEL_INSTALL_STAGING_AREA}/microcode*") + continue ;; + esac + echo "Error: '$initrd' is not a file." >&2 + exit 1 + } + + initrd_basename="${initrd##*/}" + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Installing $ENTRY_DIR_ABS/$initrd_basename" + install -m 0644 "$initrd" "$ENTRY_DIR_ABS/$initrd_basename" || { + echo "Error: could not copy '$initrd' to '$ENTRY_DIR_ABS/$initrd_basename'." >&2 + exit 1 + } + chown root:root "$ENTRY_DIR_ABS/$initrd_basename" || : +done + +mkdir -p "${LOADER_ENTRY%/*}" || { + echo "Error: could not create loader entry directory '${LOADER_ENTRY%/*}'." >&2 + exit 1 +} + +[ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Creating $LOADER_ENTRY" +{ + echo "# Boot Loader Specification type#1 entry" + echo "# File created by $0 (systemd {{GIT_VERSION}})" + echo "title $PRETTY_NAME" + echo "version $KERNEL_VERSION" + if [ "$ENTRY_TOKEN" = "$MACHINE_ID" ]; then + # See similar logic above for the systemd.machine_id= kernel command line option + echo "machine-id $MACHINE_ID" + fi + [ -n "$SORT_KEY" ] && echo "sort-key $SORT_KEY" + echo "options $BOOT_OPTIONS" + echo "linux $KERNEL_ENTRY" + [ -n "$DEVICETREE_ENTRY" ] && echo "devicetree $DEVICETREE_ENTRY" + + have_initrd= + for initrd in "${KERNEL_INSTALL_STAGING_AREA}"/microcode* "${@}" "${KERNEL_INSTALL_STAGING_AREA}"/initrd*; do + [ -f "$initrd" ] || continue + echo "initrd $ENTRY_DIR/${initrd##*/}" + have_initrd=yes + done + + # Try "initrd", generated by dracut in its kernel-install hook, if no initrds were supplied + [ -z "$have_initrd" ] && [ -f "$ENTRY_DIR_ABS/initrd" ] && echo "initrd $ENTRY_DIR/initrd" + : +} >"$LOADER_ENTRY" || { + echo "Error: could not create loader entry '$LOADER_ENTRY'." >&2 + exit 1 +} +exit 0 diff --git a/src/kernel-install/90-uki-copy.install b/src/kernel-install/90-uki-copy.install new file mode 100755 index 0000000..c66c097 --- /dev/null +++ b/src/kernel-install/90-uki-copy.install @@ -0,0 +1,97 @@ +#!/bin/sh +# -*- mode: shell-script; indent-tabs-mode: nil; sh-basic-offset: 4; -*- +# ex: ts=8 sw=4 sts=4 et filetype=sh +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. +# +# systemd 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 Lesser General Public License +# along with systemd; If not, see <https://www.gnu.org/licenses/>. + +set -e + +COMMAND="${1:?}" +KERNEL_VERSION="${2:?}" +# shellcheck disable=SC2034 +ENTRY_DIR_ABS="$3" +KERNEL_IMAGE="$4" + +[ "$KERNEL_INSTALL_LAYOUT" = "uki" ] || exit 0 + +ENTRY_TOKEN="$KERNEL_INSTALL_ENTRY_TOKEN" +BOOT_ROOT="$KERNEL_INSTALL_BOOT_ROOT" + +UKI_DIR="$BOOT_ROOT/EFI/Linux" + +case "$COMMAND" in + remove) + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && \ + echo "Removing $UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION*.efi" + exec rm -f \ + "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi" \ + "$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+"*".efi" + ;; + add) + ;; + *) + exit 0 + ;; +esac + +if ! [ -d "$UKI_DIR" ]; then + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "creating $UKI_DIR" + mkdir -p "$UKI_DIR" +fi + +TRIES_FILE="${KERNEL_INSTALL_CONF_ROOT:-/etc/kernel}/tries" + +if [ -f "$TRIES_FILE" ]; then + read -r TRIES <"$TRIES_FILE" + if ! echo "$TRIES" | grep -q '^[0-9][0-9]*$'; then + echo "$TRIES_FILE does not contain an integer." >&2 + exit 1 + fi + UKI_FILE="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION+$TRIES.efi" +else + UKI_FILE="$UKI_DIR/$ENTRY_TOKEN-$KERNEL_VERSION.efi" +fi + +# If there is a UKI named uki.efi on the staging area use that, if not use what +# was passed in as $KERNEL_IMAGE but insist it has a .efi extension +if [ -f "$KERNEL_INSTALL_STAGING_AREA/uki.efi" ]; then + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Installing $KERNEL_INSTALL_STAGING_AREA/uki.efi as $UKI_FILE" + install -m 0644 "$KERNEL_INSTALL_STAGING_AREA/uki.efi" "$UKI_FILE" || { + echo "Error: could not copy '$KERNEL_INSTALL_STAGING_AREA/uki.efi' to '$UKI_FILE'." >&2 + exit 1 + } +elif [ -n "$KERNEL_IMAGE" ]; then + [ -f "$KERNEL_IMAGE" ] || { + echo "Error: UKI '$KERNEL_IMAGE' not a file." >&2 + exit 1 + } + [ "$KERNEL_IMAGE" != "${KERNEL_IMAGE%*.efi}.efi" ] && { + echo "Error: $KERNEL_IMAGE is missing .efi suffix." >&2 + exit 1 + } + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "Installing $KERNEL_IMAGE as $UKI_FILE" + install -m 0644 "$KERNEL_IMAGE" "$UKI_FILE" || { + echo "Error: could not copy '$KERNEL_IMAGE' to '$UKI_FILE'." >&2 + exit 1 + } +else + [ "$KERNEL_INSTALL_VERBOSE" -gt 0 ] && echo "No UKI available. Nothing to do." + exit 0 +fi +chown root:root "$UKI_FILE" || : + +exit 0 diff --git a/src/kernel-install/install.conf b/src/kernel-install/install.conf new file mode 100644 index 0000000..4520c01 --- /dev/null +++ b/src/kernel-install/install.conf @@ -0,0 +1,12 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# See kernel-install(8) for details. + +#layout=bls|other|... +#initrd_generator=dracut|... +#uki_generator=ukify|... diff --git a/src/kernel-install/kernel-install.c b/src/kernel-install/kernel-install.c new file mode 100644 index 0000000..14ae1a8 --- /dev/null +++ b/src/kernel-install/kernel-install.c @@ -0,0 +1,1743 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <getopt.h> +#include <stdbool.h> +#include <sys/utsname.h> + +#include "boot-entry.h" +#include "build.h" +#include "chase.h" +#include "conf-files.h" +#include "dirent-util.h" +#include "env-file.h" +#include "env-util.h" +#include "exec-util.h" +#include "fd-util.h" +#include "fileio.h" +#include "find-esp.h" +#include "format-table.h" +#include "fs-util.h" +#include "id128-util.h" +#include "image-policy.h" +#include "kernel-image.h" +#include "main-func.h" +#include "mkdir.h" +#include "mount-util.h" +#include "parse-argument.h" +#include "path-util.h" +#include "pretty-print.h" +#include "recurse-dir.h" +#include "rm-rf.h" +#include "stat-util.h" +#include "string-table.h" +#include "string-util.h" +#include "strv.h" +#include "tmpfile-util.h" +#include "verbs.h" + +static bool arg_verbose = false; +static char *arg_esp_path = NULL; +static char *arg_xbootldr_path = NULL; +static int arg_make_entry_directory = -1; /* tristate */ +static PagerFlags arg_pager_flags = 0; +static JsonFormatFlags arg_json_format_flags = JSON_FORMAT_OFF; +static char *arg_root = NULL; +static char *arg_image = NULL; +static ImagePolicy *arg_image_policy = NULL; +static bool arg_legend = true; + +STATIC_DESTRUCTOR_REGISTER(arg_esp_path, freep); +STATIC_DESTRUCTOR_REGISTER(arg_xbootldr_path, freep); +STATIC_DESTRUCTOR_REGISTER(arg_root, freep); +STATIC_DESTRUCTOR_REGISTER(arg_image_policy, image_policy_freep); + +typedef enum Action { + ACTION_ADD, + ACTION_REMOVE, + ACTION_INSPECT, + _ACTION_MAX, + _ACTION_INVALID = -EINVAL, +} Action; + +typedef enum Layout { + LAYOUT_AUTO, + LAYOUT_UKI, + LAYOUT_BLS, + LAYOUT_OTHER, + _LAYOUT_MAX, + _LAYOUT_INVALID = -EINVAL, +} Layout; + +static const char * const layout_table[_LAYOUT_MAX] = { + [LAYOUT_AUTO] = "auto", + [LAYOUT_UKI] = "uki", + [LAYOUT_BLS] = "bls", + [LAYOUT_OTHER] = "other", +}; + +DEFINE_PRIVATE_STRING_TABLE_LOOKUP(layout, Layout); + +typedef struct Context { + int rfd; + Action action; + sd_id128_t machine_id; + bool machine_id_is_random; + KernelImageType kernel_image_type; + Layout layout; + char *layout_other; + char *conf_root; + char *boot_root; + BootEntryTokenType entry_token_type; + char *entry_token; + char *entry_dir; + char *version; + char *kernel; + char **initrds; + char *initrd_generator; + char *uki_generator; + char *staging_area; + char **plugins; + char **argv; + char **envp; +} Context; + +#define CONTEXT_NULL (Context) { .rfd = -EBADF } + +static void context_done(Context *c) { + assert(c); + + free(c->layout_other); + free(c->conf_root); + free(c->boot_root); + free(c->entry_token); + free(c->entry_dir); + free(c->version); + free(c->kernel); + strv_free(c->initrds); + free(c->initrd_generator); + free(c->uki_generator); + if (c->action == ACTION_INSPECT) + free(c->staging_area); + else + rm_rf_physical_and_free(c->staging_area); + strv_free(c->plugins); + strv_free(c->argv); + strv_free(c->envp); + + safe_close(c->rfd); +} + +static int context_copy(const Context *source, Context *ret) { + int r; + + assert(source); + assert(ret); + assert(source->rfd >= 0 || source->rfd == AT_FDCWD); + + _cleanup_(context_done) Context copy = (Context) { + .rfd = AT_FDCWD, + .action = source->action, + .machine_id = source->machine_id, + .machine_id_is_random = source->machine_id_is_random, + .kernel_image_type = source->kernel_image_type, + .layout = source->layout, + .entry_token_type = source->entry_token_type, + }; + + if (source->rfd >= 0) { + copy.rfd = fd_reopen(source->rfd, O_CLOEXEC|O_DIRECTORY|O_PATH); + if (copy.rfd < 0) + return copy.rfd; + } + + r = strdup_or_null(source->layout_other, ©.layout_other); + if (r < 0) + return r; + r = strdup_or_null(source->conf_root, ©.conf_root); + if (r < 0) + return r; + r = strdup_or_null(source->boot_root, ©.boot_root); + if (r < 0) + return r; + r = strdup_or_null(source->entry_token, ©.entry_token); + if (r < 0) + return r; + r = strdup_or_null(source->entry_dir, ©.entry_dir); + if (r < 0) + return r; + r = strdup_or_null(source->version, ©.version); + if (r < 0) + return r; + r = strdup_or_null(source->kernel, ©.kernel); + if (r < 0) + return r; + r = strv_copy_unless_empty(source->initrds, ©.initrds); + if (r < 0) + return r; + r = strdup_or_null(source->initrd_generator, ©.initrd_generator); + if (r < 0) + return r; + r = strdup_or_null(source->uki_generator, ©.uki_generator); + if (r < 0) + return r; + r = strdup_or_null(source->staging_area, ©.staging_area); + if (r < 0) + return r; + r = strv_copy_unless_empty(source->plugins, ©.plugins); + if (r < 0) + return r; + r = strv_copy_unless_empty(source->argv, ©.argv); + if (r < 0) + return r; + r = strv_copy_unless_empty(source->envp, ©.envp); + if (r < 0) + return r; + + *ret = copy; + copy = CONTEXT_NULL; + + return 0; +} + +static int context_open_root(Context *c) { + int r; + + assert(c); + assert(c->rfd < 0); + + if (isempty(arg_root)) + return 0; + + r = path_is_root(arg_root); + if (r < 0) + return log_error_errno(r, "Failed to determine if '%s' is the root directory: %m", arg_root); + if (r > 0) + return 0; + + c->rfd = open(empty_to_root(arg_root), O_CLOEXEC | O_DIRECTORY | O_PATH); + if (c->rfd < 0) + return log_error_errno(errno, "Failed to open root directory '%s': %m", empty_to_root(arg_root)); + + return 0; +} + +static const char* context_get_layout(const Context *c) { + assert(c); + assert(c->layout >= 0); + + return c->layout_other ?: layout_to_string(c->layout); +} + +static int context_set_layout(Context *c, const char *s, const char *source) { + Layout t; + + assert(c); + assert(source); + + if (c->layout >= 0 || !s) + return 0; + + assert(!c->layout_other); + + t = layout_from_string(s); + if (t >= 0) + c->layout = t; + else if (isempty(s)) + c->layout = LAYOUT_AUTO; + else { + c->layout_other = strdup(s); + if (!c->layout_other) + return log_oom(); + + c->layout = LAYOUT_OTHER; + } + + log_debug("layout=%s set via %s", context_get_layout(c), source); + return 1; +} + +static int context_set_machine_id(Context *c, const char *s, const char *source) { + int r; + + assert(c); + assert(source); + + if (!sd_id128_is_null(c->machine_id) || !s) + return 0; + + r = sd_id128_from_string(s, &c->machine_id); + if (r < 0) + return log_warning_errno(r, "Failed to parse machine ID specified via %s, ignoring.", source); + + if (sd_id128_is_null(c->machine_id)) + return 0; + + log_debug("MACHINE_ID=%s set via %s.", SD_ID128_TO_STRING(c->machine_id), source); + return 1; +} + +static int context_set_string(const char *s, const char *source, const char *name, char **dest) { + char *p; + + assert(source); + assert(name); + assert(dest); + + if (*dest || !s) + return 0; + + p = strdup(s); + if (!p) + return log_oom(); + + log_debug("%s (%s) set via %s.", name, p, source); + + *dest = p; + return 1; +} + +static int context_set_initrd_generator(Context *c, const char *s, const char *source) { + assert(c); + return context_set_string(s, source, "INITRD_GENERATOR", &c->initrd_generator); +} + +static int context_set_uki_generator(Context *c, const char *s, const char *source) { + assert(c); + return context_set_string(s, source, "UKI_GENERATOR", &c->uki_generator); +} + +static int context_set_version(Context *c, const char *s) { + assert(c); + + if (s && !filename_is_valid(s)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Invalid version specified: %s", s); + + return context_set_string(s, "command line", "kernel version", &c->version); +} + +static int context_set_path(Context *c, const char *s, const char *source, const char *name, char **dest) { + char *p; + int r; + + assert(c); + assert(source); + assert(name); + assert(dest); + + if (*dest || !s) + return 0; + + if (c->rfd >= 0) { + r = chaseat(c->rfd, s, CHASE_AT_RESOLVE_IN_ROOT, &p, /* ret_fd = */ NULL); + if (r < 0) + return log_warning_errno(r, "Failed to chase path %s for %s specified via %s, ignoring: %m", + s, name, source); + } else { + r = path_make_absolute_cwd(s, &p); + if (r < 0) + return log_warning_errno(r, "Failed to make path '%s' for %s specified via %s absolute, ignoring: %m", + s, name, source); + } + + log_debug("%s (%s) set via %s.", name, p, source); + + *dest = p; + return 1; +} + +static int context_set_boot_root(Context *c, const char *s, const char *source) { + assert(c); + return context_set_path(c, s, source, "BOOT_ROOT", &c->boot_root); +} + +static int context_set_conf_root(Context *c, const char *s, const char *source) { + assert(c); + return context_set_path(c, s, source, "CONF_ROOT", &c->conf_root); +} + +static int context_set_kernel(Context *c, const char *s) { + assert(c); + return context_set_path(c, s, "command line", "kernel image file", &c->kernel); +} + +static int context_set_path_strv(Context *c, char* const* strv, const char *source, const char *name, char ***dest) { + _cleanup_strv_free_ char **w = NULL; + int r; + + assert(c); + assert(source); + assert(name); + assert(dest); + + if (*dest) + return 0; + + STRV_FOREACH(s, strv) { + char *p; + + if (c->rfd >= 0) { + r = chaseat(c->rfd, *s, CHASE_AT_RESOLVE_IN_ROOT, &p, /* ret_fd = */ NULL); + if (r < 0) + return log_warning_errno(r, "Failed to chase path %s for %s specified via %s: %m", + *s, name, source); + } else { + r = path_make_absolute_cwd(*s, &p); + if (r < 0) + return log_warning_errno(r, "Failed to make path '%s' for %s specified via %s absolute, ignoring: %m", + *s, name, source); + } + r = strv_consume(&w, p); + if (r < 0) + return log_oom(); + } + + if (strv_isempty(w)) + return 0; + + log_debug("%s set via %s", name, source); + + *dest = TAKE_PTR(w); + return 1; +} + +static int context_set_plugins(Context *c, const char *s, const char *source) { + _cleanup_strv_free_ char **v = NULL; + + assert(c); + + if (c->plugins || !s) + return 0; + + v = strv_split(s, NULL); + if (!v) + return log_oom(); + + return context_set_path_strv(c, v, source, "plugins", &c->plugins); +} + +static int context_set_initrds(Context *c, char* const* strv) { + assert(c); + return context_set_path_strv(c, strv, "command line", "initrds", &c->initrds); +} + +static int context_load_environment(Context *c) { + assert(c); + + (void) context_set_machine_id(c, getenv("MACHINE_ID"), "environment"); + (void) context_set_boot_root(c, getenv("BOOT_ROOT"), "environment"); + (void) context_set_conf_root(c, getenv("KERNEL_INSTALL_CONF_ROOT"), "environment"); + (void) context_set_plugins(c, getenv("KERNEL_INSTALL_PLUGINS"), "environment"); + return 0; +} + +static int context_ensure_conf_root(Context *c) { + int r; + + assert(c); + + if (c->conf_root) + return 0; + + r = chaseat(c->rfd, "/etc/kernel", CHASE_AT_RESOLVE_IN_ROOT, &c->conf_root, /* ret_fd = */ NULL); + if (r < 0) + log_debug_errno(r, "Failed to chase /etc/kernel, ignoring: %m"); + + return 0; +} + +static int context_load_install_conf_one(Context *c, const char *path) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char + *conf = NULL, *machine_id = NULL, *boot_root = NULL, *layout = NULL, + *initrd_generator = NULL, *uki_generator = NULL; + int r; + + assert(c); + assert(path); + + conf = path_join(path, "install.conf"); + if (!conf) + return log_oom(); + + r = chase_and_fopenat_unlocked(c->rfd, conf, CHASE_AT_RESOLVE_IN_ROOT, "re", NULL, &f); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to chase %s: %m", conf); + + log_debug("Loading %s…", conf); + + r = parse_env_file(f, conf, + "MACHINE_ID", &machine_id, + "BOOT_ROOT", &boot_root, + "layout", &layout, + "initrd_generator", &initrd_generator, + "uki_generator", &uki_generator); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s': %m", conf); + + (void) context_set_machine_id(c, machine_id, conf); + (void) context_set_boot_root(c, boot_root, conf); + (void) context_set_layout(c, layout, conf); + (void) context_set_initrd_generator(c, initrd_generator, conf); + (void) context_set_uki_generator(c, uki_generator, conf); + + log_debug("Loaded %s.", conf); + return 1; +} + +static int context_load_install_conf(Context *c) { + int r; + + assert(c); + + if (c->conf_root) { + r = context_load_install_conf_one(c, c->conf_root); + if (r != 0) + return r; + } + + STRV_FOREACH(p, STRV_MAKE("/etc/kernel", "/usr/lib/kernel")) { + r = context_load_install_conf_one(c, *p); + if (r != 0) + return r; + } + + return 0; +} + +static int context_load_machine_info(Context *c) { + _cleanup_fclose_ FILE *f = NULL; + _cleanup_free_ char *machine_id = NULL, *layout = NULL; + static const char *path = "/etc/machine-info"; + int r; + + assert(c); + + /* If the user configured an explicit machine ID in /etc/machine-info to use for our purpose, we'll + * use that instead (for compatibility). */ + + if (!sd_id128_is_null(c->machine_id) && c->layout >= 0) + return 0; + + /* For testing. To make not read host's /etc/machine-info. */ + r = getenv_bool("KERNEL_INSTALL_READ_MACHINE_INFO"); + if (r < 0 && r != -ENXIO) + log_warning_errno(r, "Failed to read $KERNEL_INSTALL_READ_MACHINE_INFO, assuming yes: %m"); + if (r == 0) { + log_debug("Skipping to read /etc/machine-info."); + return 0; + } + + r = chase_and_fopenat_unlocked(c->rfd, path, CHASE_AT_RESOLVE_IN_ROOT, "re", NULL, &f); + if (r == -ENOENT) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to chase %s: %m", path); + + log_debug("Loading %s…", path); + + r = parse_env_file(f, path, + "KERNEL_INSTALL_MACHINE_ID", &machine_id, + "KERNEL_INSTALL_LAYOUT", &layout); + if (r < 0) + return log_error_errno(r, "Failed to parse '%s': %m", path); + + (void) context_set_machine_id(c, machine_id, path); + (void) context_set_layout(c, layout, path); + return 0; +} + +static int context_load_machine_id(Context *c) { + int r; + + assert(c); + + r = id128_get_machine_at(c->rfd, &c->machine_id); + if (ERRNO_IS_NEG_MACHINE_ID_UNSET(r)) + return 0; + if (r < 0) + return log_error_errno(r, "Failed to load machine ID from /etc/machine-id: %m"); + + log_debug("MACHINE_ID=%s set via /etc/machine-id.", SD_ID128_TO_STRING(c->machine_id)); + return 1; /* loaded */ +} + +static int context_ensure_machine_id(Context *c) { + int r; + + assert(c); + + if (!sd_id128_is_null(c->machine_id)) + return 0; + + /* If /etc/machine-id is initialized we'll use it. */ + r = context_load_machine_id(c); + if (r != 0) + return r; + + /* Otherwise we'll use a freshly generated one. */ + r = sd_id128_randomize(&c->machine_id); + if (r < 0) + return log_error_errno(r, "Failed to generate random ID: %m"); + + c->machine_id_is_random = true; + log_debug("New machine ID '%s' generated.", SD_ID128_TO_STRING(c->machine_id)); + return 0; +} + +static int context_acquire_xbootldr(Context *c) { + int r; + + assert(c); + assert(!c->boot_root); + + r = find_xbootldr_and_warn_at( + /* rfd = */ c->rfd, + /* path = */ arg_xbootldr_path, + /* unprivileged_mode= */ -1, + /* ret_path = */ &c->boot_root, + /* ret_uuid = */ NULL, + /* ret_devid = */ NULL); + if (r == -ENOKEY) { + log_debug_errno(r, "Couldn't find an XBOOTLDR partition."); + return 0; + } + if (r == -EACCES && geteuid() != 0) + return log_error_errno(r, "Failed to determine XBOOTLDR partition: %m"); + if (r < 0) + return r; + + log_debug("Using XBOOTLDR partition at %s as $BOOT_ROOT.", c->boot_root); + return 1; /* found */ +} + +static int context_acquire_esp(Context *c) { + int r; + + assert(c); + assert(!c->boot_root); + + r = find_esp_and_warn_at( + /* rfd = */ c->rfd, + /* path = */ arg_esp_path, + /* unprivileged_mode= */ -1, + /* ret_path = */ &c->boot_root, + /* ret_part = */ NULL, + /* ret_pstart = */ NULL, + /* ret_psize = */ NULL, + /* ret_uuid = */ NULL, + /* ret_devid = */ NULL); + if (r == -ENOKEY) { + log_debug_errno(r, "Couldn't find EFI system partition, ignoring."); + return 0; + } + if (r == -EACCES && geteuid() != 0) + return log_error_errno(r, "Failed to determine EFI system partition: %m"); + if (r < 0) + return r; + + log_debug("Using EFI System Partition at %s as $BOOT_ROOT.", c->boot_root); + return 1; /* found */ +} + +static int context_ensure_boot_root(Context *c) { + int r; + + assert(c); + + /* If BOOT_ROOT is specified via environment or install.conf, then use it. */ + if (c->boot_root) + return 0; + + /* Otherwise, use XBOOTLDR partition, if mounted. */ + r = context_acquire_xbootldr(c); + if (r != 0) + return r; + + /* Otherwise, use EFI system partition, if mounted. */ + r = context_acquire_esp(c); + if (r != 0) + return r; + + /* If all else fails, use /boot. */ + if (c->rfd >= 0) { + r = chaseat(c->rfd, "/boot", CHASE_AT_RESOLVE_IN_ROOT, &c->boot_root, /* ret_fd = */ NULL); + if (r < 0) + return log_error_errno(r, "Failed to chase '/boot': %m"); + } else { + c->boot_root = strdup("/boot"); + if (!c->boot_root) + return log_oom(); + } + + log_debug("KERNEL_INSTALL_BOOT_ROOT autodetection yielded no candidates, using \"%s\".", c->boot_root); + return 0; +} + +static int context_ensure_entry_token(Context *c) { + int r; + + assert(c); + + /* Now that we determined the machine ID to use, let's determine the "token" for the boot loader + * entry to generate. We use that for naming the directory below $BOOT where we want to place the + * kernel/initrd and related resources, as well for naming the .conf boot loader spec entry. + * Typically this is just the machine ID, but it can be anything else, too, if we are told so. */ + + r = boot_entry_token_ensure_at( + c->rfd, + c->conf_root, + c->machine_id, + c->machine_id_is_random, + &c->entry_token_type, + &c->entry_token); + if (r < 0) + return r; + + log_debug("Using entry token: %s", c->entry_token); + return 0; +} + +static int context_load_plugins(Context *c) { + int r; + + assert(c); + + if (c->plugins) + return 0; + + r = conf_files_list_strv_at( + &c->plugins, + ".install", + c->rfd, + CONF_FILES_EXECUTABLE | CONF_FILES_REGULAR | CONF_FILES_FILTER_MASKED, + STRV_MAKE_CONST("/etc/kernel/install.d", "/usr/lib/kernel/install.d")); + if (r < 0) + return log_error_errno(r, "Failed to find plugins: %m"); + + return 0; +} + +static int context_init(Context *c) { + int r; + + assert(c); + + r = context_open_root(c); + if (r < 0) + return r; + + r = context_load_environment(c); + if (r < 0) + return r; + + r = context_ensure_conf_root(c); + if (r < 0) + return r; + + r = context_load_install_conf(c); + if (r < 0) + return r; + + r = context_load_machine_info(c); + if (r < 0) + return r; + + r = context_ensure_machine_id(c); + if (r < 0) + return r; + + r = context_ensure_boot_root(c); + if (r < 0) + return r; + + r = context_ensure_entry_token(c); + if (r < 0) + return r; + + r = context_load_plugins(c); + if (r < 0) + return r; + + return 0; +} + +static int context_inspect_kernel(Context *c) { + assert(c); + + if (!c->kernel) + return 0; + + return inspect_kernel(c->rfd, c->kernel, &c->kernel_image_type, NULL, NULL, NULL); +} + +static int context_ensure_layout(Context *c) { + int r; + + assert(c); + assert(c->boot_root); + assert(c->entry_token); + + if (c->layout >= 0 && c->layout != LAYOUT_AUTO) + return 0; + + /* No layout configured by the administrator. Let's try to figure it out automatically from metadata + * already contained in $BOOT_ROOT. */ + + if (c->kernel_image_type == KERNEL_IMAGE_TYPE_UKI) { + c->layout = LAYOUT_UKI; + log_debug("Kernel image type is %s, using layout=%s.", + kernel_image_type_to_string(c->kernel_image_type), layout_to_string(c->layout)); + return 0; + } + + _cleanup_free_ char *srel_path = path_join(c->boot_root, "loader/entries.srel"); + if (!srel_path) + return log_oom(); + + _cleanup_free_ char *srel = NULL; + r = read_one_line_file_at(c->rfd, srel_path, &srel); + if (r >= 0) { + if (streq(srel, "type1")) + /* The loader/entries.srel file clearly indicates that the installed boot loader + * implements the proper standard upstream boot loader spec for Type #1 entries. + * Let's default to that, then. */ + c->layout = LAYOUT_BLS; + else + /* The loader/entries.srel file indicates some other spec is implemented and owns the + * /loader/entries/ directory. Since we have no idea what that means, let's stay away + * from it by default. */ + c->layout = LAYOUT_OTHER; + + log_debug("%s with '%s' found, using layout=%s.", srel_path, srel, layout_to_string(c->layout)); + return 0; + } else if (r != -ENOENT) + return log_error_errno(r, "Failed to read %s: %m", srel_path); + + _cleanup_free_ char *entry_token_path = path_join(c->boot_root, c->entry_token); + if (!entry_token_path) + return log_oom(); + + r = is_dir_full(c->rfd, entry_token_path, /* follow = */ false); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to check if '%s' is a directory: %m", entry_token_path); + if (r > 0) { + /* If the metadata in $BOOT_ROOT doesn't tell us anything, then check if the entry token + * directory already exists. If so, let's assume it's the standard boot loader spec, too. */ + c->layout = LAYOUT_BLS; + log_debug("%s exists, using layout=%s.", entry_token_path, layout_to_string(c->layout)); + return 0; + } + + /* There's no metadata in $BOOT_ROOT, and apparently no entry token directory installed? Then we + * really don't know anything. */ + c->layout = LAYOUT_OTHER; + log_debug("Entry-token directory not found, using layout=%s.", layout_to_string(c->layout)); + return 0; +} + +static int context_set_up_staging_area(Context *c) { + static const char *template = "/tmp/kernel-install.staging.XXXXXX"; + int r; + + assert(c); + + if (c->staging_area) + return 0; + + if (c->action == ACTION_INSPECT) { + /* This is only used for display. The directory will not be created. */ + c->staging_area = strdup(template); + if (!c->staging_area) + return log_oom(); + } else { + r = mkdtemp_malloc(template, &c->staging_area); + if (r < 0) + return log_error_errno(r, "Failed to create staging area: %m"); + } + + return 0; +} + +static int context_build_entry_dir(Context *c) { + assert(c); + assert(c->boot_root); + assert(c->entry_token); + assert(c->version || c->action == ACTION_INSPECT); + + if (c->entry_dir) + return 0; + + c->entry_dir = path_join(c->boot_root, c->entry_token, c->version ?: "KERNEL_VERSION"); + if (!c->entry_dir) + return log_oom(); + + log_debug("Using ENTRY_DIR=%s", c->entry_dir); + return 0; +} + +static bool context_should_make_entry_dir(Context *c) { + assert(c); + + /* Compatibility with earlier versions that used the presence of $BOOT_ROOT/$ENTRY_TOKEN to signal to + * 00-entry-directory to create $ENTRY_DIR to serve as the indication to use or to not use the BLS */ + + if (arg_make_entry_directory < 0) + return c->layout == LAYOUT_BLS; + + return arg_make_entry_directory; +} + +static int context_make_entry_dir(Context *c) { + _cleanup_close_ int fd = -EBADF; + + assert(c); + assert(c->entry_dir); + + if (c->action != ACTION_ADD) + return 0; + + if (!context_should_make_entry_dir(c)) + return 0; + + log_debug("mkdir -p %s", c->entry_dir); + fd = chase_and_openat(c->rfd, c->entry_dir, CHASE_AT_RESOLVE_IN_ROOT | CHASE_MKDIR_0755, + O_CLOEXEC | O_CREAT | O_DIRECTORY | O_PATH, NULL); + if (fd < 0) + return log_error_errno(fd, "Failed to make directory '%s': %m", c->entry_dir); + + return 0; +} + +static int context_remove_entry_dir(Context *c) { + _cleanup_free_ char *p = NULL; + _cleanup_close_ int fd = -EBADF; + struct stat st; + int r; + + assert(c); + assert(c->entry_dir); + + if (c->action != ACTION_REMOVE) + return 0; + + if (!context_should_make_entry_dir(c)) + return 0; + + log_debug("rm -rf %s", c->entry_dir); + fd = chase_and_openat(c->rfd, c->entry_dir, CHASE_AT_RESOLVE_IN_ROOT, O_CLOEXEC | O_DIRECTORY, &p); + if (fd < 0) { + if (IN_SET(fd, -ENOTDIR, -ENOENT)) + return 0; + return log_debug_errno(fd, "Failed to chase and open %s, ignoring: %m", c->entry_dir); + } + + if (fstat(fd, &st) < 0) + return log_debug_errno(errno, "Failed to stat %s: %m", p); + + r = rm_rf_children(TAKE_FD(fd), REMOVE_PHYSICAL|REMOVE_MISSING_OK|REMOVE_CHMOD, &st); + if (r < 0) + log_debug_errno(r, "Failed to remove children of %s, ignoring: %m", p); + + if (unlinkat(c->rfd, p, AT_REMOVEDIR) < 0) + log_debug_errno(errno, "Failed to remove %s, ignoring: %m", p); + + return 0; +} + +static int context_build_arguments(Context *c) { + _cleanup_strv_free_ char **a = NULL; + const char *verb; + int r; + + assert(c); + assert(c->entry_dir); + + if (c->argv) + return 0; + + switch (c->action) { + case ACTION_ADD: + assert(c->version); + assert(c->kernel); + verb = "add"; + break; + + case ACTION_REMOVE: + assert(c->version); + assert(!c->kernel); + assert(!c->initrds); + verb = "remove"; + break; + + case ACTION_INSPECT: + verb = "add|remove"; + break; + + default: + assert_not_reached(); + } + + a = strv_new("dummy-arg", /* to make strv_free() works for this variable. */ + verb, + c->version ?: "KERNEL_VERSION", + c->entry_dir); + if (!a) + return log_oom(); + + if (c->action == ACTION_ADD) { + r = strv_extend(&a, c->kernel); + if (r < 0) + return log_oom(); + + r = strv_extend_strv(&a, c->initrds, /* filter_duplicates = */ false); + if (r < 0) + return log_oom(); + + } else if (c->action == ACTION_INSPECT) { + r = strv_extend(&a, c->kernel ?: "[KERNEL_IMAGE]"); + if (r < 0) + return log_oom(); + + r = strv_extend(&a, "[INITRD...]"); + if (r < 0) + return log_oom(); + } + + c->argv = TAKE_PTR(a); + return 0; +} + +static int context_build_environment(Context *c) { + _cleanup_strv_free_ char **e = NULL; + int r; + + assert(c); + + if (c->envp) + return 0; + + r = strv_env_assign_many(&e, + "LC_COLLATE", SYSTEMD_DEFAULT_LOCALE, + "KERNEL_INSTALL_VERBOSE", one_zero(arg_verbose), + "KERNEL_INSTALL_IMAGE_TYPE", kernel_image_type_to_string(c->kernel_image_type), + "KERNEL_INSTALL_MACHINE_ID", SD_ID128_TO_STRING(c->machine_id), + "KERNEL_INSTALL_ENTRY_TOKEN", c->entry_token, + "KERNEL_INSTALL_BOOT_ROOT", c->boot_root, + "KERNEL_INSTALL_LAYOUT", context_get_layout(c), + "KERNEL_INSTALL_INITRD_GENERATOR", strempty(c->initrd_generator), + "KERNEL_INSTALL_UKI_GENERATOR", strempty(c->uki_generator), + "KERNEL_INSTALL_STAGING_AREA", c->staging_area); + if (r < 0) + return log_error_errno(r, "Failed to build environment variables for plugins: %m"); + + c->envp = TAKE_PTR(e); + return 0; +} + +static int context_prepare_execution(Context *c) { + int r; + + assert(c); + + r = context_inspect_kernel(c); + if (r < 0) + return r; + + r = context_ensure_layout(c); + if (r < 0) + return r; + + r = context_set_up_staging_area(c); + if (r < 0) + return r; + + r = context_build_entry_dir(c); + if (r < 0) + return r; + + r = context_build_arguments(c); + if (r < 0) + return r; + + r = context_build_environment(c); + if (r < 0) + return r; + + return 0; +} + +static int context_execute(Context *c) { + int r, ret; + + assert(c); + + r = context_make_entry_dir(c); + if (r < 0) + return r; + + if (DEBUG_LOGGING) { + _cleanup_free_ char *x = strv_join_full(c->plugins, "", "\n ", /* escape_separator = */ false); + log_debug("Using plugins: %s", strna(x)); + + _cleanup_free_ char *y = strv_join_full(c->envp, "", "\n ", /* escape_separator = */ false); + log_debug("Plugin environment: %s", strna(y)); + + _cleanup_free_ char *z = strv_join(strv_skip(c->argv, 1), " "); + log_debug("Plugin arguments: %s", strna(z)); + } + + ret = execute_strv( + /* name = */ NULL, + c->plugins, + /* root = */ NULL, + USEC_INFINITY, + /* callbacks = */ NULL, + /* callback_args = */ NULL, + c->argv, + c->envp, + EXEC_DIR_SKIP_REMAINING); + + r = context_remove_entry_dir(c); + if (r < 0) + return r; + + /* This returns 0 on success, positive exit code on plugin failure, negative errno on other failures. */ + return ret; +} + +static bool bypass(void) { + int r; + + r = getenv_bool("KERNEL_INSTALL_BYPASS"); + if (r < 0 && r != -ENXIO) + log_debug_errno(r, "Failed to parse $KERNEL_INSTALL_BYPASS, assuming no."); + if (r <= 0) + return false; + + log_debug("$KERNEL_INSTALL_BYPASS is enabled, skipping execution."); + return true; +} + +static int do_add( + Context *c, + const char *version, + const char *kernel, + char **initrds) { + + int r; + + assert(c); + assert(version); + assert(kernel); + + r = context_set_version(c, version); + if (r < 0) + return r; + + r = context_set_kernel(c, kernel); + if (r < 0) + return r; + + r = context_set_initrds(c, initrds); + if (r < 0) + return r; + + r = context_prepare_execution(c); + if (r < 0) + return r; + + return context_execute(c); +} + +static int kernel_from_version(const char *version, char **ret_kernel) { + _cleanup_free_ char *vmlinuz = NULL; + int r; + + assert(version); + + vmlinuz = path_join("/usr/lib/modules/", version, "/vmlinuz"); + if (!vmlinuz) + return log_oom(); + + r = laccess(vmlinuz, F_OK); + if (r < 0) { + if (r == -ENOENT) + return log_error_errno(r, "Kernel image not installed to '%s', requiring manual kernel image path specification.", vmlinuz); + + return log_error_errno(r, "Failed to determine if kernel image is installed to '%s': %m", vmlinuz); + } + + *ret_kernel = TAKE_PTR(vmlinuz); + return 0; +} + +static int verb_add(int argc, char *argv[], void *userdata) { + Context *c = ASSERT_PTR(userdata); + _cleanup_free_ char *vmlinuz = NULL; + const char *version, *kernel; + char **initrds; + struct utsname un; + int r; + + assert(argv); + + if (arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "'add' does not support --root=."); + + if (bypass()) + return 0; + + c->action = ACTION_ADD; + + /* We use the same order of arguments that "inspect" introduced, i.e. if only on argument is + * specified we take it as the kernel path, not the version, i.e. it's the first argument that is + * optional, not the 2nd. */ + version = argc > 2 ? empty_or_dash_to_null(argv[1]) : NULL; + kernel = argc > 2 ? empty_or_dash_to_null(argv[2]) : + (argc > 1 ? empty_or_dash_to_null(argv[1]) : NULL); + initrds = strv_skip(argv, 3); + + if (!version) { + assert_se(uname(&un) >= 0); + version = un.release; + } + + if (!kernel) { + r = kernel_from_version(version, &vmlinuz); + if (r < 0) + return r; + + kernel = vmlinuz; + } + + return do_add(c, version, kernel, initrds); +} + +static int verb_add_all(int argc, char *argv[], void *userdata) { + Context *c = ASSERT_PTR(userdata); + _cleanup_close_ int fd = -EBADF; + size_t n = 0; + int ret = 0, r; + + assert(argv); + + if (bypass()) + return 0; + + c->action = ACTION_ADD; + + fd = open("/usr/lib/modules", O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(fd, "Failed to open /usr/lib/modules/: %m"); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m"); + + FOREACH_ARRAY(d, de->entries, de->n_entries) { + + _cleanup_free_ char *j = path_join("/usr/lib/modules/", (*d)->d_name); + if (!j) + return log_oom(); + + r = dirent_ensure_type(fd, *d); + if (r < 0) { + if (r != -ENOENT) /* don't log if just gone by now */ + log_debug_errno(r, "Failed to check if '%s' is a directory, ignoring: %m", j); + continue; + } + + if ((*d)->d_type != DT_DIR) + continue; + + _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz"); + if (!fn) + return log_oom(); + + if (faccessat(fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to check if '/usr/lib/modules/%s/vmlinuz' exists, ignoring: %m", (*d)->d_name); + + log_notice("Not adding version '%s', because kernel image not found.", (*d)->d_name); + continue; + } + + _cleanup_(context_done) Context copy = CONTEXT_NULL; + + r = context_copy(c, ©); + if (r < 0) + return log_error_errno(r, "Failed to copy execution context: %m"); + + _cleanup_free_ char *full = path_join("/usr/lib/modules/", fn); + if (!full) + return log_oom(); + + r = do_add(©, + /* version= */ (*d)->d_name, + /* kernel= */ full, + /* initrds= */ NULL); + if (r == 0) + n++; + else if (ret == 0) + ret = r; + } + + if (n > 0) + log_debug("Installed %zu kernel(s).", n); + else if (ret == 0) + ret = log_error_errno(SYNTHETIC_ERRNO(ENOENT), "No kernels to install found."); + + return ret; +} + +static int run_as_installkernel(int argc, char *argv[], Context *c) { + /* kernel's install.sh invokes us as + * /sbin/installkernel <version> <vmlinuz> <map> <installation-dir> + * We ignore the last two arguments. */ + if (optind + 2 > argc) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "'installkernel' command requires at least two arguments."); + + return verb_add(3, STRV_MAKE("add", argv[optind], argv[optind+1]), c); +} + +static int verb_remove(int argc, char *argv[], void *userdata) { + Context *c = ASSERT_PTR(userdata); + int r; + + assert(argc >= 2); + assert(argv); + + if (arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "'remove' does not support --root=."); + + if (argc > 2) + log_debug("Too many arguments specified. 'kernel-install remove' takes only kernel version. " + "Ignoring residual arguments."); + + if (bypass()) + return 0; + + c->action = ACTION_REMOVE; + + /* Note, we do not automatically derive the kernel version to remove from uname() here (unlike we do + * it for the "add" verb), since we don't want to make it too easy to uninstall your running + * kernel, as a safety precaution */ + + r = context_set_version(c, argv[1]); + if (r < 0) + return r; + + r = context_prepare_execution(c); + if (r < 0) + return r; + + return context_execute(c); +} + +static int verb_inspect(int argc, char *argv[], void *userdata) { + Context *c = ASSERT_PTR(userdata); + _cleanup_(table_unrefp) Table *t = NULL; + _cleanup_free_ char *vmlinuz = NULL; + const char *version, *kernel; + char **initrds; + struct utsname un; + int r; + + c->action = ACTION_INSPECT; + + /* When only a single parameter is specified 'inspect' it's the kernel image path, and not the kernel + * version. i.e. it's the first argument that is optional, not the 2nd. That's a bit unfortunate, but + * we keep the behaviour for compatibility. If users want to specify only the version (and have the + * kernel image path derived automatically), then they may specify an empty string or "dash" as + * kernel image path. */ + version = argc > 2 ? empty_or_dash_to_null(argv[1]) : NULL; + kernel = argc > 2 ? empty_or_dash_to_null(argv[2]) : + (argc > 1 ? empty_or_dash_to_null(argv[1]) : NULL); + initrds = strv_skip(argv, 3); + + if (!version && !arg_root) { + assert_se(uname(&un) >= 0); + version = un.release; + } + + if (!kernel && version) { + r = kernel_from_version(version, &vmlinuz); + if (r < 0) + return r; + + kernel = vmlinuz; + } + + r = context_set_version(c, version); + if (r < 0) + return r; + + r = context_set_kernel(c, kernel); + if (r < 0) + return r; + + r = context_set_initrds(c, initrds); + if (r < 0) + return r; + + r = context_prepare_execution(c); + if (r < 0) + return r; + + t = table_new_vertical(); + if (!t) + return log_oom(); + + r = table_add_many(t, + TABLE_FIELD, "Machine ID", + TABLE_ID128, c->machine_id, + TABLE_FIELD, "Kernel Image Type", + TABLE_STRING, kernel_image_type_to_string(c->kernel_image_type), + TABLE_FIELD, "Layout", + TABLE_STRING, context_get_layout(c), + TABLE_FIELD, "Boot Root", + TABLE_STRING, c->boot_root, + TABLE_FIELD, "Entry Token Type", + TABLE_STRING, boot_entry_token_type_to_string(c->entry_token_type), + TABLE_FIELD, "Entry Token", + TABLE_STRING, c->entry_token, + TABLE_FIELD, "Entry Directory", + TABLE_STRING, c->entry_dir, + TABLE_FIELD, "Kernel Version", + TABLE_STRING, c->version, + TABLE_FIELD, "Kernel", + TABLE_STRING, c->kernel, + TABLE_FIELD, "Initrds", + TABLE_STRV, c->initrds, + TABLE_FIELD, "Initrd Generator", + TABLE_STRING, c->initrd_generator, + TABLE_FIELD, "UKI Generator", + TABLE_STRING, c->uki_generator, + TABLE_FIELD, "Plugins", + TABLE_STRV, c->plugins, + TABLE_FIELD, "Plugin Environment", + TABLE_STRV, c->envp); + if (r < 0) + return table_log_add_error(r); + + if (arg_json_format_flags & JSON_FORMAT_OFF) { + r = table_add_many(t, + TABLE_FIELD, "Plugin Arguments", + TABLE_STRV, strv_skip(c->argv, 1)); + if (r < 0) + return table_log_add_error(r); + } + + table_set_ersatz_string(t, TABLE_ERSATZ_UNSET); + + for (size_t row = 1; row < table_get_rows(t); row++) { + _cleanup_free_ char *name = NULL; + + name = strdup(table_get_at(t, row, 0)); + if (!name) + return log_oom(); + + r = table_set_json_field_name(t, row - 1, delete_chars(name, " ")); + if (r < 0) + return log_error_errno(r, "Failed to set JSON field name: %m"); + } + + return table_print_with_pager(t, arg_json_format_flags, arg_pager_flags, /* show_header= */ false); +} + +static int verb_list(int argc, char *argv[], void *userdata) { + _cleanup_close_ int fd = -EBADF; + int r; + + fd = open("/usr/lib/modules", O_DIRECTORY|O_RDONLY|O_CLOEXEC); + if (fd < 0) + return log_error_errno(fd, "Failed to open /usr/lib/modules/: %m"); + + _cleanup_free_ DirectoryEntries *de = NULL; + r = readdir_all(fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de); + if (r < 0) + return log_error_errno(r, "Failed to numerate /usr/lib/modules/ contents: %m"); + + _cleanup_(table_unrefp) Table *table = NULL; + table = table_new("version", "has kernel", "path"); + if (!table) + return log_oom(); + + table_set_ersatz_string(table, TABLE_ERSATZ_DASH); + table_set_align_percent(table, table_get_cell(table, 0, 1), 100); + + FOREACH_ARRAY(d, de->entries, de->n_entries) { + + _cleanup_free_ char *j = path_join("/usr/lib/modules/", (*d)->d_name); + if (!j) + return log_oom(); + + r = dirent_ensure_type(fd, *d); + if (r < 0) { + if (r != -ENOENT) /* don't log if just gone by now */ + log_debug_errno(r, "Failed to check if '%s' is a directory, ignoring: %m", j); + continue; + } + + if ((*d)->d_type != DT_DIR) + continue; + + _cleanup_free_ char *fn = path_join((*d)->d_name, "vmlinuz"); + if (!fn) + return log_oom(); + + bool exists; + if (faccessat(fd, fn, F_OK, AT_SYMLINK_NOFOLLOW) < 0) { + if (errno != ENOENT) + log_debug_errno(errno, "Failed to check if '/usr/lib/modules/%s/vmlinuz' exists, ignoring: %m", (*d)->d_name); + + exists = false; + } else + exists = true; + + r = table_add_many(table, + TABLE_STRING, (*d)->d_name, + TABLE_BOOLEAN_CHECKMARK, exists, + TABLE_SET_COLOR, ansi_highlight_green_red(exists), + TABLE_PATH, j); + if (r < 0) + return table_log_add_error(r); + } + + return table_print_with_pager(table, arg_json_format_flags, arg_pager_flags, arg_legend); +} + +static int help(void) { + _cleanup_free_ char *link = NULL; + int r; + + r = terminal_urlify_man("kernel-install", "8", &link); + if (r < 0) + return log_oom(); + + printf("%1$s [OPTIONS...] COMMAND ...\n\n" + "%5$sAdd and remove kernel and initrd images to and from /boot/%6$s\n" + "\n%3$sUsage:%4$s\n" + " kernel-install [OPTIONS...] add [[[KERNEL-VERSION] KERNEL-IMAGE] [INITRD ...]]\n" + " kernel-install [OPTIONS...] add-all\n" + " kernel-install [OPTIONS...] remove KERNEL-VERSION\n" + " kernel-install [OPTIONS...] inspect [[[KERNEL-VERSION] KERNEL-IMAGE]\n" + " [INITRD ...]]\n" + " kernel-install [OPTIONS...] list\n" + "\n%3$sOptions:%4$s\n" + " -h --help Show this help\n" + " --version Show package version\n" + " -v --verbose Increase verbosity\n" + " --esp-path=PATH Path to the EFI System Partition (ESP)\n" + " --boot-path=PATH Path to the $BOOT partition\n" + " --make-entry-directory=yes|no|auto\n" + " Create $BOOT/ENTRY-TOKEN/ directory\n" + " --entry-token=machine-id|os-id|os-image-id|auto|literal:…\n" + " Entry token to use for this installation\n" + " --no-pager Do not pipe inspect output into a pager\n" + " --json=pretty|short|off Generate JSON output\n" + " --no-legend Do not show the headers and footers\n" + " --root=PATH Operate on an alternate filesystem root\n" + " --image=PATH Operate on disk image as filesystem root\n" + " --image-policy=POLICY Specify disk image dissection policy\n" + "\n" + "This program may also be invoked as 'installkernel':\n" + " installkernel [OPTIONS...] VERSION VMLINUZ [MAP] [INSTALLATION-DIR]\n" + "(The optional arguments are passed by kernel build system, but ignored.)\n" + "\n" + "See the %2$s for details.\n", + program_invocation_short_name, + link, + ansi_underline(), + ansi_normal(), + ansi_highlight(), + ansi_normal()); + + return 0; +} + +static int parse_argv(int argc, char *argv[], Context *c) { + enum { + ARG_VERSION = 0x100, + ARG_NO_LEGEND, + ARG_ESP_PATH, + ARG_BOOT_PATH, + ARG_MAKE_ENTRY_DIRECTORY, + ARG_ENTRY_TOKEN, + ARG_NO_PAGER, + ARG_JSON, + ARG_ROOT, + ARG_IMAGE, + ARG_IMAGE_POLICY, + }; + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + { "verbose", no_argument, NULL, 'v' }, + { "esp-path", required_argument, NULL, ARG_ESP_PATH }, + { "boot-path", required_argument, NULL, ARG_BOOT_PATH }, + { "make-entry-directory", required_argument, NULL, ARG_MAKE_ENTRY_DIRECTORY }, + { "entry-token", required_argument, NULL, ARG_ENTRY_TOKEN }, + { "no-pager", no_argument, NULL, ARG_NO_PAGER }, + { "json", required_argument, NULL, ARG_JSON }, + { "root", required_argument, NULL, ARG_ROOT }, + { "image", required_argument, NULL, ARG_IMAGE }, + { "image-policy", required_argument, NULL, ARG_IMAGE_POLICY }, + { "no-legend", no_argument, NULL, ARG_NO_LEGEND }, + {} + }; + int t, r; + + assert(argc >= 0); + assert(argv); + assert(c); + + while ((t = getopt_long(argc, argv, "hv", options, NULL)) >= 0) + switch (t) { + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case ARG_NO_LEGEND: + arg_legend = false; + break; + + case 'v': + log_set_max_level(LOG_DEBUG); + arg_verbose = true; + break; + + case ARG_ESP_PATH: + r = parse_path_argument(optarg, /* suppress_root = */ false, &arg_esp_path); + if (r < 0) + return log_oom(); + break; + + case ARG_BOOT_PATH: + r = parse_path_argument(optarg, /* suppress_root = */ false, &arg_xbootldr_path); + if (r < 0) + return log_oom(); + break; + + case ARG_MAKE_ENTRY_DIRECTORY: + if (streq(optarg, "auto")) + arg_make_entry_directory = -1; + else { + r = parse_boolean_argument("--make-entry-directory=", optarg, NULL); + if (r < 0) + return r; + + arg_make_entry_directory = r; + } + break; + + case ARG_ENTRY_TOKEN: + r = parse_boot_entry_token_type(optarg, &c->entry_token_type, &c->entry_token); + if (r < 0) + return r; + break; + + case ARG_NO_PAGER: + arg_pager_flags |= PAGER_DISABLE; + break; + + case ARG_JSON: + r = parse_json_argument(optarg, &arg_json_format_flags); + if (r < 0) + return r; + break; + + case ARG_ROOT: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_root); + if (r < 0) + return r; + break; + + case ARG_IMAGE: + r = parse_path_argument(optarg, /* suppress_root= */ false, &arg_image); + if (r < 0) + return r; + break; + + case ARG_IMAGE_POLICY: + r = parse_image_policy_argument(optarg, &arg_image_policy); + if (r < 0) + return r; + break; + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + if (arg_image && arg_root) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Please specify either --root= or --image=, the combination of both is not supported."); + + return 1; +} + +static int run(int argc, char* argv[]) { + static const Verb verbs[] = { + { "add", 1, VERB_ANY, 0, verb_add }, + { "add-all", 1, 1, 0, verb_add_all }, + { "remove", 2, VERB_ANY, 0, verb_remove }, + { "inspect", 1, VERB_ANY, VERB_DEFAULT, verb_inspect }, + { "list", 1, 1, 0, verb_list }, + {} + }; + _cleanup_(context_done) Context c = { + .rfd = AT_FDCWD, + .action = _ACTION_INVALID, + .kernel_image_type = KERNEL_IMAGE_TYPE_UNKNOWN, + .layout = _LAYOUT_INVALID, + .entry_token_type = BOOT_ENTRY_TOKEN_AUTO, + }; + _cleanup_(loop_device_unrefp) LoopDevice *loop_device = NULL; + _cleanup_(umount_and_freep) char *mounted_dir = NULL; + int r; + + log_setup(); + + r = parse_argv(argc, argv, &c); + if (r <= 0) + return r; + + if (arg_image) { + assert(!arg_root); + + r = mount_image_privately_interactively( + arg_image, + arg_image_policy, + DISSECT_IMAGE_GENERIC_ROOT | + DISSECT_IMAGE_REQUIRE_ROOT | + DISSECT_IMAGE_RELAX_VAR_CHECK | + DISSECT_IMAGE_VALIDATE_OS, + &mounted_dir, + /* ret_dir_fd= */ NULL, + &loop_device); + if (r < 0) + return r; + + arg_root = strdup(mounted_dir); + if (!arg_root) + return log_oom(); + } + + r = context_init(&c); + if (r < 0) + return r; + + if (invoked_as(argv, "installkernel")) + return run_as_installkernel(argc, argv, &c); + + return dispatch_verb(argc, argv, verbs, &c); +} + +DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run); diff --git a/src/kernel-install/meson.build b/src/kernel-install/meson.build new file mode 100644 index 0000000..7f61fcc --- /dev/null +++ b/src/kernel-install/meson.build @@ -0,0 +1,51 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later + +want_kernel_install = conf.get('ENABLE_KERNEL_INSTALL') == 1 + +executables += [ + executable_template + { + 'name' : 'kernel-install', + 'public' : true, + 'conditions' : ['ENABLE_KERNEL_INSTALL'], + 'sources' : files('kernel-install.c'), + }, +] + +ukify_install = custom_target( + '60-ukify.install', + input : '60-ukify.install.in', + output : '60-ukify.install', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : want_kernel_install and want_ukify, + install_mode : 'rwxr-xr-x', + install_dir : kernelinstalldir) + +loaderentry_install = custom_target( + '90-loaderentry.install', + input : '90-loaderentry.install.in', + output : '90-loaderentry.install', + command : [jinja2_cmdline, '@INPUT@', '@OUTPUT@'], + install : want_kernel_install, + install_mode : 'rwxr-xr-x', + install_dir : kernelinstalldir) + +uki_copy_install = files('90-uki-copy.install') + +kernel_install_files = uki_copy_install + files( + '50-depmod.install', +) + +if want_kernel_install + install_data(kernel_install_files, + install_mode : 'rwxr-xr-x', + install_dir : kernelinstalldir) + + install_data('install.conf', + install_dir : kerneldir) + + if install_sysconfdir + install_emptydir(sysconfdir / 'kernel/install.d') + endif +endif + +test_kernel_install_sh = find_program('test-kernel-install.sh') diff --git a/src/kernel-install/test-kernel-install.sh b/src/kernel-install/test-kernel-install.sh new file mode 100755 index 0000000..338d811 --- /dev/null +++ b/src/kernel-install/test-kernel-install.sh @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +# shellcheck disable=SC2235 +set -eux +set -o pipefail + +export SYSTEMD_LOG_LEVEL=debug + +kernel_install="${1:?}" +loaderentry_install="${2:?}" +uki_copy_install="${3:?}" +ukify="${4:-}" +ukify_install="${5:-}" +boot_stub="${6:-}" +if [[ -d "${PROJECT_BUILD_ROOT:-}" ]]; then + bootctl="${PROJECT_BUILD_ROOT}/bootctl" +else + bootctl= +fi + +D="$(mktemp --tmpdir --directory "test-kernel-install.XXXXXXXXXX")" + +# shellcheck disable=SC2064 +trap "rm -rf '$D'" EXIT INT QUIT PIPE +mkdir -p "$D/boot" +mkdir -p "$D/efi" +mkdir -p "$D/sources" + +echo 'buzy image' >"$D/sources/linux" +echo 'the initrd' >"$D/sources/initrd" +echo 'the-token' >"$D/sources/entry-token" +echo 'opt1 opt2' >"$D/sources/cmdline" + +cat >"$D/sources/install.conf" <<EOF +layout=bls +initrd_generator=none +# those are overridden by envvars +BOOT_ROOT="$D/badboot" +MACHINE_ID=badbadbadbadbadbad6abadbadbadbad +EOF + +# Create a 'devicetree' config file that points to a fake dtb file +echo 'subdir/whatever.dtb' >"$D/sources/devicetree" +mkdir "$D/sources/subdir" +echo 'DTBDTBDTBDTB' >"$D/sources/subdir/whatever.dtb" + +export KERNEL_INSTALL_CONF_ROOT="$D/sources" +# We "install" multiple plugins, but control which ones will be active via install.conf. +export KERNEL_INSTALL_PLUGINS="${ukify_install} ${loaderentry_install} ${uki_copy_install}" +export BOOT_ROOT="$D/boot" +export BOOT_MNT="$D/boot" +export MACHINE_ID='3e0484f3634a418b8e6a39e8828b03e3' +export KERNEL_INSTALL_UKIFY="$ukify" +export KERNEL_INSTALL_BOOT_STUB="$boot_stub" +export KERNEL_INSTALL_READ_MACHINE_INFO="no" +export KERNEL_INSTALL_BYPASS="no" + +# Test type#1 installation +"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" + +entry="$BOOT_ROOT/loader/entries/the-token-1.1.1.conf" +test -f "$entry" +grep -qE '^title ' "$entry" +grep -qE '^version +1.1.1' "$entry" +grep -qE '^options +opt1 opt2' "$entry" +grep -qE '^linux .*/the-token/1.1.1/linux' "$entry" +grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry" +grep -qE '^devicetree .*/the-token/1.1.1/whatever.dtb' "$entry" + +grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux" +grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd" +grep -qE 'DTBDTB' "$BOOT_ROOT/the-token/1.1.1/whatever.dtb" + +test -f /usr/lib/modules/"$(uname -r)"/vmlinuz && "$kernel_install" inspect +"$kernel_install" inspect "$D/sources/linux" + +"$kernel_install" -v remove 1.1.1 +test ! -e "$entry" +test ! -e "$BOOT_ROOT/the-token/1.1.1/linux" +test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd" + +# Test again with too many arguments for 'remove' command. See #28448. +"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" + +test -f "$entry" +test -f "$BOOT_ROOT/the-token/1.1.1/linux" +test -f "$BOOT_ROOT/the-token/1.1.1/initrd" + +"$kernel_install" -v remove 1.1.1 hoge foo bar +test ! -e "$entry" +test ! -e "$BOOT_ROOT/the-token/1.1.1/linux" +test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd" + +# Invoke kernel-install as installkernel +ln -s --relative -v "$kernel_install" "$D/sources/installkernel" +"$D/sources/installkernel" -v 1.1.2 "$D/sources/linux" System.map /somedirignored + +entry="$BOOT_ROOT/loader/entries/the-token-1.1.2.conf" +test -f "$entry" +grep -qE '^title ' "$entry" +grep -qE '^version +1.1.2' "$entry" +grep -qE '^options +opt1 opt2' "$entry" +grep -qE '^linux .*/the-token/1.1.2/linux' "$entry" +( ! grep -qE '^initrd' "$entry" ) +grep -qE '^devicetree .*/the-token/1.1.2/whatever.dtb' "$entry" + +grep -qE 'image' "$BOOT_ROOT/the-token/1.1.2/linux" +test ! -e "$BOOT_ROOT/the-token/1.1.2/initrd" +grep -qE 'DTBDTB' "$BOOT_ROOT/the-token/1.1.2/whatever.dtb" + +# Check installation with boot counting +echo '56' >"$D/sources/tries" + +"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +entry="$BOOT_ROOT/loader/entries/the-token-1.1.1+56.conf" +test -f "$entry" +grep -qE '^title ' "$entry" +grep -qE '^version +1.1.1' "$entry" +grep -qE '^options +opt1 opt2' "$entry" +grep -qE '^linux .*/the-token/1.1.1/linux' "$entry" +grep -qE '^initrd .*/the-token/1.1.1/initrd' "$entry" + +grep -qE 'image' "$BOOT_ROOT/the-token/1.1.1/linux" +grep -qE 'initrd' "$BOOT_ROOT/the-token/1.1.1/initrd" + +# Install UKI +if [ -f "$ukify" ]; then + cat >>"$D/sources/install.conf" <<EOF +layout=uki +uki_generator=ukify +EOF + + "$kernel_install" -v add 1.1.3 "$D/sources/linux" "$D/sources/initrd" + uki="${BOOT_ROOT}/EFI/Linux/the-token-1.1.3+56.efi" + test -f "$uki" + + if [ -x "$bootctl" ]; then + "$bootctl" kernel-inspect "$uki" | grep -qE 'Kernel Type: +uki$' + "$bootctl" kernel-inspect "$uki" | grep -qE 'Version: +1\.1\.3$' + "$bootctl" kernel-inspect "$uki" | grep -qE 'Cmdline: +opt1 opt2$' + fi + + "$ukify" inspect "$uki" | grep -qE '^.sbat' + "$ukify" inspect "$uki" | grep -qE '^.cmdline' + "$ukify" inspect "$uki" | grep -qE '^.uname' + "$ukify" inspect "$uki" | grep -qE '^.initrd' + "$ukify" inspect "$uki" | grep -qE '^.linux' + "$ukify" inspect "$uki" | grep -qE '^.dtb' +fi + +# Test bootctl +if [ -x "$bootctl" ]; then + echo "Testing bootctl" + e2="${entry%+*}_2.conf" + cp "$entry" "$e2" + export SYSTEMD_ESP_PATH=/boot + # We use --root so strip the root prefix from KERNEL_INSTALL_CONF_ROOT + export KERNEL_INSTALL_CONF_ROOT="sources" + + # create file that is not referenced. Check if cleanup removes + # it but leaves the rest alone + :> "$BOOT_ROOT/the-token/1.1.2/initrd" + "$bootctl" --root="$D" cleanup + test ! -e "$BOOT_ROOT/the-token/1.1.2/initrd" + test -e "$BOOT_ROOT/the-token/1.1.2/linux" + test -e "$BOOT_ROOT/the-token/1.1.1/linux" + test -e "$BOOT_ROOT/the-token/1.1.1/initrd" + + # now remove duplicated entry and make sure files are left over + "$bootctl" --root="$D" unlink "${e2##*/}" + test -e "$BOOT_ROOT/the-token/1.1.1/linux" + test -e "$BOOT_ROOT/the-token/1.1.1/initrd" + test -e "$entry" + test ! -e "$e2" + # remove last entry referencing those files + entry_id="${entry##*/}" + entry_id="${entry_id%+*}.conf" + "$bootctl" --root="$D" unlink "$entry_id" + test ! -e "$entry" + test ! -e "$BOOT_ROOT/the-token/1.1.1/linux" + test ! -e "$BOOT_ROOT/the-token/1.1.1/initrd" +fi + +########################################### +# tests for --make-entry-directory= +########################################### + +# disable all dropins +cat >"$D/00-skip.install" <<EOF +#!/bin/bash +exit 77 +EOF +chmod +x "$D/00-skip.install" +export KERNEL_INSTALL_PLUGINS="$D/00-skip.install" + +# drop layout= from install.conf +cat >"$D/sources/install.conf" <<EOF +initrd_generator=none +# those are overridden by envvars +BOOT_ROOT="$D/badboot" +MACHINE_ID=badbadbadbadbadbad6abadbadbadbad +EOF +export KERNEL_INSTALL_CONF_ROOT="$D/sources" + +rm -rf "$BOOT_ROOT" +mkdir -p "$BOOT_ROOT" + +# 1. defaults to 'auto', and the entry directory is created only when the layout is BLS +# 1.1 token directory does not exist -> layout is other. +"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test ! -e "$BOOT_ROOT/the-token/1.1.1" +"$kernel_install" -v remove 1.1.1 +test ! -e "$BOOT_ROOT/the-token/1.1.1" + +# 1.2 token directory exists -> layout is BLS +mkdir -p "$BOOT_ROOT/the-token" +"$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test -d "$BOOT_ROOT/the-token/1.1.1" +"$kernel_install" -v remove 1.1.1 +test ! -e "$BOOT_ROOT/the-token/1.1.1" +rmdir "$BOOT_ROOT/the-token" + +# 2. --make-entry-directory=yes +# 2.1 token directory does not exist -> layout is other. +"$kernel_install" -v --make-entry-directory=yes add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test -d "$BOOT_ROOT/the-token/1.1.1" +"$kernel_install" -v --make-entry-directory=yes remove 1.1.1 +test ! -e "$BOOT_ROOT/the-token/1.1.1" +test -d "$BOOT_ROOT/the-token" + +# 2.2 token directory exists -> layout is BLS +mkdir -p "$BOOT_ROOT/the-token" +"$kernel_install" -v --make-entry-directory=yes add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test -d "$BOOT_ROOT/the-token/1.1.1" +"$kernel_install" -v --make-entry-directory=yes remove 1.1.1 +test ! -e "$BOOT_ROOT/the-token/1.1.1" +test -d "$BOOT_ROOT/the-token" +rmdir "$BOOT_ROOT/the-token" + +# 3. --make-entry-directory=no +# 3.1 token directory does not exist -> layout is other. +"$kernel_install" -v --make-entry-directory=no add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test ! -e "$BOOT_ROOT/the-token/1.1.1" +"$kernel_install" -v --make-entry-directory=no remove 1.1.1 +test ! -e "$BOOT_ROOT/the-token/1.1.1" + +# 3.2 token directory exists -> layout is BLS +mkdir -p "$BOOT_ROOT/the-token" +"$kernel_install" -v --make-entry-directory=no add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test ! -e "$BOOT_ROOT/the-token/1.1.1" +"$kernel_install" -v --make-entry-directory=no remove 1.1.1 +test ! -e "$BOOT_ROOT/the-token/1.1.1" +test -d "$BOOT_ROOT/the-token" +rmdir "$BOOT_ROOT/the-token" + +########################################### +# tests for --entry-token= +########################################### +"$kernel_install" -v --make-entry-directory=yes --entry-token=machine-id add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test -d "$BOOT_ROOT/$MACHINE_ID/1.1.1" +"$kernel_install" -v --make-entry-directory=yes --entry-token=machine-id remove 1.1.1 +test ! -e "$BOOT_ROOT/$MACHINE_ID/1.1.1" +test -d "$BOOT_ROOT/$MACHINE_ID" +rmdir "$BOOT_ROOT/$MACHINE_ID" + +"$kernel_install" -v --make-entry-directory=yes --entry-token=literal:hoge add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +test -d "$BOOT_ROOT/hoge/1.1.1" +"$kernel_install" -v --make-entry-directory=yes --entry-token=literal:hoge remove 1.1.1 +test ! -e "$BOOT_ROOT/hoge/1.1.1" +test -d "$BOOT_ROOT/hoge" +rmdir "$BOOT_ROOT/hoge" + +########################################### +# tests for --json= +########################################### +output="$("$kernel_install" -v --json=pretty inspect 1.1.1 "$D/sources/linux")" + +diff -u <(echo "$output") - <<EOF +{ + "MachineID" : "3e0484f3634a418b8e6a39e8828b03e3", + "KernelImageType" : "unknown", + "Layout" : "other", + "BootRoot" : "$BOOT_ROOT", + "EntryTokenType" : "literal", + "EntryToken" : "the-token", + "EntryDirectory" : "$BOOT_ROOT/the-token/1.1.1", + "KernelVersion" : "1.1.1", + "Kernel" : "$D/sources/linux", + "Initrds" : null, + "InitrdGenerator" : "none", + "UKIGenerator" : null, + "Plugins" : [ + "$D/00-skip.install" + ], + "PluginEnvironment" : [ + "LC_COLLATE=C.UTF-8", + "KERNEL_INSTALL_VERBOSE=1", + "KERNEL_INSTALL_IMAGE_TYPE=unknown", + "KERNEL_INSTALL_MACHINE_ID=3e0484f3634a418b8e6a39e8828b03e3", + "KERNEL_INSTALL_ENTRY_TOKEN=the-token", + "KERNEL_INSTALL_BOOT_ROOT=$BOOT_ROOT", + "KERNEL_INSTALL_LAYOUT=other", + "KERNEL_INSTALL_INITRD_GENERATOR=none", + "KERNEL_INSTALL_UKI_GENERATOR=", + "KERNEL_INSTALL_STAGING_AREA=/tmp/kernel-install.staging.XXXXXX" + ] +} +EOF + +########################################### +# tests for propagation of plugin failure (issue #30087) +########################################### +cat >"$D/00-plugin-skip" <<EOF +#!/usr/bin/env bash +exit 77 +EOF +chmod +x "$D/00-plugin-skip" + +cat >"$D/10-plugin-fail" <<EOF +#!/usr/bin/env bash +exit 42 +EOF +chmod +x "$D/10-plugin-fail" + +# Exit code 77 means remaining plugins will be skipped. +KERNEL_INSTALL_PLUGINS="$D/00-plugin-skip $D/10-plugin-fail" "$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" + +# Other non-zero exit code will be propagated. +set +e +KERNEL_INSTALL_PLUGINS="$D/10-plugin-fail" "$kernel_install" -v add 1.1.1 "$D/sources/linux" "$D/sources/initrd" +ret=$? +set -e +test "$ret" -eq "42" |