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/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..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 { @@ -193,7 +200,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 +266,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 +282,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 +352,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;