From f75e4ec319dbed21de72e0f4b52752de513332ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20L=C3=B3pez?= Date: Fri, 9 Jan 2026 00:43:03 +0100 Subject: [PATCH 1/2] netboot: take const pointer in parseNetbootinfo() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The name passed to parseNetbootinfo() is never modified, since all changes are made in a separate copy of the string, so mark the parameter as const. Signed-off-by: Carlos López --- include/netboot.h | 2 +- netboot.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/include/netboot.h b/include/netboot.h index 296f10f00..20fcf5a03 100644 --- a/include/netboot.h +++ b/include/netboot.h @@ -7,7 +7,7 @@ extern BOOLEAN findNetboot(EFI_HANDLE image_handle); -extern EFI_STATUS parseNetbootinfo(EFI_HANDLE image_handle, CHAR8 *name); +extern EFI_STATUS parseNetbootinfo(EFI_HANDLE image_handle, CONST CHAR8 *name); extern EFI_STATUS FetchNetbootimage(EFI_HANDLE image_handle, VOID **buffer, UINT64 *bufsiz, int flags); diff --git a/netboot.c b/netboot.c index 0ec43e5a6..c3bb2636e 100644 --- a/netboot.c +++ b/netboot.c @@ -193,7 +193,7 @@ static CHAR8 *str2ip6(CHAR8 *str) return (CHAR8 *)ip; } -static BOOLEAN extract_tftp_info(CHAR8 *url, CHAR8 *name) +static BOOLEAN extract_tftp_info(CHAR8 *url, CONST CHAR8 *name) { CHAR8 *start, *end; CHAR8 ip6str[40]; @@ -259,7 +259,7 @@ static BOOLEAN extract_tftp_info(CHAR8 *url, CHAR8 *name) return TRUE; } -static EFI_STATUS parseDhcp6(CHAR8 *name) +static EFI_STATUS parseDhcp6(CONST CHAR8 *name) { EFI_PXE_BASE_CODE_DHCPV6_PACKET *packet = (EFI_PXE_BASE_CODE_DHCPV6_PACKET *)&pxe->Mode->DhcpAck.Raw; CHAR8 *bootfile_url; @@ -275,7 +275,7 @@ static EFI_STATUS parseDhcp6(CHAR8 *name) return EFI_SUCCESS; } -static EFI_STATUS parseDhcp4(CHAR8 *name) +static EFI_STATUS parseDhcp4(CONST CHAR8 *name) { CHAR8 *template; INTN template_len = 0; @@ -345,7 +345,7 @@ static EFI_STATUS parseDhcp4(CHAR8 *name) return EFI_SUCCESS; } -EFI_STATUS parseNetbootinfo(EFI_HANDLE image_handle UNUSED, CHAR8 *netbootname) +EFI_STATUS parseNetbootinfo(EFI_HANDLE image_handle UNUSED, CONST CHAR8 *netbootname) { EFI_STATUS efi_status; From 478f83a3d18b3770594558bd644ef137814fef81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20L=C3=B3pez?= Date: Fri, 9 Jan 2026 01:27:09 +0100 Subject: [PATCH 2/2] netboot: add fuzzer for TFTP network boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a fuzzer for the netboot code, which focuses on its two public APIs: `parseNetbootinfo()` and `FetchNetbootimage()`. It stubs out the global PXE protocol handle to feed in bytes from libfuzzer into the netboot code, which means that this variable cannot be static when building the fuzzer. On top of that, the full_path static is allocated once and never freed, which is not problematic in normal operation, but triggers address sanitizer's leak detector, so expose it as well so that the harness can free the memory after each run. Add as well a dictionary with some magic strings and bytes that help get coverage faster. After a couple hours running and getting practically full coverage (verified with llvm-cov) the fuzzer luckily found no issues. Signed-off-by: Carlos López --- data/netboot-fuzz-dict.txt | 7 ++ fuzz-netboot.c | 214 +++++++++++++++++++++++++++++++++++++ include/fuzz.mk | 3 + netboot.c | 11 +- 4 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 data/netboot-fuzz-dict.txt create mode 100644 fuzz-netboot.c diff --git a/data/netboot-fuzz-dict.txt b/data/netboot-fuzz-dict.txt new file mode 100644 index 000000000..63cc5d257 --- /dev/null +++ b/data/netboot-fuzz-dict.txt @@ -0,0 +1,7 @@ +"tftp://" +"tftp://[" +"tftp://[]" +"tftp://[ABCD]" +# TFTP bootfile URL option, in network byteorder +"\x3b\x00" +"/" diff --git a/fuzz-netboot.c b/fuzz-netboot.c new file mode 100644 index 000000000..c88d9488d --- /dev/null +++ b/fuzz-netboot.c @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: BSD-2-Clause-Patent +/* + * fuzz-netboot.c - fuzz TFTP netboot code. + */ +#include +#include + +#ifndef SHIM_UNIT_TEST +#define SHIM_UNIT_TEST +#endif + +#include "shim.h" + +extern EFI_PXE_BASE_CODE *pxe; +extern CHAR8 *full_path; + +UINT8 mok_policy = 0; +UINTN hsi_status = 0; + +/* A struct to track fuzzing input bytes */ +typedef struct { + const uint8_t *data; + size_t len; +} state_t; + +/* Consumes `len` fuzzing input bytes into `dst` */ +static int +fuzzer_consume_bytes(state_t *state, void *dst, size_t len) +{ + if (state->len < len) + return 1; + + memcpy(dst, state->data, len); + state->data += len; + state->len -= len; + return 0; +} + +/* Returns a random length of bytes that `state` is guaranteed to + * be able to satisfy */ +static int +fuzzer_consume_len(state_t *state, size_t *len) +{ + if (state->len <= sizeof(*len)) + return 1; + + fuzzer_consume_bytes(state, len, sizeof(*len)); + *len %= state->len; + + return 0; +} + +/* Consumes a `BOOLEAN` from the fuzzing input bytes */ +static int +fuzzer_consume_bool(state_t *state, BOOLEAN *b) +{ + int ret, val = 0; + + ret = fuzzer_consume_bytes(state, &val, 1); + if (!ret) + *b = val & 1; + return ret; +} + +/* Global fuzzing state, set from LLVMFuzzerTestOneInput() so that + * mtftp_xfer() can access input bytes */ +static state_t *gstate = NULL; + +static EFI_STATUS EFIAPI +mtftp_xfer(struct _EFI_PXE_BASE_CODE_PROTOCOL *pxe, + EFI_PXE_BASE_CODE_TFTP_OPCODE op, VOID *buf, + BOOLEAN overwrite UNUSED, UINT64 *bufsize, UINT64 *blocksize UNUSED, + EFI_IP_ADDRESS *addr UNUSED, UINT8 *filename UNUSED, + EFI_PXE_BASE_CODE_MTFTP_INFO *info UNUSED, BOOLEAN dontusebuf UNUSED) +{ + EFI_STATUS status; + size_t size; + unsigned int i; + EFI_PXE_BASE_CODE_TFTP_ERROR *error; + uint8_t c; + + if (op != EFI_PXE_BASE_CODE_TFTP_READ_FILE) { + status = EFI_UNSUPPORTED; + goto out_err; + } + + if (fuzzer_consume_len(gstate, &size)) { + status = EFI_TFTP_ERROR; + goto out_err; + } + + if (*bufsize < size) { + status = EFI_BUFFER_TOO_SMALL; + goto out_err; + } + + fuzzer_consume_bytes(gstate, buf, size); + + *bufsize = size; + return EFI_SUCCESS; + +out_err: + pxe->Mode->TftpErrorReceived = 1; + error = &pxe->Mode->TftpError; + for (i = 0; + i < sizeof(error->ErrorString) / sizeof(error->ErrorString[0]); + ++i) { + if (fuzzer_consume_bytes(gstate, &c, sizeof(c))) + error->ErrorString[i] = c; + } + return status; +} + +static int +fuzzer_init_mode(state_t *state, EFI_PXE_BASE_CODE_MODE *mode) +{ +#define FUZZ_GET_BOOL(_state, _dst) \ + do { \ + if (fuzzer_consume_bool(_state, _dst) != 0) \ + goto out; \ + } while (0) + +#define FUZZ_GET_BYTES(_state, _dst) \ + do { \ + if (fuzzer_consume_bytes(_state, _dst, sizeof(*_dst)) != 0) \ + goto out; \ + } while (0) + + memset(mode, 0, sizeof(*mode)); + + FUZZ_GET_BOOL(state, &mode->DhcpAckReceived); + FUZZ_GET_BOOL(state, &mode->ProxyOfferReceived); + FUZZ_GET_BOOL(state, &mode->PxeReplyReceived); + FUZZ_GET_BOOL(state, &mode->UsingIpv6); + + if (mode->UsingIpv6) { + FUZZ_GET_BYTES(state, &mode->DhcpAck.Dhcpv6); + FUZZ_GET_BYTES(state, &mode->PxeReply.Dhcpv6); + FUZZ_GET_BYTES(state, &mode->ProxyOffer.Dhcpv6); + } else { + FUZZ_GET_BYTES(state, &mode->DhcpAck.Dhcpv4); + FUZZ_GET_BYTES(state, &mode->PxeReply.Dhcpv4); + FUZZ_GET_BYTES(state, &mode->ProxyOffer.Dhcpv4); + } + + return 0; + +out: + return -1; +} + +static char * +fuzzer_create_name(state_t *state) +{ + char *name; + size_t name_len = 0; + + if (fuzzer_consume_len(state, &name_len) || !name_len) + return NULL; + + name = calloc(1, name_len); + if (name) + fuzzer_consume_bytes(state, name, name_len - 1); + + return name; +} + +static int +fuzzer_main(state_t *state) +{ + EFI_STATUS status; + char *name = NULL, *netbootname; + void *sourcebuffer = NULL; + UINT64 sourcesize; + + if (fuzzer_init_mode(state, pxe->Mode)) + return -1; + + name = fuzzer_create_name(state); + netbootname = name ? name : "boot64.efi"; + + status = parseNetbootinfo(NULL, netbootname); + if (EFI_ERROR(status)) + goto out; + + status = FetchNetbootimage(NULL, &sourcebuffer, &sourcesize, 0); + if (!EFI_ERROR(status)) + FreePool(sourcebuffer); + +out: + if (full_path) { + FreePool(full_path); + full_path = NULL; + } + if (name) + free(name); + return 0; +} + +static EFI_PXE_BASE_CODE_MODE fuzz_mode = { 0 }; + +static EFI_PXE_BASE_CODE fuzz_pxe = { + .Mtftp = mtftp_xfer, + .Mode = &fuzz_mode, +}; + +int +LLVMFuzzerTestOneInput(const UINT8 *data, size_t size) +{ + state_t state = { .data = data, .len = size }; + pxe = &fuzz_pxe; + gstate = &state; + return fuzzer_main(&state); +} diff --git a/include/fuzz.mk b/include/fuzz.mk index 1cec6c561..517bb5975 100644 --- a/include/fuzz.mk +++ b/include/fuzz.mk @@ -72,6 +72,9 @@ libefi-test.a : fuzz-sbat_FILES = csv.c lib/variables.c lib/guid.c sbat_var.S mock-variables.c fuzz-sbat :: CFLAGS+=-DHAVE_GET_VARIABLE -DHAVE_GET_VARIABLE_ATTR -DHAVE_SHIM_LOCK_GUID +fuzz-netboot_FILES = lib/string.c +fuzz-netboot :: FUZZ_ARGS+=-dict=$(TOPDIR)/data/netboot-fuzz-dict.txt + fuzzers := $(patsubst %.c,%,$(wildcard fuzz-*.c)) $(fuzzers) :: fuzz-% : | libefi-test.a diff --git a/netboot.c b/netboot.c index c3bb2636e..50f9d62a7 100644 --- a/netboot.c +++ b/netboot.c @@ -26,9 +26,16 @@ #define TFTP_ERROR_EXISTS 6 /* File already exists. */ #define TFTP_ERROR_NO_USER 7 /* No such user. */ -static EFI_PXE_BASE_CODE *pxe; +/* Fuzzing harness needs access to some variables that are normally static */ +#ifdef SHIM_ENABLE_LIBFUZZER +#define __expose_libfuzzer +#else +#define __expose_libfuzzer static +#endif + +__expose_libfuzzer EFI_PXE_BASE_CODE *pxe; static EFI_IP_ADDRESS tftp_addr; -static CHAR8 *full_path; +__expose_libfuzzer CHAR8 *full_path; typedef struct {