Files
systemd/src/fuzz/fuzz-pe-binary.c
T
jeffhuang 0c5cba6420 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>
2026-05-29 04:30:48 +09:00

92 lines
3.3 KiB
C

/* 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;
}