test: fuzz manifest/protocol/xattr, hardlink unit, fault injection
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Fuzz the delete-manifest parser: receive_manifest_entries(int fd).
|
||||
*
|
||||
* The parser reads three length-delimited sections (keeps, protected prefixes,
|
||||
* missing-args paths) from the connection. Feeding raw bytes alone exercises
|
||||
* the "reject the first malformed count/string" fast paths, but because each
|
||||
* section is self-delimiting a single bad value hides every later section.
|
||||
*
|
||||
* To reach the protected-prefix and missing-args parsers (the paths that drive
|
||||
* actual destination deletion) we build one canonical, fully-valid manifest
|
||||
* with hand-written wire framing and then feed the receiver several shapes:
|
||||
*
|
||||
* 1. raw : the raw fuzz bytes as the whole manifest.
|
||||
* 2. keeps : the valid keep count only + the fuzz bytes, so the fuzzer
|
||||
* drives the keep count and entries directly.
|
||||
* 3. prot : the valid keeps section + the fuzz bytes, so the fuzzer drives
|
||||
* the protected count and prefixes.
|
||||
* 4. missing: the valid keeps+protected sections + the fuzz bytes, so the
|
||||
* fuzzer drives the trailing missing-args section, including the
|
||||
* aggregate MAX_MANIFEST_BYTES budget.
|
||||
*
|
||||
* The wire encoding matches receive_int (native int) and receive_wire_str
|
||||
* (native size_t length prefix + body); no charset conversion is configured in
|
||||
* the fuzz process, so receive_wire_str is receive_str.
|
||||
*/
|
||||
#include "file_receive.h"
|
||||
#include "protocol.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static unsigned char g_manifest[512];
|
||||
static size_t g_len_after_count; /* offset of the first keep entry */
|
||||
static size_t g_len_after_keeps; /* offset of the protected count */
|
||||
static size_t g_len_after_protected; /* offset of the missing count */
|
||||
static int g_manifest_ready;
|
||||
|
||||
static void append_int32(unsigned char* buf, size_t* off, int32_t value) {
|
||||
memcpy(buf + *off, &value, sizeof(value));
|
||||
*off += sizeof(value);
|
||||
}
|
||||
|
||||
static void append_wire_str(unsigned char* buf, size_t* off, const char* s) {
|
||||
size_t n = strlen(s);
|
||||
memcpy(buf + *off, &n, sizeof(n));
|
||||
*off += sizeof(n);
|
||||
memcpy(buf + *off, s, n);
|
||||
*off += n;
|
||||
}
|
||||
|
||||
static void build_canonical_manifest(void) {
|
||||
g_manifest_ready = 1;
|
||||
size_t off = 0;
|
||||
append_int32(g_manifest, &off, 2);
|
||||
g_len_after_count = off;
|
||||
append_wire_str(g_manifest, &off, "keep/a");
|
||||
append_wire_str(g_manifest, &off, "keep/b");
|
||||
g_len_after_keeps = off;
|
||||
append_int32(g_manifest, &off, 1);
|
||||
append_wire_str(g_manifest, &off, "excluded/prefix");
|
||||
g_len_after_protected = off;
|
||||
append_int32(g_manifest, &off, 1);
|
||||
append_wire_str(g_manifest, &off, "missing/path");
|
||||
}
|
||||
|
||||
/* Best-effort non-blocking write: an oversized fuzz input is truncated rather
|
||||
* than stalling the harness. */
|
||||
static void write_best_effort(int fd, const void* data, size_t size) {
|
||||
const unsigned char* p = data;
|
||||
size_t off = 0;
|
||||
while (off < size) {
|
||||
ssize_t n = write(fd, p + off, size - off);
|
||||
if (n > 0) {
|
||||
off += (size_t)n;
|
||||
continue;
|
||||
}
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Build prefix ++ data as a stream and drive receive_manifest_entries over it.
|
||||
* The write half is shut down first so the parser always sees EOF instead of
|
||||
* blocking on a missing frame tail. */
|
||||
static void receive_stream(const unsigned char* prefix, size_t prefix_len, const uint8_t* data,
|
||||
size_t size) {
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0)
|
||||
return;
|
||||
|
||||
int flags = fcntl(sv[0], F_GETFL, 0);
|
||||
if (flags != -1)
|
||||
(void)fcntl(sv[0], F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
if (prefix_len > 0)
|
||||
write_best_effort(sv[0], prefix, prefix_len);
|
||||
if (size > 0)
|
||||
write_best_effort(sv[0], data, size);
|
||||
shutdown(sv[0], SHUT_WR);
|
||||
|
||||
DeleteManifest* manifest = receive_manifest_entries(sv[1]);
|
||||
delete_manifest_free(manifest);
|
||||
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (!g_manifest_ready)
|
||||
build_canonical_manifest();
|
||||
|
||||
/* Raw bytes as the whole manifest. */
|
||||
receive_stream(NULL, 0, data, size);
|
||||
|
||||
/* Keep the valid framing so the fuzzer reaches each later section. */
|
||||
receive_stream(g_manifest, g_len_after_protected, data, size);
|
||||
receive_stream(g_manifest, g_len_after_keeps, data, size);
|
||||
receive_stream(g_manifest, g_len_after_count, data, size);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Fuzz the base protocol framing: receive_str / receive_data / receive_status
|
||||
* (plus the redacted string, the size-limited data and the timed-status
|
||||
* variants) fed arbitrary bytes over an in-memory socketpair.
|
||||
*
|
||||
* Every receive primitive reads a fixed-width header (a size_t string length,
|
||||
* an unsigned long long data length, an int status/int value) and then a body.
|
||||
* The fuzzer attacks:
|
||||
* - oversized length headers (the MAX_STRING_SIZE / MAX_DATA_PAYLOAD_SIZE
|
||||
* gates must reject before allocating),
|
||||
* - truncated bodies (a declared body larger than the stream must fail
|
||||
* cleanly at EOF, never read uninitialised memory or leak),
|
||||
* - embedded NUL bytes in strings (must be refused),
|
||||
* - out-of-range status enum values (status_to_string must stay in bounds).
|
||||
*
|
||||
* Each entry point gets its own socketpair because a single receive consumes a
|
||||
* variable number of bytes from the stream; reusing one would make the later
|
||||
* calls meaningless. The write half is shut down first so a truncated frame
|
||||
* always terminates at EOF instead of blocking.
|
||||
*/
|
||||
#include "data.h"
|
||||
#include "protocol.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static void write_best_effort(int fd, const void* data, size_t size) {
|
||||
const unsigned char* p = data;
|
||||
size_t off = 0;
|
||||
while (off < size) {
|
||||
ssize_t n = write(fd, p + off, size - off);
|
||||
if (n > 0) {
|
||||
off += (size_t)n;
|
||||
continue;
|
||||
}
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Create a socketpair pre-loaded with `data`, shut down the write half and
|
||||
* return the read end (which the receiver reads from). `*write_end` is also
|
||||
* returned so the caller can close it. */
|
||||
static int make_stream(const uint8_t* data, size_t size, int* write_end) {
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) {
|
||||
*write_end = -1;
|
||||
return -1;
|
||||
}
|
||||
int flags = fcntl(sv[0], F_GETFL, 0);
|
||||
if (flags != -1)
|
||||
(void)fcntl(sv[0], F_SETFL, flags | O_NONBLOCK);
|
||||
if (size > 0)
|
||||
write_best_effort(sv[0], data, size);
|
||||
shutdown(sv[0], SHUT_WR);
|
||||
*write_end = sv[0];
|
||||
return sv[1];
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
int w;
|
||||
|
||||
int rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
char* s = receive_str(rd);
|
||||
free(s);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
char* s = receive_str_redacted(rd);
|
||||
free(s);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
Data* d = receive_data(rd);
|
||||
data_destroy(d);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
/* The size-limited variant must reject anything beyond its explicit bound
|
||||
* before allocating the body buffer. */
|
||||
rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
Data* d = receive_data_limited(rd, 256);
|
||||
data_destroy(d);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
Status status = STATUS_OK;
|
||||
(void)receive_status(rd, &status);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
Status status = STATUS_OK;
|
||||
(void)receive_status_timed(rd, &status, 1);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
rd = make_stream(data, size, &w);
|
||||
if (rd >= 0) {
|
||||
int value = 0;
|
||||
(void)receive_int(rd, &value);
|
||||
close(rd);
|
||||
close(w);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Fuzz the xattr wire block parser: xattr_receive(int fd, int* ok).
|
||||
*
|
||||
* The block is a count followed by that many (name_len, name, value_len, value)
|
||||
* records. The receiver must reject an invalid count, an out-of-range or
|
||||
* negative name/value length, an embedded NUL or non-whitelisted namespace in
|
||||
* the name, an oversized value, and an aggregate payload beyond
|
||||
* XATTR_TOTAL_MAX -- all without over-allocating or leaking.
|
||||
*
|
||||
* Raw bytes mostly stop at the first invalid count/length, so we also build a
|
||||
* canonical, fully-valid two-entry block by hand and feed the receiver valid
|
||||
* prefixes of it followed by the fuzz bytes. That drives the deep value-
|
||||
* parsing and per-entry namespace/budget checks with attacker-controlled input.
|
||||
*/
|
||||
#include "protocol.h"
|
||||
#include "xattr.h"
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static unsigned char g_block[512];
|
||||
static size_t g_off_after_count; /* start of entry 0 */
|
||||
static size_t g_off_after_entry0; /* start of entry 1 */
|
||||
static size_t g_off_value0; /* start of the first value length */
|
||||
static int g_block_ready;
|
||||
|
||||
static void append_int32(unsigned char* buf, size_t* off, int32_t value) {
|
||||
memcpy(buf + *off, &value, sizeof(value));
|
||||
*off += sizeof(value);
|
||||
}
|
||||
|
||||
static void append_bytes(unsigned char* buf, size_t* off, const void* p, size_t n) {
|
||||
if (n > 0)
|
||||
memcpy(buf + *off, p, n);
|
||||
*off += n;
|
||||
}
|
||||
|
||||
static void build_canonical_block(void) {
|
||||
g_block_ready = 1;
|
||||
size_t off = 0;
|
||||
append_int32(g_block, &off, 2);
|
||||
g_off_after_count = off;
|
||||
|
||||
int32_t name0_len = (int32_t)strlen("user.foo");
|
||||
append_int32(g_block, &off, name0_len);
|
||||
append_bytes(g_block, &off, "user.foo", (size_t)name0_len);
|
||||
g_off_value0 = off;
|
||||
append_int32(g_block, &off, 3);
|
||||
append_bytes(g_block, &off, "bar", 3);
|
||||
|
||||
g_off_after_entry0 = off;
|
||||
int32_t name1_len = (int32_t)strlen("user.empty");
|
||||
append_int32(g_block, &off, name1_len);
|
||||
append_bytes(g_block, &off, "user.empty", (size_t)name1_len);
|
||||
append_int32(g_block, &off, 0);
|
||||
}
|
||||
|
||||
static void write_best_effort(int fd, const void* data, size_t size) {
|
||||
const unsigned char* p = data;
|
||||
size_t off = 0;
|
||||
while (off < size) {
|
||||
ssize_t n = write(fd, p + off, size - off);
|
||||
if (n > 0) {
|
||||
off += (size_t)n;
|
||||
continue;
|
||||
}
|
||||
if (n < 0 && errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void receive_stream(const unsigned char* prefix, size_t prefix_len, const uint8_t* data,
|
||||
size_t size) {
|
||||
int sv[2];
|
||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0)
|
||||
return;
|
||||
|
||||
int flags = fcntl(sv[0], F_GETFL, 0);
|
||||
if (flags != -1)
|
||||
(void)fcntl(sv[0], F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
if (prefix_len > 0)
|
||||
write_best_effort(sv[0], prefix, prefix_len);
|
||||
if (size > 0)
|
||||
write_best_effort(sv[0], data, size);
|
||||
shutdown(sv[0], SHUT_WR);
|
||||
|
||||
int ok = 0;
|
||||
FileXattrList* list = xattr_receive(sv[1], &ok);
|
||||
xattr_list_free(list);
|
||||
|
||||
close(sv[0]);
|
||||
close(sv[1]);
|
||||
}
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (!g_block_ready)
|
||||
build_canonical_block();
|
||||
|
||||
/* Raw bytes as the whole block. */
|
||||
receive_stream(NULL, 0, data, size);
|
||||
|
||||
/* Valid framing so the fuzzer mutates the entry list, the first value and
|
||||
* the second entry respectively instead of stopping at the count. */
|
||||
receive_stream(g_block, g_off_after_entry0, data, size);
|
||||
receive_stream(g_block, g_off_value0, data, size);
|
||||
receive_stream(g_block, g_off_after_count, data, size);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user