---
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