diff --git a/tests/fuzz/fuzz_config_receive.c b/tests/fuzz/fuzz_config_receive.c new file mode 100644 index 0000000..082359a --- /dev/null +++ b/tests/fuzz/fuzz_config_receive.c @@ -0,0 +1,241 @@ +/* + * Fuzz the binary config-frame receive path: Config* config_receive(int fd). + * + * The frame is a length-prefixed stream of strings/ints/bools, so the receiver + * stops at the first malformed field. Feeding raw fuzz bytes alone therefore + * almost never reaches the deep P8 trailing blocks (--super / --copy-as) or the + * identity-map block, because every preceding wire bool must be exactly 0 or 1. + * + * To exercise those paths we first build one canonical, fully-valid frame with + * the production sender and then feed the receiver four shapes: + * + * 1. raw : the raw fuzz bytes as the whole frame (version gate included). + * 2. general : the valid version-string prefix + the raw fuzz bytes, so the + * fuzzer can walk the early/core/selection blocks from arbitrary + * input while staying past the version gate. + * 3. tail : the valid frame up to its last P8_TAIL_BYTES (super_mode + + * copy-as presence/uid/gid) + the raw fuzz bytes, so the fuzzer + * directly mutates super_mode and the copy-as ids and truncates + * the tail at any byte. + * 4. map : the valid frame up to the --usermap count + the raw fuzz bytes, + * so the fuzzer directly drives the map count (huge/extreme) and + * the map entries. + * + * The canonical frame is captured by running config_send once, writing the + * frame into a pipe whose read end is drained afterwards; the STATUS_OK ack is + * pre-loaded into a second pipe so a single thread suffices. + */ +#include "config.h" +#include "protocol.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* super_mode (4) + copy-as presence (4) + uid (4) + gid (4) = the P8 tail. */ +#define P8_TAIL_BYTES 16 + +/* Distinctive --usermap entry used to locate the map-count field in the + * canonical frame without duplicating the wire layout here. */ +#define MAP_FROM 0x11223344 +#define MAP_TO 0x55667788 + +static unsigned char* g_frame; +static size_t g_frame_len; +static size_t g_version_len; /* length of the leading version-string frame */ +static size_t g_usermap_count_off; /* offset of the usermap count int, 0 = unknown */ +static bool g_frame_ready; + +/* Read the canonical frame from the send peer. The producer shuts down its + * write half first, so a blocking read drains the frame and then sees EOF. */ +static unsigned char* drain_frame(int fd, size_t* out_len) { + size_t cap = 4096; + size_t len = 0; + unsigned char* buf = malloc(cap); + if (!buf) + return NULL; + for (;;) { + if (len == cap) { + size_t grown = cap * 2; + unsigned char* bigger = realloc(buf, grown); + if (!bigger) { + free(buf); + return NULL; + } + buf = bigger; + cap = grown; + } + ssize_t n = read(fd, buf + len, cap - len); + if (n > 0) { + len += (size_t)n; + continue; + } + if (n < 0 && errno == EINTR) + continue; + break; /* 0 (EOF) or error */ + } + *out_len = len; + return buf; +} + +/* Serialize a valid Config with the real sender. The frame is written into a + * pipe (64 KiB kernel buffer, far larger than one config frame) whose read end + * is drained afterwards; the STATUS_OK ack is pre-loaded into a second pipe so + * a single thread suffices (config_send writes the whole frame before it reads + * the ack). */ +static void build_canonical_frame(void) { + g_frame_ready = true; + + Config* cfg = config_create(); + if (!cfg) + return; + cfg->send_directory = str_dup("/src"); + cfg->receive_root_directory = str_dup("/dst"); + /* Force the three P8 tail fields to be present (copy-as requires metadata). */ + cfg->copy_as_set = true; + cfg->copy_as_uid = 0; + cfg->copy_as_gid = 0; + cfg->use_metadata = true; + /* Force one usermap entry with a locatable sentinel. */ + cfg->usermap = malloc(sizeof(IdentityMap)); + if (cfg->usermap) { + cfg->usermap_count = 1; + cfg->usermap[0].from = MAP_FROM; + cfg->usermap[0].to = MAP_TO; + } + if (!cfg->send_directory || !cfg->receive_root_directory || !cfg->usermap) { + config_delete(cfg); + return; + } + + int frame_pipe[2] = {-1, -1}; + int status_pipe[2] = {-1, -1}; + if (pipe(frame_pipe) != 0 || pipe(status_pipe) != 0) + goto out; + + int ack = STATUS_OK; + if (write(status_pipe[1], &ack, sizeof(ack)) != (ssize_t)sizeof(ack)) + goto out; + + io_set_fds(status_pipe[0], frame_pipe[1]); + io_set_bwlimit(0); + bool sent = config_send(frame_pipe[1], cfg); + close(frame_pipe[1]); + frame_pipe[1] = -1; + close(status_pipe[0]); + status_pipe[0] = -1; + close(status_pipe[1]); + status_pipe[1] = -1; + + if (sent) + g_frame = drain_frame(frame_pipe[0], &g_frame_len); + +out: + if (frame_pipe[0] != -1) + close(frame_pipe[0]); + if (frame_pipe[1] != -1) + close(frame_pipe[1]); + if (status_pipe[0] != -1) + close(status_pipe[0]); + if (status_pipe[1] != -1) + close(status_pipe[1]); + config_delete(cfg); + if (!g_frame || g_frame_len == 0) { + free(g_frame); + g_frame = NULL; + g_frame_len = 0; + return; + } + + g_version_len = sizeof(size_t) + strlen(PROTOCOL_VERSION); + if (g_version_len > g_frame_len) + g_version_len = g_frame_len; + + /* Locate the usermap entry sentinel; its count int sits 4 bytes before it. */ + int32_t from = MAP_FROM; + int32_t to = MAP_TO; + unsigned char pattern[8]; + memcpy(pattern, &from, sizeof(from)); + memcpy(pattern + sizeof(from), &to, sizeof(to)); + if (g_frame_len >= sizeof(pattern)) { + for (size_t i = 4; i + sizeof(pattern) <= g_frame_len; i++) { + if (memcmp(g_frame + i, pattern, sizeof(pattern)) == 0) { + g_usermap_count_off = i - sizeof(int32_t); + break; + } + } + } +} + +/* 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 config_receive over it. */ +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); + /* Signal EOF without closing the read half, so the receiver's STATUS_ERROR + * replies do not hit EPIPE. */ + shutdown(sv[0], SHUT_WR); + + io_set_fds(sv[1], sv[1]); + io_set_bwlimit(0); + Config* cfg = config_receive(sv[1]); + config_delete(cfg); + + close(sv[0]); + close(sv[1]); +} + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + if (!g_frame_ready) + build_canonical_frame(); + + /* Raw bytes as the whole frame (version gate and all). */ + receive_stream(NULL, 0, data, size); + + if (g_frame) { + /* Keep the valid version prefix, fuzz everything after it. */ + receive_stream(g_frame, g_version_len, data, size); + + /* Keep the valid frame up to the P8 tail, fuzz super_mode + copy-as. */ + if (g_frame_len > P8_TAIL_BYTES) + receive_stream(g_frame, g_frame_len - P8_TAIL_BYTES, data, size); + + /* Keep the valid frame up to the usermap count, fuzz the count + entries. */ + if (g_usermap_count_off > 0) + receive_stream(g_frame, g_usermap_count_off, data, size); + } + return 0; +} diff --git a/tests/fuzz/fuzz_identity_parse.c b/tests/fuzz/fuzz_identity_parse.c new file mode 100644 index 0000000..0b9f961 --- /dev/null +++ b/tests/fuzz/fuzz_identity_parse.c @@ -0,0 +1,58 @@ +/* + * Fuzz the CLI-time identity parsers (identity.h): + * - identity_parse_copy_as + * - identity_parse_map (user and group variants) + * - identity_parse_chown + * + * Each parser mutates a Config, so every input gets a fresh config_create() + * (freed afterwards). After a successful parse the shared wire validator and + * the ownership predicate are also exercised on the mutated config. The input + * is NUL-terminated; embedded NULs simply shorten the effective spec, which is + * fine for a parser fuzzer. + */ +#include "config.h" +#include "identity.h" +#include +#include +#include + +static void exercise(Config* c, const char* spec, int which) { + if (!c) + return; + switch (which) { + case 0: + (void)identity_parse_copy_as(c, spec); + break; + case 1: + (void)identity_parse_map(c, spec, false); + break; + case 2: + (void)identity_parse_map(c, spec, true); + break; + default: + (void)identity_parse_chown(c, spec); + break; + } + (void)identity_wire_valid(c); + (void)identity_ownership_requested(c); + config_delete(c); +} + +int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + if (size == 0) + return 0; + + char* spec = malloc(size + 1); + if (!spec) + return 0; + memcpy(spec, data, size); + spec[size] = '\0'; + + exercise(config_create(), spec, 0); + exercise(config_create(), spec, 1); + exercise(config_create(), spec, 2); + exercise(config_create(), spec, 3); + + free(spec); + return 0; +} diff --git a/tests/test_fuzz_smoke.c b/tests/test_fuzz_smoke.c index 9b2c415..50d6c29 100644 --- a/tests/test_fuzz_smoke.c +++ b/tests/test_fuzz_smoke.c @@ -1,16 +1,24 @@ #include "test_fuzz_smoke.h" #include "chunk.h" #include "compression.h" +#include "config.h" #include "data.h" #include "delta.h" #include "metadata.h" +#include "protocol.h" #include "test_utils.h" #include "utils.h" +#include +#include #include #include #include +#include #include +/* P8 config-frame tail: super_mode (4) + copy-as presence (4) + uid (4) + gid (4). */ +#define P8_TAIL_BYTES 16 + /* Smoke test for chunk_deserialize fuzz target */ static void test_fuzz_chunk_deserialize() { /* Create a minimal valid chunk to serialize and deserialize */ @@ -170,6 +178,272 @@ static void test_fuzz_glob_match() { EXPECT_FALSE(glob_match("*.md", "readme.txt")); } +/* ---- Deterministic config-frame receive hardening (P8) ---- + * + * The P8 tail (--super / --copy-as) and the identity-map count only parse after + * the entire preceding frame validates, which random bytes almost never reach. + * These tests capture one valid frame with the production sender and then + * mutate/truncate the exact tail bytes. */ + +/* Serialize cfg with the production sender into a heap buffer. The frame is + * written into a pipe (64 KiB kernel buffer, far larger than one config frame) + * whose read end is drained afterwards; the required STATUS_OK ack is + * pre-loaded into a second pipe, so a single thread suffices. */ +static bool capture_config_frame(const Config* cfg, unsigned char** out, size_t* out_len) { + *out = NULL; + *out_len = 0; + + int frame_pipe[2]; + int status_pipe[2]; + if (pipe(frame_pipe) != 0) + return false; + if (pipe(status_pipe) != 0) { + close(frame_pipe[0]); + close(frame_pipe[1]); + return false; + } + + int ack = STATUS_OK; + bool ok = write(status_pipe[1], &ack, sizeof(ack)) == (ssize_t)sizeof(ack); + if (ok) { + io_set_fds(status_pipe[0], frame_pipe[1]); + io_set_bwlimit(0); + ok = config_send(frame_pipe[1], cfg); + } + close(frame_pipe[1]); + close(status_pipe[0]); + close(status_pipe[1]); + + unsigned char* buf = NULL; + if (ok) { + size_t cap = 4096; + size_t len = 0; + buf = malloc(cap); + if (!buf) { + ok = false; + } + while (ok) { + if (len == cap) { + size_t grown = cap * 2; + unsigned char* bigger = realloc(buf, grown); + if (!bigger) { + ok = false; + break; + } + buf = bigger; + cap = grown; + } + ssize_t n = read(frame_pipe[0], buf + len, cap - len); + if (n > 0) { + len += (size_t)n; + continue; + } + if (n < 0 && errno == EINTR) + continue; + break; + } + if (ok && len > 0) { + *out = buf; + *out_len = len; + buf = NULL; + } + } + close(frame_pipe[0]); + free(buf); + return *out != NULL; +} + +/* Feed a raw config frame to config_receive over a socketpair. The write half + * is shut down (not closed) after the data so the receiver sees EOF but its + * STATUS_ERROR replies do not hit a closed peer. */ +static bool receive_config_frame(const unsigned char* buf, size_t len) { + int sv[2]; + if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) != 0) + return false; + + size_t off = 0; + while (off < len) { + ssize_t n = write(sv[0], buf + off, len - off); + if (n > 0) { + off += (size_t)n; + continue; + } + if (n < 0 && errno == EINTR) + continue; + break; + } + shutdown(sv[0], SHUT_WR); + io_set_fds(sv[1], sv[1]); + io_set_bwlimit(0); + Config* cfg = config_receive(sv[1]); + bool accepted = cfg != NULL; + config_delete(cfg); + close(sv[0]); + close(sv[1]); + return accepted; +} + +static void put_i32(unsigned char* buf, size_t off, int32_t value) { + memcpy(buf + off, &value, sizeof(value)); +} + +static size_t find_bytes(const unsigned char* haystack, size_t haystack_len, + const unsigned char* needle, size_t needle_len) { + if (needle_len == 0 || haystack_len < needle_len) + return SIZE_MAX; + for (size_t i = 0; i + needle_len <= haystack_len; i++) { + if (memcmp(haystack + i, needle, needle_len) == 0) + return i; + } + return SIZE_MAX; +} + +static Config* make_copy_as_config(void) { + Config* c = config_create(); + if (!c) + return NULL; + c->send_directory = str_dup("/src"); + c->receive_root_directory = str_dup("/dst"); + c->copy_as_set = true; + c->copy_as_uid = 0; + c->copy_as_gid = 0; + c->use_metadata = true; /* --copy-as requires the metadata path */ + return c; +} + +/* The P8 tail must reject an out-of-range super_mode, a negative copy-as id and + * any truncation inside the tail, while the untouched frame is accepted. */ +static void test_fuzz_config_receive_p8_tail() { + Config* c = make_copy_as_config(); + EXPECT_NOT_NULL(c); + + unsigned char* frame = NULL; + size_t len = 0; + bool captured = capture_config_frame(c, &frame, &len); + config_delete(c); + if (!captured || len <= P8_TAIL_BYTES) { + free(frame); + EXPECT_TRUE(false); + return; + } + + /* Baseline: the untouched frame is accepted. */ + EXPECT_TRUE(receive_config_frame(frame, len)); + + unsigned char* mut = malloc(len); + EXPECT_NOT_NULL(mut); + + /* super_mode outside the 0..2 tri-state is refused. */ + memcpy(mut, frame, len); + put_i32(mut, len - P8_TAIL_BYTES, 99); + EXPECT_FALSE(receive_config_frame(mut, len)); + put_i32(mut, len - P8_TAIL_BYTES, -1); + EXPECT_FALSE(receive_config_frame(mut, len)); + + /* A negative (sentinel) and an extreme copy-as uid/gid are refused. */ + memcpy(mut, frame, len); + put_i32(mut, len - P8_TAIL_BYTES, SUPER_MODE_AUTO); + put_i32(mut, len - P8_TAIL_BYTES + 4, 1); + put_i32(mut, len - P8_TAIL_BYTES + 8, -1); + put_i32(mut, len - P8_TAIL_BYTES + 12, 0); + EXPECT_FALSE(receive_config_frame(mut, len)); + put_i32(mut, len - P8_TAIL_BYTES + 8, 0); + put_i32(mut, len - P8_TAIL_BYTES + 12, INT32_MIN); + EXPECT_FALSE(receive_config_frame(mut, len)); + + /* A presence int that is not a wire bool is refused. */ + memcpy(mut, frame, len); + put_i32(mut, len - P8_TAIL_BYTES, SUPER_MODE_AUTO); + put_i32(mut, len - P8_TAIL_BYTES + 4, 2); + EXPECT_FALSE(receive_config_frame(mut, len)); + + /* Truncating anywhere inside the P8 tail is refused. */ + EXPECT_FALSE(receive_config_frame(frame, len - 2)); + EXPECT_FALSE(receive_config_frame(frame, len - P8_TAIL_BYTES)); + + free(mut); + free(frame); +} + +/* A huge or negative --usermap count must be refused up front, never driving a + * giant allocation. The count is located by searching for a sentinel entry. */ +static void test_fuzz_config_receive_huge_map_count() { + Config* c = make_copy_as_config(); + EXPECT_NOT_NULL(c); + int32_t sentinel_from = 0x11223344; + int32_t sentinel_to = 0x55667788; + c->usermap = malloc(sizeof(IdentityMap)); + if (!c->usermap) { + config_delete(c); + EXPECT_TRUE(false); + return; + } + c->usermap_count = 1; + c->usermap[0].from = sentinel_from; + c->usermap[0].to = sentinel_to; + + unsigned char* frame = NULL; + size_t len = 0; + bool captured = capture_config_frame(c, &frame, &len); + config_delete(c); + if (!captured) { + EXPECT_TRUE(false); + return; + } + + unsigned char pattern[8]; + memcpy(pattern, &sentinel_from, sizeof(sentinel_from)); + memcpy(pattern + sizeof(sentinel_from), &sentinel_to, sizeof(sentinel_to)); + size_t entry_off = find_bytes(frame, len, pattern, sizeof(pattern)); + if (entry_off == SIZE_MAX || entry_off < sizeof(int32_t)) { + free(frame); + EXPECT_TRUE(false); + return; + } + size_t count_off = entry_off - sizeof(int32_t); + + /* Baseline accepted. */ + EXPECT_TRUE(receive_config_frame(frame, len)); + + unsigned char* mut = malloc(len); + EXPECT_NOT_NULL(mut); + memcpy(mut, frame, len); + put_i32(mut, count_off, INT_MAX); + EXPECT_FALSE(receive_config_frame(mut, len)); + put_i32(mut, count_off, -1); + EXPECT_FALSE(receive_config_frame(mut, len)); + put_i32(mut, count_off, MAX_IDENTITY_MAP + 1); + EXPECT_FALSE(receive_config_frame(mut, len)); + + free(mut); + free(frame); +} + +/* A mismatched version and a matching version followed by a wrong-order field + * (an int that is not a wire bool) are both refused at/just after the gate. */ +static void test_fuzz_config_receive_version_gate() { + unsigned char buf[64]; + + size_t off = 0; + const char* bad_version = "1.2.3"; + size_t bad_len = strlen(bad_version); + memcpy(buf + off, &bad_len, sizeof(bad_len)); + off += sizeof(bad_len); + memcpy(buf + off, bad_version, bad_len); + off += bad_len; + EXPECT_FALSE(receive_config_frame(buf, off)); + + off = 0; + size_t good_len = strlen(PROTOCOL_VERSION); + memcpy(buf + off, &good_len, sizeof(good_len)); + off += sizeof(good_len); + memcpy(buf + off, PROTOCOL_VERSION, good_len); + off += good_len; + put_i32(buf, off, -1); + off += sizeof(int32_t); + EXPECT_FALSE(receive_config_frame(buf, off)); +} + void test_fuzz_smoke() { test_fuzz_chunk_deserialize(); test_fuzz_compress_decompress(); @@ -177,4 +451,7 @@ void test_fuzz_smoke() { test_fuzz_metadata_from_buf(); test_fuzz_delta_signature_deserialize(); test_fuzz_glob_match(); + test_fuzz_config_receive_p8_tail(); + test_fuzz_config_receive_huge_map_count(); + test_fuzz_config_receive_version_gate(); }