Skip to content

Commit 478f83a

Browse files
committed
netboot: add fuzzer for TFTP network boot
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 <carlos.lopezr4096@gmail.com>
1 parent f75e4ec commit 478f83a

4 files changed

Lines changed: 233 additions & 2 deletions

File tree

data/netboot-fuzz-dict.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"tftp://"
2+
"tftp://["
3+
"tftp://[]"
4+
"tftp://[ABCD]"
5+
# TFTP bootfile URL option, in network byteorder
6+
"\x3b\x00"
7+
"/"

fuzz-netboot.c

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// SPDX-License-Identifier: BSD-2-Clause-Patent
2+
/*
3+
* fuzz-netboot.c - fuzz TFTP netboot code.
4+
*/
5+
#include <stdint.h>
6+
#include <stdio.h>
7+
8+
#ifndef SHIM_UNIT_TEST
9+
#define SHIM_UNIT_TEST
10+
#endif
11+
12+
#include "shim.h"
13+
14+
extern EFI_PXE_BASE_CODE *pxe;
15+
extern CHAR8 *full_path;
16+
17+
UINT8 mok_policy = 0;
18+
UINTN hsi_status = 0;
19+
20+
/* A struct to track fuzzing input bytes */
21+
typedef struct {
22+
const uint8_t *data;
23+
size_t len;
24+
} state_t;
25+
26+
/* Consumes `len` fuzzing input bytes into `dst` */
27+
static int
28+
fuzzer_consume_bytes(state_t *state, void *dst, size_t len)
29+
{
30+
if (state->len < len)
31+
return 1;
32+
33+
memcpy(dst, state->data, len);
34+
state->data += len;
35+
state->len -= len;
36+
return 0;
37+
}
38+
39+
/* Returns a random length of bytes that `state` is guaranteed to
40+
* be able to satisfy */
41+
static int
42+
fuzzer_consume_len(state_t *state, size_t *len)
43+
{
44+
if (state->len <= sizeof(*len))
45+
return 1;
46+
47+
fuzzer_consume_bytes(state, len, sizeof(*len));
48+
*len %= state->len;
49+
50+
return 0;
51+
}
52+
53+
/* Consumes a `BOOLEAN` from the fuzzing input bytes */
54+
static int
55+
fuzzer_consume_bool(state_t *state, BOOLEAN *b)
56+
{
57+
int ret, val = 0;
58+
59+
ret = fuzzer_consume_bytes(state, &val, 1);
60+
if (!ret)
61+
*b = val & 1;
62+
return ret;
63+
}
64+
65+
/* Global fuzzing state, set from LLVMFuzzerTestOneInput() so that
66+
* mtftp_xfer() can access input bytes */
67+
static state_t *gstate = NULL;
68+
69+
static EFI_STATUS EFIAPI
70+
mtftp_xfer(struct _EFI_PXE_BASE_CODE_PROTOCOL *pxe,
71+
EFI_PXE_BASE_CODE_TFTP_OPCODE op, VOID *buf,
72+
BOOLEAN overwrite UNUSED, UINT64 *bufsize, UINT64 *blocksize UNUSED,
73+
EFI_IP_ADDRESS *addr UNUSED, UINT8 *filename UNUSED,
74+
EFI_PXE_BASE_CODE_MTFTP_INFO *info UNUSED, BOOLEAN dontusebuf UNUSED)
75+
{
76+
EFI_STATUS status;
77+
size_t size;
78+
unsigned int i;
79+
EFI_PXE_BASE_CODE_TFTP_ERROR *error;
80+
uint8_t c;
81+
82+
if (op != EFI_PXE_BASE_CODE_TFTP_READ_FILE) {
83+
status = EFI_UNSUPPORTED;
84+
goto out_err;
85+
}
86+
87+
if (fuzzer_consume_len(gstate, &size)) {
88+
status = EFI_TFTP_ERROR;
89+
goto out_err;
90+
}
91+
92+
if (*bufsize < size) {
93+
status = EFI_BUFFER_TOO_SMALL;
94+
goto out_err;
95+
}
96+
97+
fuzzer_consume_bytes(gstate, buf, size);
98+
99+
*bufsize = size;
100+
return EFI_SUCCESS;
101+
102+
out_err:
103+
pxe->Mode->TftpErrorReceived = 1;
104+
error = &pxe->Mode->TftpError;
105+
for (i = 0;
106+
i < sizeof(error->ErrorString) / sizeof(error->ErrorString[0]);
107+
++i) {
108+
if (fuzzer_consume_bytes(gstate, &c, sizeof(c)))
109+
error->ErrorString[i] = c;
110+
}
111+
return status;
112+
}
113+
114+
static int
115+
fuzzer_init_mode(state_t *state, EFI_PXE_BASE_CODE_MODE *mode)
116+
{
117+
#define FUZZ_GET_BOOL(_state, _dst) \
118+
do { \
119+
if (fuzzer_consume_bool(_state, _dst) != 0) \
120+
goto out; \
121+
} while (0)
122+
123+
#define FUZZ_GET_BYTES(_state, _dst) \
124+
do { \
125+
if (fuzzer_consume_bytes(_state, _dst, sizeof(*_dst)) != 0) \
126+
goto out; \
127+
} while (0)
128+
129+
memset(mode, 0, sizeof(*mode));
130+
131+
FUZZ_GET_BOOL(state, &mode->DhcpAckReceived);
132+
FUZZ_GET_BOOL(state, &mode->ProxyOfferReceived);
133+
FUZZ_GET_BOOL(state, &mode->PxeReplyReceived);
134+
FUZZ_GET_BOOL(state, &mode->UsingIpv6);
135+
136+
if (mode->UsingIpv6) {
137+
FUZZ_GET_BYTES(state, &mode->DhcpAck.Dhcpv6);
138+
FUZZ_GET_BYTES(state, &mode->PxeReply.Dhcpv6);
139+
FUZZ_GET_BYTES(state, &mode->ProxyOffer.Dhcpv6);
140+
} else {
141+
FUZZ_GET_BYTES(state, &mode->DhcpAck.Dhcpv4);
142+
FUZZ_GET_BYTES(state, &mode->PxeReply.Dhcpv4);
143+
FUZZ_GET_BYTES(state, &mode->ProxyOffer.Dhcpv4);
144+
}
145+
146+
return 0;
147+
148+
out:
149+
return -1;
150+
}
151+
152+
static char *
153+
fuzzer_create_name(state_t *state)
154+
{
155+
char *name;
156+
size_t name_len = 0;
157+
158+
if (fuzzer_consume_len(state, &name_len) || !name_len)
159+
return NULL;
160+
161+
name = calloc(1, name_len);
162+
if (name)
163+
fuzzer_consume_bytes(state, name, name_len - 1);
164+
165+
return name;
166+
}
167+
168+
static int
169+
fuzzer_main(state_t *state)
170+
{
171+
EFI_STATUS status;
172+
char *name = NULL, *netbootname;
173+
void *sourcebuffer = NULL;
174+
UINT64 sourcesize;
175+
176+
if (fuzzer_init_mode(state, pxe->Mode))
177+
return -1;
178+
179+
name = fuzzer_create_name(state);
180+
netbootname = name ? name : "boot64.efi";
181+
182+
status = parseNetbootinfo(NULL, netbootname);
183+
if (EFI_ERROR(status))
184+
goto out;
185+
186+
status = FetchNetbootimage(NULL, &sourcebuffer, &sourcesize, 0);
187+
if (!EFI_ERROR(status))
188+
FreePool(sourcebuffer);
189+
190+
out:
191+
if (full_path) {
192+
FreePool(full_path);
193+
full_path = NULL;
194+
}
195+
if (name)
196+
free(name);
197+
return 0;
198+
}
199+
200+
static EFI_PXE_BASE_CODE_MODE fuzz_mode = { 0 };
201+
202+
static EFI_PXE_BASE_CODE fuzz_pxe = {
203+
.Mtftp = mtftp_xfer,
204+
.Mode = &fuzz_mode,
205+
};
206+
207+
int
208+
LLVMFuzzerTestOneInput(const UINT8 *data, size_t size)
209+
{
210+
state_t state = { .data = data, .len = size };
211+
pxe = &fuzz_pxe;
212+
gstate = &state;
213+
return fuzzer_main(&state);
214+
}

include/fuzz.mk

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ libefi-test.a :
7272
fuzz-sbat_FILES = csv.c lib/variables.c lib/guid.c sbat_var.S mock-variables.c
7373
fuzz-sbat :: CFLAGS+=-DHAVE_GET_VARIABLE -DHAVE_GET_VARIABLE_ATTR -DHAVE_SHIM_LOCK_GUID
7474

75+
fuzz-netboot_FILES = lib/string.c
76+
fuzz-netboot :: FUZZ_ARGS+=-dict=$(TOPDIR)/data/netboot-fuzz-dict.txt
77+
7578
fuzzers := $(patsubst %.c,%,$(wildcard fuzz-*.c))
7679

7780
$(fuzzers) :: fuzz-% : | libefi-test.a

netboot.c

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@
2626
#define TFTP_ERROR_EXISTS 6 /* File already exists. */
2727
#define TFTP_ERROR_NO_USER 7 /* No such user. */
2828

29-
static EFI_PXE_BASE_CODE *pxe;
29+
/* Fuzzing harness needs access to some variables that are normally static */
30+
#ifdef SHIM_ENABLE_LIBFUZZER
31+
#define __expose_libfuzzer
32+
#else
33+
#define __expose_libfuzzer static
34+
#endif
35+
36+
__expose_libfuzzer EFI_PXE_BASE_CODE *pxe;
3037
static EFI_IP_ADDRESS tftp_addr;
31-
static CHAR8 *full_path;
38+
__expose_libfuzzer CHAR8 *full_path;
3239

3340

3441
typedef struct {

0 commit comments

Comments
 (0)