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>
92 lines
3.3 KiB
C
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, §ions) < 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;
|
|
}
|