pe-binary: bound section data against file size, cap UKI zero-padding hash, validate optional header size

A hostile but structurally valid 382-byte PE32+ "EFI application" with a
single section whose VirtualSize is ~4 GiB and SizeOfRawData is 0 drives
uki_hash() into ~4.17 M iterations of SHA-256 over 1024 bytes of zeros
— wedging the parser for >10 s. Nine more slow-units share the same
shape. A separate MSAN finding from the new fuzzer (CIFuzz, memory
sanitizer) shows pe_load_headers() reading uninitialised heap memory
when SizeOfOptionalHeader is too small to actually contain
NumberOfRvaAndSizes.

Three tightenings in src/shared/pe-binary.c:

  1. In pe_load_sections, reject sections whose PointerToRawData +
     SizeOfRawData exceeds the actual file size. Raw section data must
     fit inside the file; this is the parser-wide invariant
     pe_hash / uki_hash / pe_read_section_data rely on.

  2. In uki_hash, cap the (VirtualSize - SizeOfRawData) zero-padding
     hash loop at 64 MiB. Real UKIs do not pad sections with tens of
     MiB of zero-equivalent data; anything above this cap is a
     malformed PE.

  3. In pe_load_headers, reject a PE whose SizeOfOptionalHeader is too
     small to cover up to NumberOfRvaAndSizes. Without this guard the
     subsequent size-mismatch check reads uninitialised optional-header
     bytes, caught by MSAN under CIFuzz.

Add the 382 B canonical reproducer (plus two structural siblings) and
the MSAN reproducer to test/fuzz/fuzz-pe-binary/. Also add a libFuzzer
harness in src/fuzz/fuzz-pe-binary.c and unit tests in
src/test/test-pe-binary.c that exercise each fix branch in isolation.
The 64 MiB hash boundary test is gated behind SYSTEMD_SLOW_TESTS so it
doesn't slow down emulated-arch CI.

This is a robustness fix, not a security fix: PE binaries consumed by
bootctl / systemd-stub / pcrlock / kernel-install / systemd-measure are
already trusted and signed at the consumer side, so the worst pre-fix
behaviour is wasted CPU on a UKI install / measure / inspect call.

Closes #42344.

Reported-by: AI-assisted libFuzzer campaign
Co-developed-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jeffhuang
2026-05-29 04:30:48 +09:00
committed by Yu Watanabe
parent 97a0ec135b
commit 0c5cba6420
11 changed files with 658 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* Fuzz target for userspace UKI / PE inspection.
*
* pe_load_headers / pe_load_sections / pe_read_section_data_by_name / uki_hash
* are exercised by bootctl, systemd-measure, pcrlock, kernel-install and
* reboot-util against UKIs that an unprivileged actor may have produced.
*
* Expected input: bytes that look like a PE/COFF file (DOS "MZ" header,
* `e_lfanew` pointing to a "PE\0\0" signature, followed by IMAGE_FILE_HEADER,
* IMAGE_OPTIONAL_HEADER and a section table). The harness wraps the bytes in
* a memfd, walks the headers, then attempts to read the known UKI sections
* and finally hashes them via uki_hash() when OpenSSL is available.
*/
#include <unistd.h>
#include "alloc-util.h"
#include "crypto-util.h"
#include "fd-util.h"
#include "fuzz.h"
#include "memfd-util.h"
#include "pe-binary.h"
#include "tests.h"
#include "uki.h"
/* Cap section reads so a crafted VirtualSize cannot drive a multi-GiB malloc
* and OOM-kill the fuzzer. Well under the 16 MiB input limit; real code uses
* PE_SECTION_READ_MAX (16 KiB) or smaller. */
#define FUZZ_SECTION_READ_MAX (1U*1024U*1024U)
static void fuzz_read_section(
int fd,
const PeHeader *pe_header,
const IMAGE_SECTION_HEADER *sections,
const char *name) {
_cleanup_free_ void *buf = NULL;
size_t buf_size = 0;
(void) pe_read_section_data_by_name(fd, pe_header, sections, name, FUZZ_SECTION_READ_MAX, &buf, &buf_size);
DO_NOT_OPTIMIZE(buf);
DO_NOT_OPTIMIZE(buf_size);
}
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
_cleanup_free_ IMAGE_DOS_HEADER *dos_header = NULL;
_cleanup_free_ PeHeader *pe_header = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
if (outside_size_range(size, 0, 16 * 1024 * 1024))
return 0;
fuzz_setup_logging();
_cleanup_close_ int fd = ASSERT_OK(memfd_new("fuzz-pe-binary"));
if (size > 0)
ASSERT_OK_EQ_ERRNO(write(fd, data, size), (ssize_t) size);
ASSERT_OK_ERRNO(lseek(fd, 0, SEEK_SET));
if (pe_load_headers(fd, &dos_header, &pe_header) < 0)
return 0;
if (pe_load_sections(fd, dos_header, pe_header, &sections) < 0)
return 0;
/* Exercise the section-read path for every UKI section. unified_sections[]
* (uki.h) is the canonical list; .sdmagic is written by sd-boot/sd-stub but
* is not part of that table, so read it as an extra step. */
FOREACH_ARRAY(s, unified_sections, _UNIFIED_SECTION_MAX)
fuzz_read_section(fd, pe_header, sections, *s);
fuzz_read_section(fd, pe_header, sections, ".sdmagic");
(void) pe_is_uki(pe_header, sections);
(void) pe_is_addon(pe_header, sections);
(void) pe_is_native(pe_header);
#if HAVE_OPENSSL
if (dlopen_libcrypto(LOG_DEBUG) >= 0) {
void *hashes[_UNIFIED_SECTION_MAX] = {};
size_t hash_size = 0;
/* uki_hash() can return partway through with some hashes already
* allocated; free unconditionally. */
(void) uki_hash(fd, sym_EVP_sha256(), hashes, &hash_size);
free_many(hashes, _UNIFIED_SECTION_MAX);
}
#endif
return 0;
}
+1
View File
@@ -8,6 +8,7 @@ simple_fuzzers += files(
'fuzz-env-file.c',
'fuzz-hostname-setup.c',
'fuzz-json.c',
'fuzz-pe-binary.c',
'fuzz-time-util.c',
'fuzz-udev-database.c',
'fuzz-user-record.c',
+42
View File
@@ -13,6 +13,11 @@
#include "string-table.h"
#include "string-util.h"
/* Cap on the (VirtualSize - SizeOfRawData) zero-padding the UKI hasher
* will produce for a single section. Any value beyond this is treated as
* a malformed PE — bounds the hash work an attacker can drive (#42344). */
#define UKI_HASH_VIRTUAL_SIZE_PADDING_MAX (64U * 1024U * 1024U)
/* Note: none of these function change the file position of the provided fd, as they use pread() */
bool pe_header_is_64bit(const PeHeader *h) {
@@ -140,6 +145,12 @@ int pe_load_headers(
if (!IN_SET(le16toh(pe_header->optional.Magic), UINT16_C(0x010B), UINT16_C(0x020B)))
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Optional header magic invalid.");
/* The optional header must be at least large enough to cover everything
* up to and including NumberOfRvaAndSizes — otherwise the equality
* check below would read uninitialised memory for that field. */
if (pe_header_size(pe_header) < PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory))
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Optional header too small.");
if (pe_header_size(pe_header) !=
PE_HEADER_OPTIONAL_FIELD_OFFSET(pe_header, DataDirectory) +
sizeof(IMAGE_DATA_DIRECTORY) * (uint64_t) le32toh(PE_HEADER_OPTIONAL_FIELD(pe_header, NumberOfRvaAndSizes)))
@@ -160,6 +171,7 @@ int pe_load_sections(
IMAGE_SECTION_HEADER **ret_sections) {
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
struct stat st;
size_t nos;
ssize_t n;
@@ -182,6 +194,26 @@ int pe_load_sections(
if ((size_t) n != sizeof(IMAGE_SECTION_HEADER) * nos)
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG), "Short read while reading section table.");
/* The section's raw bytes must fit inside the file. This is the
* fundamental invariant the parser relies on later (pe_hash, uki_hash,
* pe_read_section_data, ...); reject obvious malformations early so
* downstream loops don't get driven by attacker-controlled sizes. */
if (fstat(fd, &st) < 0)
return log_debug_errno(errno, "Failed to stat PE file: %m");
FOREACH_ARRAY(section, sections, nos) {
uint64_t prd = le32toh(section->PointerToRawData), srd = le32toh(section->SizeOfRawData), end;
/* SizeOfRawData == 0 is legitimate (BSS-like, uninitialised) —
* PointerToRawData is then meaningless and not used as an offset. */
if (srd == 0)
continue;
if (!ADD_SAFE(&end, prd, srd) || end > (uint64_t) st.st_size)
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
"PE section raw data exceeds file size.");
}
if (ret_sections)
*ret_sections = TAKE_PTR(sections);
@@ -578,6 +610,16 @@ int uki_hash(int fd,
uint8_t zeroes[1024] = {};
size_t remaining = le32toh(section->VirtualSize) - le32toh(section->SizeOfRawData);
/* Bound the zero-padding hash work. An attacker can otherwise
* set VirtualSize close to UINT32_MAX with SizeOfRawData = 0,
* driving ~4 GiB of SHA-256 work per section on a tiny file
* (issue #42344 — wedges the parser for >10 s on 382 B). */
if (remaining > UKI_HASH_VIRTUAL_SIZE_PADDING_MAX)
return log_debug_errno(SYNTHETIC_ERRNO(EBADMSG),
"Section VirtualSize exceeds SizeOfRawData by %zu bytes (cap %zu); refusing to hash.",
remaining,
(size_t) UKI_HASH_VIRTUAL_SIZE_PADDING_MAX);
while (remaining > 0) {
size_t sz = MIN(sizeof(zeroes), remaining);
+1
View File
@@ -168,6 +168,7 @@ simple_tests += files(
'test-parse-util.c',
'test-path-lookup.c',
'test-path-util.c',
'test-pe-binary.c',
'test-percent-util.c',
'test-pidref.c',
'test-pressure.c',
+523
View File
@@ -0,0 +1,523 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
/* Regression tests for the algorithmic-complexity DoS fix in src/shared/pe-binary.c
* (issue #42344 — PE file with attacker-controlled VirtualSize wedges uki_hash
* in an unbounded SHA-256 zero-padding loop).
*
* Coverage:
* - pe_load_sections() rejects sections whose PointerToRawData + SizeOfRawData
* exceeds the actual file size (PLAN §3.1).
* - uki_hash() refuses to hash a section whose VirtualSize exceeds
* SizeOfRawData by more than UKI_HASH_VIRTUAL_SIZE_PADDING_MAX (=64 MiB)
* and instead returns -EBADMSG (PLAN §3.2).
*
* Tests build small PE32+ images in a memfd. The header layout mirrors the
* canonical 382-byte reproducer (DOS at 0x00, e_lfanew=0x40, PE32+ optional
* header of 0xf0 bytes, section table starting at file offset 0x148) so
* pe_load_headers() accepts every test fixture; only the section table and
* file size are varied per test. */
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "alloc-util.h"
#include "fd-util.h"
#include "memfd-util.h"
#include "pe-binary.h"
#include "tests.h"
#include "uki.h"
#include "unaligned.h"
#if HAVE_OPENSSL
# include "crypto-util.h"
#endif
/* Match the cap defined in src/shared/pe-binary.c (PLAN §3.2). */
#define UKI_HASH_PADDING_CAP_BYTES (64U * 1024U * 1024U)
/* File offsets of the PE32+ header skeleton this test builds. */
#define SECTION_TABLE_OFFSET 0x148
#define IMAGE_FILE_HEADER_NOS_OFFSET 0x46 /* NumberOfSections (le16) */
#define IMAGE_SECTION_HEADER_BYTES 40
/* Header skeleton, byte-for-byte compatible with the canonical reproducer
* so that pe_load_headers() succeeds without further mutation. We only
* patch NumberOfSections at runtime (offset 0x46) and append section
* headers + raw data after the optional header. */
static const uint8_t HEADER_SKELETON[SECTION_TABLE_OFFSET] = {
/* DOS header: "MZ" at 0x00, e_lfanew = 0x40 at 0x3c */
[0x00] = 'M', 'Z',
[0x3c] = 0x40, 0x00, 0x00, 0x00,
/* PE signature at 0x40 */
[0x40] = 'P', 'E', 0x00, 0x00,
/* IMAGE_FILE_HEADER at 0x44 */
[0x44] = 0x64, 0x86, /* Machine = 0x8664 (x86_64) */
/* [0x46]: NumberOfSections — patched per test */
[0x54] = 0xf0, 0x00, /* SizeOfOptionalHeader = 0xf0 */
[0x56] = 0x22, 0x00, /* Characteristics = executable image */
/* IMAGE_OPTIONAL_HEADER (PE32+) at 0x58 */
[0x58] = 0x0b, 0x02, /* Magic = 0x020b (PE32+) */
[0x9c] = 0x0a, 0x00, /* Subsystem = EFI Application */
[0xc4] = 0x10, 0x00, 0x00, 0x00, /* NumberOfRvaAndSizes = 16 (matches SizeOfOptionalHeader=0xf0) */
};
typedef struct SectionSpec {
const char *name; /* PE section name; truncated/padded to 8 bytes on write */
uint32_t virtual_size;
uint32_t virtual_address;
uint32_t size_of_raw_data;
uint32_t pointer_to_raw_data;
} SectionSpec;
/* Build a PE32+ image in a fresh memfd containing the given sections.
*
* If override_file_size > 0, the memfd is written with exactly that many
* bytes (zero-padded if larger than what the sections need). This lets a
* test claim section data past the actual EOF, or reserve headroom past
* the last section. If 0, the file is sized to exactly contain the
* headers + section table + every section's raw data. */
static int build_pe_file(
const SectionSpec *specs,
size_t n_sections,
size_t override_file_size) {
/* Minimum size is "headers + section table"; sections with SizeOfRawData>0
* must additionally fit inside the file. */
size_t base = SECTION_TABLE_OFFSET + n_sections * IMAGE_SECTION_HEADER_BYTES;
size_t needed = base;
FOREACH_ARRAY(spec, specs, n_sections)
if (spec->size_of_raw_data > 0) {
uint64_t end = (uint64_t) spec->pointer_to_raw_data + spec->size_of_raw_data;
if (end > needed)
needed = end;
}
size_t file_size = override_file_size > 0 ? override_file_size : needed;
ASSERT_GE(file_size, base);
_cleanup_free_ uint8_t *buf = ASSERT_NOT_NULL(new0(uint8_t, file_size));
memcpy(buf, HEADER_SKELETON, sizeof(HEADER_SKELETON));
unaligned_write_le16(buf + IMAGE_FILE_HEADER_NOS_OFFSET, (uint16_t) n_sections);
/* Section table at 0x148. */
FOREACH_ARRAY(spec, specs, n_sections) {
uint8_t *sh = buf + SECTION_TABLE_OFFSET + (spec - specs) * IMAGE_SECTION_HEADER_BYTES;
size_t nlen = strnlen(spec->name, 8);
memcpy(sh, spec->name, nlen);
unaligned_write_le32(sh + 8, spec->virtual_size);
unaligned_write_le32(sh + 12, spec->virtual_address);
unaligned_write_le32(sh + 16, spec->size_of_raw_data);
unaligned_write_le32(sh + 20, spec->pointer_to_raw_data);
/* Characteristics, relocs, etc., left zero. */
}
int fd = ASSERT_OK(memfd_new("test-pe-binary"));
/* Write only file_size bytes — that lets the caller construct a
* PE whose section table claims data past EOF. */
ASSERT_OK_EQ_ERRNO(write(fd, buf, file_size), (ssize_t) file_size);
ASSERT_OK_ERRNO(lseek(fd, 0, SEEK_SET));
return fd;
}
static int load_headers_and_sections(
int fd,
IMAGE_DOS_HEADER **ret_dos,
PeHeader **ret_pe,
IMAGE_SECTION_HEADER **ret_sections) {
int r = pe_load_headers(fd, ret_dos, ret_pe);
if (r < 0)
return r;
return pe_load_sections(fd, *ret_dos, *ret_pe, ret_sections);
}
/* ======================================================================
* pe_load_headers — SizeOfOptionalHeader / NumberOfRvaAndSizes bound
* ====================================================================== */
/* If SizeOfOptionalHeader is so small that pread() does not actually
* populate NumberOfRvaAndSizes, the optional-header size check at the end
* of pe_load_headers would read uninitialised heap memory (caught by MSAN
* under CIFuzz). pe_load_headers must reject such files with -EBADMSG
* before touching that field. */
TEST(pe_load_headers_optional_header_too_small) {
/* Minimum allowed by the existing magic check is 2 bytes (only
* IMAGE_OPTIONAL_HEADER.Magic). That is well below the
* NumberOfRvaAndSizes offset for both PE32 and PE32+. */
uint8_t buf[64 + 4 + 20 + 2] = {}; /* DOS + PE sig + file hdr + 2-byte optional */
buf[0] = 'M'; buf[1] = 'Z';
unaligned_write_le32(buf + 0x3c, 64);
memcpy(buf + 64, "PE\0\0", 4);
/* IMAGE_FILE_HEADER: leave most fields zero, set
* SizeOfOptionalHeader = 2 (offset 0x10 within IMAGE_FILE_HEADER). */
unaligned_write_le16(buf + 64 + 4 + 16, 2);
/* Optional header Magic = 0x020B (PE32+). */
unaligned_write_le16(buf + 64 + 4 + 20, 0x020B);
_cleanup_close_ int fd = ASSERT_OK(memfd_new("test-pe-binary"));
ASSERT_OK_EQ_ERRNO(write(fd, buf, sizeof(buf)), (ssize_t) sizeof(buf));
ASSERT_OK_ERRNO(lseek(fd, 0, SEEK_SET));
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
ASSERT_ERROR(pe_load_headers(fd, &dos, &pe), EBADMSG);
}
/* ======================================================================
* pe_load_sections — bounds check from PLAN §3.1
* ====================================================================== */
/* Happy path: a section whose raw data fits comfortably inside the file
* (i.e. file has plenty of trailing headroom beyond the section). */
TEST(pe_load_sections_valid_in_bounds) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = 16,
.size_of_raw_data = 16,
.pointer_to_raw_data = SECTION_TABLE_OFFSET + IMAGE_SECTION_HEADER_BYTES,
};
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, /* override_file_size= */ 4096);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
}
/* Boundary: PointerToRawData + SizeOfRawData == file_size is OK. */
TEST(pe_load_sections_section_exactly_fills_file) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = 16,
.size_of_raw_data = 16,
.pointer_to_raw_data = SECTION_TABLE_OFFSET + IMAGE_SECTION_HEADER_BYTES,
};
size_t exact_size = (size_t) s.pointer_to_raw_data + s.size_of_raw_data;
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, exact_size);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
}
/* Section's raw data claims to extend one byte past EOF — must be rejected. */
TEST(pe_load_sections_section_past_eof_by_one) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = 16,
.size_of_raw_data = 16,
.pointer_to_raw_data = SECTION_TABLE_OFFSET + IMAGE_SECTION_HEADER_BYTES,
};
size_t short_size = (size_t) s.pointer_to_raw_data + s.size_of_raw_data - 1;
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, short_size);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_ERROR(load_headers_and_sections(fd, &dos, &pe, &sections), EBADMSG);
}
/* PointerToRawData itself already past EOF, with non-zero SizeOfRawData. */
TEST(pe_load_sections_pointer_way_past_eof) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = 16,
.size_of_raw_data = 16,
.pointer_to_raw_data = UINT32_C(0x10000000),
};
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, /* override_file_size= */ 4096);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_ERROR(load_headers_and_sections(fd, &dos, &pe, &sections), EBADMSG);
}
/* SizeOfRawData == 0 means "BSS-equivalent / uninitialised"; PointerToRawData
* is a don't-care and must NOT be bounds-checked, even when nonsense. */
TEST(pe_load_sections_size_zero_with_huge_pointer) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = 0x100,
.size_of_raw_data = 0,
.pointer_to_raw_data = UINT32_MAX,
};
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
}
/* SizeOfRawData == 0 with PointerToRawData == 0 — most natural BSS case. */
TEST(pe_load_sections_size_zero_pointer_zero) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = 0x40,
.size_of_raw_data = 0,
.pointer_to_raw_data = 0,
};
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
}
/* PointerToRawData + SizeOfRawData overflows uint32 (both near UINT32_MAX).
* The fix uses uint64 arithmetic + __builtin_add_overflow, so this must be
* rejected, not silently wrap. */
TEST(pe_load_sections_uint32_overflow_sum) {
SectionSpec s = {
.name = ".initrd",
.virtual_size = UINT32_C(0xffffffff),
.size_of_raw_data = UINT32_C(0x80000000),
.pointer_to_raw_data = UINT32_C(0x80000000),
};
/* uint32 sum wraps to 0, but uint64 sum = 0x100000000 — must still
* be rejected against a tiny file. */
_cleanup_close_ int fd = build_pe_file(&s, /* n_sections= */ 1, /* override_file_size= */ 4096);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_ERROR(load_headers_and_sections(fd, &dos, &pe, &sections), EBADMSG);
}
/* Zero sections: bounds check loop runs zero times, fstat still succeeds. */
TEST(pe_load_sections_zero_sections) {
_cleanup_close_ int fd = build_pe_file(NULL, /* n_sections= */ 0, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
}
/* Multiple sections, all valid — happy path with N>1. */
TEST(pe_load_sections_multi_all_valid) {
size_t base = SECTION_TABLE_OFFSET + 3 * IMAGE_SECTION_HEADER_BYTES;
SectionSpec specs[3] = {
{ .name = ".linux", .virtual_size = 16, .size_of_raw_data = 16,
.pointer_to_raw_data = (uint32_t) base },
{ .name = ".initrd", .virtual_size = 8, .size_of_raw_data = 8,
.pointer_to_raw_data = (uint32_t) (base + 16) },
{ .name = ".cmdline", .virtual_size = 0, .size_of_raw_data = 0,
.pointer_to_raw_data = 0 },
};
_cleanup_close_ int fd = build_pe_file(specs, /* n_sections= */ 3, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
}
/* Multiple sections; one of them claims data past EOF — the whole file
* must be rejected, not just that section silently skipped. */
TEST(pe_load_sections_multi_one_past_eof) {
size_t base = SECTION_TABLE_OFFSET + 3 * IMAGE_SECTION_HEADER_BYTES;
SectionSpec specs[3] = {
{ .name = ".linux", .virtual_size = 16, .size_of_raw_data = 16,
.pointer_to_raw_data = (uint32_t) base },
{ .name = ".initrd", .virtual_size = 8, .size_of_raw_data = 8,
.pointer_to_raw_data = (uint32_t) (base + 16) },
{ .name = ".cmdline", .virtual_size = 32, .size_of_raw_data = 32,
.pointer_to_raw_data = UINT32_C(0x40000000) }, /* nonsense */
};
_cleanup_close_ int fd = build_pe_file(specs, /* n_sections= */ 3, /* override_file_size= */ 4096);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_ERROR(load_headers_and_sections(fd, &dos, &pe, &sections), EBADMSG);
}
/* ======================================================================
* uki_hash — zero-padding cap from PLAN §3.2
* ====================================================================== */
#if HAVE_OPENSSL
/* Helper: call uki_hash on a freshly built single-section PE. Returns the
* uki_hash() return value; any allocated hashes are freed. */
static int call_uki_hash_with(SectionSpec spec) {
_cleanup_close_ int fd = build_pe_file(&spec, /* n_sections= */ 1, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
int r = load_headers_and_sections(fd, &dos, &pe, &sections);
if (r < 0)
return r;
void *hashes[_UNIFIED_SECTION_MAX] = {};
size_t hash_size = 0;
r = uki_hash(fd, sym_EVP_sha256(), hashes, &hash_size);
free_many(hashes, _UNIFIED_SECTION_MAX);
return r;
}
/* No padding needed (VirtualSize == SizeOfRawData) — the cap branch is
* skipped entirely; hash succeeds. */
TEST_RET(uki_hash_no_padding) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
SectionSpec s = {
.name = ".initrd",
.virtual_size = 16,
.size_of_raw_data = 16,
.pointer_to_raw_data = SECTION_TABLE_OFFSET + IMAGE_SECTION_HEADER_BYTES,
};
ASSERT_OK(call_uki_hash_with(s));
return EXIT_SUCCESS;
}
/* One-byte padding — well below the cap. Must succeed. */
TEST_RET(uki_hash_one_byte_padding) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
SectionSpec s = {
.name = ".initrd",
.virtual_size = 17,
.size_of_raw_data = 16,
.pointer_to_raw_data = SECTION_TABLE_OFFSET + IMAGE_SECTION_HEADER_BYTES,
};
ASSERT_OK(call_uki_hash_with(s));
return EXIT_SUCCESS;
}
/* Boundary: VirtualSize - SizeOfRawData == cap (64 MiB) — must succeed
* (PLAN §4 says the comparison is strict ">"). SHA-256 of 64 MiB of zeros
* is ~100–200 ms on a modern CPU but can be much slower on emulated arches,
* so gate behind the slow-tests opt-in. */
TEST_RET(uki_hash_at_cap_boundary) {
if (!slow_tests_enabled())
return log_tests_skipped("slow tests disabled");
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
SectionSpec s = {
.name = ".initrd",
.virtual_size = UKI_HASH_PADDING_CAP_BYTES,
.size_of_raw_data = 0,
.pointer_to_raw_data = 0,
};
ASSERT_OK(call_uki_hash_with(s));
return EXIT_SUCCESS;
}
/* One byte over the cap — must be rejected with -EBADMSG. */
TEST_RET(uki_hash_one_over_cap_rejected) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
SectionSpec s = {
.name = ".initrd",
.virtual_size = UKI_HASH_PADDING_CAP_BYTES + 1,
.size_of_raw_data = 0,
.pointer_to_raw_data = 0,
};
ASSERT_ERROR(call_uki_hash_with(s), EBADMSG);
return EXIT_SUCCESS;
}
/* VirtualSize == UINT32_MAX, SizeOfRawData == 0. This is the worst-case
* shape an attacker can produce: ~4 GiB of zero-padding. Without the cap
* this is the >10 s wedge from #42344; with the cap it must return
* -EBADMSG essentially instantly. */
TEST_RET(uki_hash_max_virtual_size_rejected) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
SectionSpec s = {
.name = ".initrd",
.virtual_size = UINT32_MAX,
.size_of_raw_data = 0,
.pointer_to_raw_data = 0,
};
ASSERT_ERROR(call_uki_hash_with(s), EBADMSG);
return EXIT_SUCCESS;
}
/* Reproduces the exact section shape of the canonical 382-byte slow-unit
* (.initrd VS=0xff000000 RSD=0). Pre-fix this drove ~8.5 s of SHA-256; the
* cap must reject it instantly. This is the in-code analogue of the
* regression input pinned in test/fuzz/fuzz-pe-binary/. */
TEST_RET(uki_hash_canonical_42344_reproducer_pattern) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
SectionSpec s = {
.name = ".initrd",
.virtual_size = UINT32_C(0xff000000),
.size_of_raw_data = 0,
.pointer_to_raw_data = 0,
};
ASSERT_ERROR(call_uki_hash_with(s), EBADMSG);
return EXIT_SUCCESS;
}
/* Multiple sections, one of which trips the cap — uki_hash() must abort
* the entire call (return -EBADMSG) rather than silently skipping. */
TEST_RET(uki_hash_one_bad_among_many_rejected) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
size_t base = SECTION_TABLE_OFFSET + 2 * IMAGE_SECTION_HEADER_BYTES;
SectionSpec specs[2] = {
{ .name = ".linux", .virtual_size = 16, .size_of_raw_data = 16,
.pointer_to_raw_data = (uint32_t) base },
{ .name = ".initrd", .virtual_size = UKI_HASH_PADDING_CAP_BYTES + 1,
.size_of_raw_data = 0, .pointer_to_raw_data = 0 },
};
_cleanup_close_ int fd = build_pe_file(specs, /* n_sections= */ 2, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
void *hashes[_UNIFIED_SECTION_MAX] = {};
size_t hash_size = 0;
ASSERT_ERROR(uki_hash(fd, sym_EVP_sha256(), hashes, &hash_size), EBADMSG);
free_many(hashes, _UNIFIED_SECTION_MAX);
return EXIT_SUCCESS;
}
/* Sanity check: uki_hash on a "no UKI sections present" PE doesn't hit
* the cap path at all (no section with VS>SRD that matches the unified
* sections table). Just confirms the cap doesn't reject the empty case. */
TEST_RET(uki_hash_empty_pe_does_not_reject) {
if (dlopen_libcrypto(LOG_DEBUG) < 0)
return log_tests_skipped("openssl not available");
_cleanup_close_ int fd = build_pe_file(NULL, /* n_sections= */ 0, /* override_file_size= */ 0);
_cleanup_free_ IMAGE_DOS_HEADER *dos = NULL;
_cleanup_free_ PeHeader *pe = NULL;
_cleanup_free_ IMAGE_SECTION_HEADER *sections = NULL;
ASSERT_OK(load_headers_and_sections(fd, &dos, &pe, &sections));
void *hashes[_UNIFIED_SECTION_MAX] = {};
size_t hash_size = 0;
/* No UKI sections to hash — succeeds without hitting the cap path. */
ASSERT_OK(uki_hash(fd, sym_EVP_sha256(), hashes, &hash_size));
free_many(hashes, _UNIFIED_SECTION_MAX);
return EXIT_SUCCESS;
}
#endif /* HAVE_OPENSSL */
DEFINE_TEST_MAIN(LOG_DEBUG);
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.