diff --git a/src/shared/reboot-util.c b/src/shared/reboot-util.c index d9ff532921..5e460b1dc5 100644 --- a/src/shared/reboot-util.c +++ b/src/shared/reboot-util.c @@ -14,19 +14,40 @@ #include #include "errno-util.h" -#include "fd-util.h" #endif #include "alloc-util.h" +#include "compress.h" +#include "copy.h" +#include "fd-util.h" #include "fileio.h" +#include "io-util.h" #include "log.h" +#include "memfd-util.h" +#include "pe-binary.h" #include "proc-cmdline.h" #include "reboot-util.h" +#include "sparse-endian.h" +#include "stat-util.h" #include "string-util.h" #include "umask-util.h" #include "utf8.h" #include "virt.h" +/* ZBOOT header layout — see linux/drivers/firmware/efi/libstub/zboot-header.S */ +struct zboot_header { + le16_t mz_magic; /* 0x00: "MZ" DOS signature */ + le16_t _pad0; + uint8_t zimg_magic[4]; /* 0x04: "zimg" identifier */ + le32_t payload_offset; /* 0x08: offset to compressed payload */ + le32_t payload_size; /* 0x0C: size of compressed payload */ + uint8_t _pad1[8]; + char comp_type[6]; /* 0x18: NUL-terminated compression type (e.g. "gzip", "zstd") */ + uint8_t _pad2[2]; +} _packed_; +assert_cc(sizeof(struct zboot_header) == 0x20); +assert_cc(offsetof(struct zboot_header, comp_type) == 0x18); + int raw_reboot(int cmd, const void *arg) { return syscall(SYS_reboot, LINUX_REBOOT_MAGIC1, LINUX_REBOOT_MAGIC2, cmd, arg); } @@ -246,6 +267,228 @@ int kexec(void) { return 0; } +static int decompress_to_memfd(Compression compression, int fd) { + int r; + + _cleanup_close_ int memfd = memfd_new("kexec-kernel"); + if (memfd < 0) + return log_error_errno(memfd, "Failed to create memfd: %m"); + + r = decompress_stream(compression, fd, memfd, UINT64_MAX); + if (r < 0) + return log_error_errno(r, "Failed to decompress kernel: %m"); + + if (lseek(memfd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek memfd: %m"); + + return TAKE_FD(memfd); +} + +static int decompress_zboot_to_memfd(int fd, uint32_t payload_offset, uint32_t payload_size, const char *comp_type) { + int r; + + Compression c = compression_from_string(comp_type); + if (c < 0) + return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), + "Unsupported ZBOOT compression type '%s'.", comp_type); + + struct stat st; + if (fstat(fd, &st) < 0) + return log_error_errno(errno, "Failed to stat ZBOOT image: %m"); + + r = stat_verify_regular(&st); + if (r < 0) + return log_error_errno(r, "Kernel image is not a regular file: %m"); + + if (payload_offset < 0x20 || + payload_size == 0 || + payload_offset > (uint64_t) st.st_size || + payload_size > (uint64_t) st.st_size - payload_offset) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "ZBOOT payload offset/size invalid."); + + if (payload_size > 256 * 1024 * 1024) /* generous for any compressed kernel */ + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "ZBOOT payload unreasonably large."); + + _cleanup_free_ void *payload = malloc(payload_size); + if (!payload) + return log_oom(); + + ssize_t n = pread(fd, payload, payload_size, payload_offset); + if (n < 0) + return log_error_errno(errno, "Failed to read ZBOOT payload: %m"); + if ((uint32_t) n < payload_size) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read of ZBOOT payload."); + + _cleanup_free_ void *decompressed = NULL; + size_t decompressed_size; + r = decompress_blob(c, payload, payload_size, &decompressed, &decompressed_size, /* dst_max= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to decompress ZBOOT payload: %m"); + + payload = mfree(payload); + + _cleanup_close_ int memfd = memfd_new("kexec-kernel"); + if (memfd < 0) + return log_error_errno(memfd, "Failed to create memfd: %m"); + + r = loop_write(memfd, decompressed, decompressed_size); + if (r < 0) + return log_error_errno(r, "Failed to write decompressed kernel to memfd: %m"); + + if (lseek(memfd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek memfd: %m"); + + return TAKE_FD(memfd); +} + +static int pe_section_to_memfd(int fd, const IMAGE_SECTION_HEADER *section, const char *name) { + int r; + + assert(fd >= 0); + assert(section); + + uint32_t offset = le32toh(section->PointerToRawData), + size = MIN(le32toh(section->VirtualSize), le32toh(section->SizeOfRawData)); + + _cleanup_close_ int memfd = memfd_new(name); + if (memfd < 0) + return log_error_errno(memfd, "Failed to create memfd for PE section '%s': %m", name); + + if (lseek(fd, offset, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek to PE section '%s': %m", name); + + r = copy_bytes(fd, memfd, size, /* copy_flags= */ 0); + if (r < 0) + return log_error_errno(r, "Failed to copy PE section '%s': %m", name); + + if (lseek(memfd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek memfd: %m"); + + return TAKE_FD(memfd); +} + +static int extract_uki(const char *path, int fd, int *ret_kernel_fd, int *ret_initrd_fd) { + int r; + + assert(fd >= 0); + assert(ret_kernel_fd); + + _cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL; + _cleanup_free_ PeHeader *pe_header = NULL; + r = pe_load_headers(fd, &dos_header, &pe_header); + if (r < 0) + return log_debug_errno(r, "Not a valid PE file '%s': %m", path); + + _cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL; + r = pe_load_sections(fd, dos_header, pe_header, §ions); + if (r < 0) + return log_debug_errno(r, "Failed to load PE sections from '%s': %m", path); + + if (!pe_is_uki(pe_header, sections)) + return 0; /* Not a UKI */ + + /* FIXME: we currently only extract .linux and .initrd, but sd-stub does a lot more: + * profiles, .cmdline, .dtb/.dtbauto, .ucode, .pcrsig/.pcrpkey, sidecar addons, + * credentials, sysexts/confexts, and TPM PCR measurements. */ + + const IMAGE_SECTION_HEADER *linux_section = pe_header_find_section(pe_header, sections, ".linux"); + if (!linux_section) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "UKI '%s' has no .linux section.", path); + + log_debug("Detected UKI image '%s', extracting .linux section.", path); + + _cleanup_close_ int kernel_memfd = pe_section_to_memfd(fd, linux_section, "kexec-uki-kernel"); + if (kernel_memfd < 0) + return kernel_memfd; + + _cleanup_close_ int initrd_memfd = -EBADF; + if (ret_initrd_fd) { + const IMAGE_SECTION_HEADER *initrd_section = pe_header_find_section(pe_header, sections, ".initrd"); + if (initrd_section) { + log_debug("Extracting .initrd section from UKI '%s'.", path); + + initrd_memfd = pe_section_to_memfd(fd, initrd_section, "kexec-uki-initrd"); + if (initrd_memfd < 0) + return initrd_memfd; + } + } + + *ret_kernel_fd = TAKE_FD(kernel_memfd); + if (ret_initrd_fd) + *ret_initrd_fd = TAKE_FD(initrd_memfd); + + return 1; +} + +int kexec_maybe_decompress_kernel(const char *path, int fd, int *ret_kernel_fd, int *ret_initrd_fd) { + uint8_t magic[8]; + ssize_t n; + int r; + + assert(fd >= 0); + assert(ret_kernel_fd); + + n = pread(fd, magic, sizeof(magic), 0); + if (n < 0) + return log_error_errno(errno, "Failed to read kernel magic from '%s': %m", path); + if ((size_t) n < sizeof(magic)) + /* Too small to detect, pass through as-is */ + return 0; + + if (magic[0] == 'M' && magic[1] == 'Z') { + + if (magic[4] == 'z' && magic[5] == 'i' && magic[6] == 'm' && magic[7] == 'g') { + struct zboot_header h; + + n = pread(fd, &h, sizeof(h), 0); + if (n < 0) + return log_error_errno(errno, "Failed to read ZBOOT header from '%s': %m", path); + if ((size_t) n < sizeof(h)) + return log_error_errno(SYNTHETIC_ERRNO(EBADMSG), + "Short read of ZBOOT header from '%s'.", path); + + char comp_type[sizeof(h.comp_type) + 1]; + memcpy(comp_type, h.comp_type, sizeof(h.comp_type)); + comp_type[sizeof(h.comp_type)] = '\0'; + + uint32_t payload_offset = le32toh(h.payload_offset), + payload_size = le32toh(h.payload_size); + + log_debug("Detected ZBOOT image '%s' (compression=%s, offset=%"PRIu32", size=%"PRIu32")", + path, comp_type, payload_offset, payload_size); + + r = decompress_zboot_to_memfd(fd, payload_offset, payload_size, comp_type); + if (r < 0) + return r; + + *ret_kernel_fd = r; + return 1; + } + + /* MZ but not ZBOOT — check if it's a UKI */ + return extract_uki(path, fd, ret_kernel_fd, ret_initrd_fd); + } + + Compression c = compression_detect_from_magic(magic); + if (c < 0) + /* Not a recognized compressed format, pass through as-is */ + return 0; + + log_debug("Detected %s-compressed kernel '%s', decompressing.", compression_to_string(c), path); + + /* Seek back to start before decompression */ + if (lseek(fd, 0, SEEK_SET) < 0) + return log_error_errno(errno, "Failed to seek kernel fd: %m"); + + r = decompress_to_memfd(c, fd); + if (r < 0) + return r; + + *ret_kernel_fd = r; + return 1; +} + int create_shutdown_run_nologin_or_warn(void) { int r; diff --git a/src/shared/reboot-util.h b/src/shared/reboot-util.h index 4548903a4c..658d065ce9 100644 --- a/src/shared/reboot-util.h +++ b/src/shared/reboot-util.h @@ -28,4 +28,6 @@ bool shall_restore_state(void); bool kexec_loaded(void); int kexec(void); +int kexec_maybe_decompress_kernel(const char *path, int fd, int *ret_kernel_fd, int *ret_initrd_fd); + int create_shutdown_run_nologin_or_warn(void); diff --git a/src/systemctl/systemctl-start-special.c b/src/systemctl/systemctl-start-special.c index d947f1f9e4..b7d10eb891 100644 --- a/src/systemctl/systemctl-start-special.c +++ b/src/systemctl/systemctl-start-special.c @@ -1,5 +1,6 @@ /* SPDX-License-Identifier: LGPL-2.1-or-later */ +#include #include #include "sd-bus.h" @@ -8,6 +9,7 @@ #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" @@ -23,9 +25,6 @@ #include "systemctl-util.h" static int load_kexec_kernel(void) { - _cleanup_(boot_config_free) BootConfig config = BOOT_CONFIG_NULL; - _cleanup_free_ char *kernel = NULL, *initrd = NULL, *options = NULL; - const BootEntry *e; int r; if (kexec_loaded()) { @@ -33,9 +32,7 @@ static int load_kexec_kernel(void) { return 0; } - if (access(KEXEC, X_OK) < 0) - return log_error_errno(errno, KEXEC" is not available: %m"); - + _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. */ @@ -51,7 +48,7 @@ static int load_kexec_kernel(void) { if (r < 0) return r; - e = boot_config_default_entry(&config); + 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."); @@ -65,29 +62,82 @@ static int load_kexec_kernel(void) { 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(); } - options = strv_join(e->options, " "); + _cleanup_free_ char *options = strv_join(e->options, " "); if (!options) return log_oom(); log_full(arg_quiet ? LOG_DEBUG : LOG_INFO, - "%s "KEXEC" --load \"%s\" --append \"%s\"%s%s%s", - arg_dry_run ? "Would run" : "Running", + "%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 \"" : NULL, strempty(initrd), initrd ? "\"" : ""); + 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 (saved_errno == ENOEXEC) { + /* 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. */ + 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, diff --git a/src/test/meson.build b/src/test/meson.build index 4cb77505f3..c930971815 100644 --- a/src/test/meson.build +++ b/src/test/meson.build @@ -346,6 +346,10 @@ executables += [ 'sources' : files('test-json.c'), 'dependencies' : libm, }, + test_template + { + 'sources' : files('test-kexec.c'), + 'link_with' : [libshared], + }, test_template + { 'sources' : files('test-libcrypt-util.c'), 'conditions' : ['HAVE_LIBCRYPT'], diff --git a/src/test/test-kexec.c b/src/test/test-kexec.c new file mode 100644 index 0000000000..2a400f75d0 --- /dev/null +++ b/src/test/test-kexec.c @@ -0,0 +1,261 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include +#include +#include + +#include "alloc-util.h" +#include "compress.h" +#include "fd-util.h" +#include "io-util.h" +#include "reboot-util.h" +#include "string-util.h" +#include "tests.h" +#include "tmpfile-util.h" +#include "unaligned.h" + +static int find_kernel_image(char **ret) { + struct utsname u; + + ASSERT_OK_ERRNO(uname(&u)); + + /* Kernel image names vary across architectures and distributions: + * vmlinuz — compressed Linux kernel (x86, most distros) + * vmlinux — uncompressed ELF kernel (ppc64, s390) + * Image — uncompressed flat binary (arm64, riscv) + * Image.gz — gzip-compressed Image (arm64) + * zImage — compressed kernel (arm 32-bit) + * vmlinuz.efi — EFI ZBOOT PE wrapper (arm64 with CONFIG_EFI_ZBOOT) */ + static const char *const names[] = { + "vmlinuz", + "vmlinux", + "Image", + "Image.gz", + "zImage", + "vmlinuz.efi", + }; + + /* Try /usr/lib/modules// first (kernel-install convention), + * then /boot/-, then /boot/ */ + for (size_t i = 0; i < ELEMENTSOF(names); i++) { + _cleanup_free_ char *path = NULL; + + path = strjoin("/usr/lib/modules/", u.release, "/", names[i]); + if (!path) + return -ENOMEM; + + if (access(path, R_OK) >= 0) { + *ret = TAKE_PTR(path); + return 0; + } + } + + /* /boot may not be accessible without root, skip gracefully */ + if (access("/boot", R_OK) >= 0) { + for (size_t i = 0; i < ELEMENTSOF(names); i++) { + _cleanup_free_ char *path = NULL; + + path = strjoin("/boot/", names[i], "-", u.release); + if (!path) + return -ENOMEM; + + if (access(path, R_OK) >= 0) { + *ret = TAKE_PTR(path); + return 0; + } + } + + for (size_t i = 0; i < ELEMENTSOF(names); i++) { + _cleanup_free_ char *path = NULL; + + path = strjoin("/boot/", names[i]); + if (!path) + return -ENOMEM; + + if (access(path, R_OK) >= 0) { + *ret = TAKE_PTR(path); + return 0; + } + } + } + + return -ENOENT; +} + +TEST(passthrough_unrecognized) { + /* A file with unrecognized magic should pass through as-is (return 0) */ + _cleanup_close_ int fd = -EBADF; + _cleanup_(unlink_tempfilep) char path[] = "/tmp/test-kexec.XXXXXX"; + + ASSERT_OK(fd = mkostemp_safe(path)); + ASSERT_OK_EQ_ERRNO(write(fd, "HELLO WORLD\0", 12), 12); + ASSERT_OK_ERRNO(lseek(fd, 0, SEEK_SET)); + + _cleanup_close_ int kernel_fd = -EBADF, initrd_fd = -EBADF; + ASSERT_OK_ZERO(kexec_maybe_decompress_kernel(path, fd, &kernel_fd, &initrd_fd)); + ASSERT_EQ(kernel_fd, -EBADF); + ASSERT_EQ(initrd_fd, -EBADF); +} + +TEST(gzip_round_trip) { + _cleanup_close_ int src_fd = -EBADF, gz_fd = -EBADF; + _cleanup_(unlink_tempfilep) char + src_path[] = "/tmp/test-kexec-src.XXXXXX", + gz_path[] = "/tmp/test-kexec-gz.XXXXXX"; + int r; + + r = dlopen_zlib(); + if (r < 0) { + log_tests_skipped("zlib not available"); + return; + } + + /* Create a source file with known content */ + ASSERT_OK(src_fd = mkostemp_safe(src_path)); + char buf[4096]; + memset(buf, 'A', sizeof(buf)); + ASSERT_OK(loop_write(src_fd, buf, sizeof(buf))); + + /* Compress it with gzip */ + ASSERT_OK_ERRNO(lseek(src_fd, 0, SEEK_SET)); + ASSERT_OK(gz_fd = mkostemp_safe(gz_path)); + ASSERT_OK(compress_stream(COMPRESSION_GZIP, src_fd, gz_fd, UINT64_MAX, NULL)); + + /* Feed the gzip file to kexec_maybe_decompress_kernel */ + ASSERT_OK_ERRNO(lseek(gz_fd, 0, SEEK_SET)); + + _cleanup_close_ int kernel_fd = -EBADF, initrd_fd = -EBADF; + ASSERT_OK_POSITIVE(kexec_maybe_decompress_kernel(gz_path, gz_fd, &kernel_fd, &initrd_fd)); + ASSERT_GE(kernel_fd, 0); + ASSERT_EQ(initrd_fd, -EBADF); + + /* Verify the decompressed content matches the original */ + char result[4096]; + ASSERT_OK_EQ_ERRNO(pread(kernel_fd, result, sizeof(result), 0), (ssize_t) sizeof(result)); + ASSERT_EQ(memcmp(buf, result, sizeof(buf)), 0); +} + +TEST(zboot_synthetic) { + /* Construct a minimal ZBOOT header with a gzip-compressed payload */ + _cleanup_close_ int src_fd = -EBADF, gz_fd = -EBADF, zboot_fd = -EBADF; + _cleanup_(unlink_tempfilep) char + src_path[] = "/tmp/test-kexec-zboot-src.XXXXXX", + gz_path[] = "/tmp/test-kexec-zboot-gz.XXXXXX", + zboot_path[] = "/tmp/test-kexec-zboot.XXXXXX"; + int r; + + r = dlopen_zlib(); + if (r < 0) { + log_tests_skipped("zlib not available"); + return; + } + + /* Create and compress a payload */ + char payload[512]; + memset(payload, 'K', sizeof(payload)); + + ASSERT_OK(src_fd = mkostemp_safe(src_path)); + ASSERT_OK(loop_write(src_fd, payload, sizeof(payload))); + ASSERT_OK_ERRNO(lseek(src_fd, 0, SEEK_SET)); + + ASSERT_OK(gz_fd = mkostemp_safe(gz_path)); + ASSERT_OK(compress_stream(COMPRESSION_GZIP, src_fd, gz_fd, UINT64_MAX, NULL)); + + /* Read the compressed data */ + struct stat st; + ASSERT_OK_ERRNO(fstat(gz_fd, &st)); + size_t compressed_size = st.st_size; + _cleanup_free_ void *compressed = malloc(compressed_size); + ASSERT_NOT_NULL(compressed); + ASSERT_OK_EQ_ERRNO(pread(gz_fd, compressed, compressed_size, 0), (ssize_t) compressed_size); + + /* Build the ZBOOT header: + * 0x00: "MZ" + * 0x04: "zimg" + * 0x08: payload offset (LE32) + * 0x0C: payload size (LE32) + * 0x18: "gzip\0" */ + uint8_t header[0x40] = {}; + uint32_t payload_offset = sizeof(header); + + header[0] = 'M'; + header[1] = 'Z'; + memcpy(header + 0x04, "zimg", 4); + unaligned_write_le32(header + 0x08, payload_offset); + unaligned_write_le32(header + 0x0C, (uint32_t) compressed_size); + memcpy(header + 0x18, "gzip", 5); + + ASSERT_OK(zboot_fd = mkostemp_safe(zboot_path)); + ASSERT_OK(loop_write(zboot_fd, header, sizeof(header))); + ASSERT_OK(loop_write(zboot_fd, compressed, compressed_size)); + ASSERT_OK_ERRNO(lseek(zboot_fd, 0, SEEK_SET)); + + /* Test extraction */ + _cleanup_close_ int kernel_fd = -EBADF, initrd_fd = -EBADF; + ASSERT_OK_POSITIVE(kexec_maybe_decompress_kernel(zboot_path, zboot_fd, &kernel_fd, &initrd_fd)); + ASSERT_GE(kernel_fd, 0); + + /* Verify decompressed content matches original payload */ + char result[512]; + ASSERT_OK_EQ_ERRNO(pread(kernel_fd, result, sizeof(result), 0), (ssize_t) sizeof(result)); + ASSERT_EQ(memcmp(payload, result, sizeof(payload)), 0); +} + +TEST(system_kernel) { + _cleanup_free_ char *path = NULL; + _cleanup_close_ int fd = -EBADF; + int r; + + r = find_kernel_image(&path); + if (r < 0) { + log_tests_skipped_errno(r, "No kernel image found on this system"); + return; + } + + log_info("Found kernel image: %s", path); + + fd = open(path, O_RDONLY|O_CLOEXEC); + if (fd < 0) { + log_tests_skipped_errno(errno, "Cannot open kernel image '%s'", path); + return; + } + + _cleanup_close_ int kernel_fd = -EBADF, initrd_fd = -EBADF; + ASSERT_OK(r = kexec_maybe_decompress_kernel(path, fd, &kernel_fd, &initrd_fd)); + + if (r == 0) { + log_info("Kernel image was not compressed (passed through as-is)."); + return; + } + + log_info("Kernel image was decompressed/extracted successfully."); + ASSERT_GE(kernel_fd, 0); + + /* Verify the decompressed result is non-empty and looks plausible */ + struct stat st; + ASSERT_OK_ERRNO(fstat(kernel_fd, &st)); + ASSERT_GT(st.st_size, 0); + log_info("Decompressed kernel size: %zu bytes", (size_t) st.st_size); + + /* Read the first bytes and check for known kernel magic */ + uint8_t magic[8]; + ASSERT_OK_EQ_ERRNO(pread(kernel_fd, magic, sizeof(magic), 0), (ssize_t) sizeof(magic)); + + if (magic[0] == 0x7f && magic[1] == 'E' && magic[2] == 'L' && magic[3] == 'F') + log_info("Decompressed kernel is an ELF image."); + else if (magic[0] == 'M' && magic[1] == 'Z') + log_info("Decompressed kernel is a PE image."); + else + log_info("Decompressed kernel magic: %02x %02x %02x %02x %02x %02x %02x %02x", + magic[0], magic[1], magic[2], magic[3], + magic[4], magic[5], magic[6], magic[7]); + + /* If a UKI initrd was extracted, verify it too */ + if (initrd_fd >= 0) { + ASSERT_OK_ERRNO(fstat(initrd_fd, &st)); + ASSERT_GT(st.st_size, 0); + log_info("Extracted initrd size: %zu bytes", (size_t) st.st_size); + } +} + +DEFINE_TEST_MAIN(LOG_DEBUG);