--- fuzzing/Makefile.am | 6 +- fuzzing/libnbd-fuzz-wrapper.c | 398 ++++++++++++++++++++------------ fuzzing/libnbd-libfuzzer-test.c | 4 + 3 files changed, 253 insertions(+), 155 deletions(-)
diff --git a/fuzzing/Makefile.am b/fuzzing/Makefile.am index 2d46cd0..928cdc9 100644 --- a/fuzzing/Makefile.am +++ b/fuzzing/Makefile.am @@ -38,10 +38,14 @@ libnbd_fuzz_wrapper_CPPFLAGS = \ -I$(top_srcdir)/common/include \ -I$(top_srcdir)/common/utils \ $(NULL) -libnbd_fuzz_wrapper_CFLAGS = $(WARNINGS_CFLAGS) +libnbd_fuzz_wrapper_CFLAGS = \ + $(WARNINGS_CFLAGS) \ + $(PTHREAD_CFLAGS) \ + $(NULL) libnbd_fuzz_wrapper_LDADD = \ $(top_builddir)/common/utils/libutils.la \ $(top_builddir)/lib/libnbd.la \ + $(PTHREAD_LIBS) \ $(NULL) libnbd_libfuzzer_test_SOURCES = libnbd-libfuzzer-test.c diff --git a/fuzzing/libnbd-fuzz-wrapper.c b/fuzzing/libnbd-fuzz-wrapper.c index 338adc0..10f10e4 100644 --- a/fuzzing/libnbd-fuzz-wrapper.c +++ b/fuzzing/libnbd-fuzz-wrapper.c @@ -17,10 +17,38 @@ */ /* This is a wrapper allowing libnbd to be tested using common fuzzers - * such as afl. It takes the fuzzer test case as a filename on the - * command line. This is fed to the libnbd socket. Any output to the - * socket from libnbd is sent to /dev/null. This is basically the - * same way we fuzz nbdkit, but in reverse (see nbdkit.git/fuzzing). + * such as AFL++. It takes the fuzzer test case as a filename on the + * command line. + * + * It uses fuzzed-data-provider.h to parse the input allowing a choice + * of APIs to be called in any order under control of the fuzzer. The + * test cases therefore do not correspond very closely to raw NBD + * protocol. + * + * The fuzzer input is parsed as: + * + * <initial server buffer> (includes implicit length) + * zero or more <command>s + * + * The <initial server buffer> field is the data that is written back + * to libnbd over the socket. The first buffer will contain the + * initial NBD handshake. (Any data sent by libnbd over the socket is + * discarded.) + * + * The series of <command>s directs the program to execute different + * libnbd APIs. When the input is exhausted we stop the test. Each + * <command> is: + * + * <enum of API to call> + * <offset> + * <flags> + * <data buffer> (includes implicit length) + * <server buffer> (includes implicit length) + * + * The <data buffer> field is the buffer used by the libnbd API. The + * actual data in this buffer is only used by nbd_aio_pwrite. For + * other APIs only the length is used and the content is ignored. The + * <server buffer> is more data to send to libnbd if required. */ #include <config.h> @@ -31,86 +59,84 @@ #include <stdint.h> #include <inttypes.h> #include <string.h> -#include <fcntl.h> #include <unistd.h> -#include <poll.h> +#include <fcntl.h> #include <errno.h> -#include <sys/types.h> -#include <sys/socket.h> -#include <sys/wait.h> +#include <sys/stat.h> + +#include <pthread.h> #include <libnbd.h> +#include "minmax.h" +#include "vector.h" + +#include "fuzzed-data-provider.h" + #ifndef SOCK_CLOEXEC #define SOCK_CLOEXEC 0 /* This file doesn't use exec */ #endif -static void client (int s); -static void server (int fd, int s); +/* The test case from the fuzzer. This is loaded from the input file + * given on the command line. + */ +byte_vector fuzzed_data; + +static void *writer_thread (void *); +static void send_to_writer_thread (byte_vector v); +static void end_writer_thread (void); +static void do_test (int sock); int main (int argc, char *argv[]) { - int fd; - pid_t pid; - int sv[2], r, status; + int fd, err; + struct stat statbuf; + int sv[2]; + pthread_t thread; - if (argc == 2) { - /* Open the test case before we fork so we know the file exists. */ - fd = open (argv[1], O_RDONLY); - if (fd == -1) { - perror (argv[1]); - exit (EXIT_FAILURE); - } - } - else { + if (argc != 2) { fprintf (stderr, "libnbd-fuzz-wrapper testcase\n"); exit (EXIT_FAILURE); } + /* Load the test case fully into memory. */ + fd = open (argv[1], O_RDONLY); + if (fd == -1) { + perror (argv[1]); + exit (EXIT_FAILURE); + } + if (fstat (fd, &statbuf) == -1) { + perror ("fstat"); + abort (); + } + if (byte_vector_reserve_exactly (&fuzzed_data, statbuf.st_size) == -1) { + perror ("malloc"); + abort (); + } + if (read (fd, fuzzed_data.ptr, statbuf.st_size) != statbuf.st_size) { + fprintf (stderr, "%s: incomplete read\n", argv[0]); + abort (); + } + close (fd); + /* Create a connected socket. */ if (socketpair (AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC, 0, sv) == -1) { perror ("socketpair"); - exit (EXIT_FAILURE); + abort (); } - /* Fork: The parent will be the libnbd process (client). The child - * will be the (usually phony) NBD server listening on the socket. - */ - pid = fork (); - if (pid == -1) { - perror ("fork"); - exit (EXIT_FAILURE); + err = pthread_create (&thread, NULL, writer_thread, &sv[1]); + if (err != 0) { + errno = err; + perror ("pthread_create"); + abort (); } - if (pid > 0) { - /* Parent: libnbd client. */ - close (sv[1]); - close (fd); + /* Start running the test. */ + do_test (sv[0]); - client (sv[0]); - - close (sv[0]); - - r = wait (&status); - if (r == -1) { - perror ("wait"); - exit (EXIT_FAILURE); - } - if (!WIFEXITED (status) || WEXITSTATUS (status) != 0) - exit (EXIT_FAILURE); - else - exit (EXIT_SUCCESS); - } - - /* Child: NBD server. */ - close (sv[0]); - - server (fd, sv[1]); - - close (sv[1]); - - _exit (EXIT_SUCCESS); + exit (EXIT_SUCCESS); } /* Structured reads callback, does nothing. */ @@ -146,73 +172,133 @@ extent64_callback (void *user_data, return 0; } -/* This is the client (parent process) running libnbd. */ -static char buf[512]; -static char prbuf[65536]; +enum api_type { + PREAD, + PWRITE, + FLUSH, + TRIM, + ZERO, + CACHE, + PREAD_STRUCTURED, + BLOCK_STATUS, + BLOCK_STATUS_64, + API_TYPE_MAX = BLOCK_STATUS_64 +}; static void -client (int sock) +do_test (int sock) { struct nbd_handle *nbd; + byte_vector data, from_server; int64_t length; + int api; + uint64_t offset; + uint32_t flags; nbd = nbd_create (); if (nbd == NULL) { fprintf (stderr, "%s\n", nbd_get_error ()); - exit (EXIT_FAILURE); + abort (); } /* Note we ignore errors in these calls because we are only - * interested in whether the process crashes. Likewise, we don't - * want to accidentally avoid sending traffic to the server merely - * because client side strictness sees a problem. + * interested in whether the process crashes. + */ + + /* We don't want to accidentally avoid sending traffic to the server + * merely because client side strictness sees a problem. */ nbd_set_strict_mode (nbd, 0); - /* Enable a metadata context, for block status below. */ + /* Enable a metadata context, for block status. */ nbd_add_meta_context (nbd, LIBNBD_CONTEXT_BASE_ALLOCATION); /* This tests the handshake phase. */ nbd_set_opt_mode (nbd, true); + + /* Consume the initial server buffer and get ready to write it to + * the server. + */ + from_server = fuzzed_data_consume_buffer (); + send_to_writer_thread (from_server); + + /* Connect to the socket. */ nbd_connect_socket (nbd, sock); + nbd_opt_info (nbd); nbd_opt_go (nbd); length = nbd_get_size (nbd); - /* Test common asynchronous I/O calls. */ - nbd_aio_pread (nbd, buf, sizeof buf, 0, NBD_NULL_COMPLETION, 0); - nbd_aio_pwrite (nbd, buf, sizeof buf, 0, NBD_NULL_COMPLETION, 0); - nbd_aio_flush (nbd, NBD_NULL_COMPLETION, 0); - nbd_aio_trim (nbd, 8192, 8192, NBD_NULL_COMPLETION, 0); - nbd_aio_zero (nbd, 8192, 65536, NBD_NULL_COMPLETION, 0); - nbd_aio_cache (nbd, 8192, 0, NBD_NULL_COMPLETION, 0); + /* Main loop: Consume fuzzer data to decide which calls we will make. */ + while (more_fuzzed_data ()) { + api = fuzzed_data_consume_enum (API_TYPE_MAX); + offset = fuzzed_data_consume_uint64_t (0, length); + flags = fuzzed_data_consume_unsigned (0, 65535); + data = fuzzed_data_consume_buffer (); + from_server = fuzzed_data_consume_buffer (); + send_to_writer_thread (from_server); - /* Test structured reads. */ - nbd_aio_pread_structured (nbd, prbuf, sizeof prbuf, 8192, - (nbd_chunk_callback) { - .callback = chunk_callback, + switch (api) { + case PREAD: + nbd_aio_pread (nbd, data.ptr, data.len, offset, + NBD_NULL_COMPLETION, flags); + break; + case PWRITE: + nbd_aio_pwrite (nbd, data.ptr, data.len, offset, + NBD_NULL_COMPLETION, flags); + break; + case FLUSH: + nbd_aio_flush (nbd, NBD_NULL_COMPLETION, flags); + break; + case TRIM: + nbd_aio_trim (nbd, data.len, offset, NBD_NULL_COMPLETION, flags); + break; + case ZERO: + nbd_aio_zero (nbd, data.len, offset, NBD_NULL_COMPLETION, flags); + break; + case CACHE: + nbd_aio_cache (nbd, data.len, offset, NBD_NULL_COMPLETION, flags); + break; + case PREAD_STRUCTURED: + nbd_aio_pread_structured (nbd, data.ptr, data.len, offset, + (nbd_chunk_callback) { + .callback = chunk_callback, + }, + NBD_NULL_COMPLETION, + flags); + break; + case BLOCK_STATUS: + nbd_aio_block_status (nbd, data.len, offset, + (nbd_extent_callback) { + .callback = extent_callback, }, NBD_NULL_COMPLETION, - 0); + flags); + break; + case BLOCK_STATUS_64: + nbd_aio_block_status_64 (nbd, data.len, offset, + (nbd_extent64_callback) { + .callback = extent64_callback, + }, + NBD_NULL_COMPLETION, + flags); + break; + default: + abort (); + } - /* Test both sizes of block status. */ - nbd_aio_block_status (nbd, length, 0, - (nbd_extent_callback) { - .callback = extent_callback, - }, - NBD_NULL_COMPLETION, - 0); - nbd_aio_block_status_64 (nbd, length, 0, - (nbd_extent64_callback) { - .callback = extent64_callback, - }, - NBD_NULL_COMPLETION, - 0); + /* Run the state machine until it blocks. */ + while (nbd_poll (nbd, 0) == 1) + ; + } - /* Run the commands until there are no more in flight or there is an - * error caused by the server side disconnecting. + /* No more data from the "server", tell the writer thread to shut + * down once it has finished writing all its data. */ + end_writer_thread (); + + /* Keep running until all commands have finished. */ while (nbd_aio_in_flight (nbd) > 0) { if (nbd_poll (nbd, -1) == -1) break; @@ -222,71 +308,75 @@ client (int sock) nbd_shutdown (nbd, 0); } -/* This is the server (child process) acting like an NBD server. */ -static void -server (int fd, int sock) +/* The writer thread. */ +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; +static byte_vector to_write = empty_vector; +static bool writer_done = false; + +static void * +writer_thread (void *vp) { - struct pollfd pfds[1]; - char rbuf[512], wbuf[512]; - size_t wsize = 0; - ssize_t r; + int sock = *(int *)vp; + uint8_t buf[128]; + size_t n; for (;;) { - pfds[0].fd = sock; - pfds[0].events = POLLIN; - if (wsize > 0 || fd >= 0) pfds[0].events |= POLLOUT; - pfds[0].revents = 0; - - if (poll (pfds, 1, -1) == -1) { - if (errno == EINTR) - continue; - perror ("poll"); - /* This is not an error. */ - return; + /* Get next data to write. Take a copy because as soon as we + * release the lock the to_write.ptr buffer might be reallocated. + */ + pthread_mutex_lock (&lock); + again: + if (writer_done) { + /* Indicate no more data below. */ + n = 0; } - - /* We can read from the client socket. Just throw away anything sent. */ - if ((pfds[0].revents & POLLIN) != 0) { - r = read (sock, rbuf, sizeof rbuf); - if (r == -1 && errno != EINTR) { - perror ("read"); - return; - } - else if (r == 0) /* end of input from the server */ - return; + else if (to_write.len > 0) { + /* Copy some data out of to_write, ready to write it below. */ + n = MIN (sizeof buf, to_write.len); + memcpy (buf, to_write.ptr, n); + memmove (to_write.ptr, &to_write.ptr[n], to_write.len - n); + to_write.len -= n; } + else { + /* Not done and waiting on more data to write. */ + pthread_cond_wait (&cond, &lock); + goto again; + } + pthread_mutex_unlock (&lock); - /* We can write to the client socket. */ - if ((pfds[0].revents & POLLOUT) != 0) { - /* Write more data from the wbuf. */ - if (wsize > 0) { - morewrite: - r = write (sock, wbuf, wsize); - if (r == -1 && errno != EAGAIN && errno != EWOULDBLOCK) { - perror ("write"); - return; - } - else if (r > 0) { - memmove (wbuf, &wbuf[r], wsize-r); - wsize -= r; - } - } - /* Write more data from the file. */ - else if (fd >= 0) { - r = read (fd, wbuf, sizeof wbuf); - if (r == -1) { - perror ("read"); - _exit (EXIT_FAILURE); - } - else if (r == 0) { - fd = -1; /* ignore the file from now on */ - shutdown (sock, SHUT_WR); - } - else { - wsize = r; - goto morewrite; - } - } + if (n == 0) { /* no more data */ + close (sock); + pthread_exit (NULL); } - } /* for (;;) */ + + /* Write the data. Assumes we can always write 128 bytes + * atomically to a Unix domain socket, which is likely always true + * in Linux. + */ + if (write (sock, buf, n) != n) + perror ("write"); + } +} + +static void +send_to_writer_thread (byte_vector v) +{ + pthread_mutex_lock (&lock); + if (byte_vector_reserve (&to_write, v.len) == -1) + abort (); + memcpy (&to_write.ptr[to_write.len], v.ptr, v.len); + to_write.len += v.len; + byte_vector_reset (&v); + pthread_cond_signal (&cond); + pthread_mutex_unlock (&lock); +} + +static void +end_writer_thread (void) +{ + pthread_mutex_lock (&lock); + writer_done = true; + pthread_cond_signal (&cond); + pthread_mutex_unlock (&lock); } diff --git a/fuzzing/libnbd-libfuzzer-test.c b/fuzzing/libnbd-libfuzzer-test.c index 1721b74..cf6c1cf 100644 --- a/fuzzing/libnbd-libfuzzer-test.c +++ b/fuzzing/libnbd-libfuzzer-test.c @@ -22,6 +22,10 @@ * * - This case is mostly unmaintained. The maintainers use AFL++ for * fuzzing (see libnbd-fuzz-wrapper.c). + * + * - This test needs to be updated to use the new method of reading + * input via the fuzzed data provider (see again + * libnbd-fuzz-wrapper.c). */ #include <config.h> -- 2.43.1 _______________________________________________ Libguestfs mailing list -- guestfs@lists.libguestfs.org To unsubscribe send an email to guestfs-le...@lists.libguestfs.org