Merge branch 'fix/w5-config' into fix/w5-integration
This commit is contained in:
@@ -5,19 +5,19 @@
|
||||
#include <string.h>
|
||||
|
||||
int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
|
||||
if (size < sizeof(int) + FILE_METADATA_WIRE_SIZE)
|
||||
return 0;
|
||||
|
||||
char* buf = malloc(size);
|
||||
/* Exercise the bounds-checked decoder on EVERY input length, including
|
||||
* records shorter than a full metadata body; the decoder must reject those
|
||||
* without reading past `size`. */
|
||||
char* buf = malloc(size > 0 ? size : 1);
|
||||
if (!buf)
|
||||
return 0;
|
||||
memcpy(buf, data, size);
|
||||
if (size > 0)
|
||||
memcpy(buf, data, size);
|
||||
|
||||
char* original_buf = buf;
|
||||
FileMetadata* m = metadata_from_buf(&buf);
|
||||
FileMetadata* m = metadata_from_buf((const uint8_t*)buf, size);
|
||||
if (m)
|
||||
free(m);
|
||||
|
||||
free(original_buf);
|
||||
free(buf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -122,6 +122,52 @@ static void test_validate_config_delta_sendfile_constraints() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* The client must still reject every combination now enforced by the shared
|
||||
config_invariants_error() predicate (the server trusts the same rules). */
|
||||
static void test_validate_config_unified_invariants() {
|
||||
Config* cfg = valid_client_config();
|
||||
cfg->use_incremental = true;
|
||||
cfg->use_delta = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* delta + chunk */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->use_delta = true; /* whole_file false */
|
||||
EXPECT_FALSE(validate_config(cfg)); /* delta without incremental */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->use_sendfile = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* sendfile + chunk */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->preserve_hard_links = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* hard-links + chunk */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->preserve_hard_links = true;
|
||||
cfg->append = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* hard-links + append */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->append = true;
|
||||
cfg->whole_file = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* append + whole-file */
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = valid_client_config();
|
||||
cfg->preserve_xattrs = true;
|
||||
cfg->use_chunk_serialization = true;
|
||||
EXPECT_FALSE(validate_config(cfg)); /* xattrs + chunk */
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test main() with --help flag (early return path, no server connection needed) */
|
||||
static void test_cli_help() {
|
||||
/* We can't easily call main() because it calls send_files which needs a server.
|
||||
@@ -3155,6 +3201,7 @@ void test_client_cli() {
|
||||
test_validate_config_tls_requirements();
|
||||
test_validate_config_credentials_require_tls_or_loopback();
|
||||
test_validate_config_delta_sendfile_constraints();
|
||||
test_validate_config_unified_invariants();
|
||||
test_cli_help();
|
||||
test_cli_archive_flags();
|
||||
test_cli_dry_run();
|
||||
|
||||
@@ -2042,6 +2042,156 @@ static void test_super_does_not_imply_numeric() {
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
/* The single shared predicate must reject every cross-field combination the
|
||||
client/server enforce and accept a plain valid config. Because both
|
||||
validate_config() (client) and validate_received_config() (server) call it,
|
||||
this table documents the whole invariant set in one place. */
|
||||
static void test_config_invariants_error_all_combinations() {
|
||||
Config* c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->send_directory = str_dup("/src");
|
||||
c->receive_root_directory = str_dup("/dst");
|
||||
EXPECT_NULL(config_invariants_error(c));
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
EXPECT_EQ_INT(config_basis_append(c, BASIS_DEST_COMPARE, "sub"), 0);
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* basis + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_sendfile = true;
|
||||
c->use_compression = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* sendfile + compression */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_sendfile = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* sendfile + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_incremental = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* incremental + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->skip_compress_set = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* skip-compress + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_delta = true; /* whole_file false -> active */
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta without incremental */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_delta = true;
|
||||
c->use_incremental = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->use_delta = true;
|
||||
c->use_incremental = true;
|
||||
c->use_sendfile = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delta + sendfile */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->append = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* append + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->append = true;
|
||||
c->whole_file = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* append + whole-file */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->preserve_hard_links = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* hard-links + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->preserve_xattrs = true;
|
||||
c->use_chunk_serialization = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* xattrs + chunk */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->preserve_hard_links = true;
|
||||
c->append = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* hard-links + append */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->delay_updates = true;
|
||||
c->inplace = true;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delay-updates + inplace */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->delay_updates = true;
|
||||
c->backup_dir = str_dup(".fastsync-stage");
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* delay-updates staging conflict */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->delete_delay = true; /* a timing flag without --delete */
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* invalid delete timing */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->iconv_spec = str_dup("no-such-charset,utf-8");
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* malformed iconv spec */
|
||||
config_delete(c);
|
||||
|
||||
c = config_create();
|
||||
c->copy_as_set = true;
|
||||
c->use_metadata = false;
|
||||
EXPECT_NOT_NULL(config_invariants_error(c)); /* copy-as without metadata */
|
||||
config_delete(c);
|
||||
}
|
||||
|
||||
/* The receiver previously missed several of these; a forged frame that sets
|
||||
the offending serialized fields must now be refused at the config
|
||||
handshake. (whole_file is client-only, so its rules cannot appear here.) */
|
||||
static void test_config_receive_rejects_unified_invariants() {
|
||||
if (is_running_under_valgrind())
|
||||
return;
|
||||
struct {
|
||||
bool incremental, delta, chunk, sendfile, compression;
|
||||
} cases[] = {
|
||||
{true, false, true, false, false}, /* --incremental + -s */
|
||||
{false, true, true, false, false}, /* --delta + -s */
|
||||
{false, true, false, false, false}, /* --delta without --incremental */
|
||||
{false, false, false, true, true}, /* --sendfile + compression */
|
||||
{false, false, true, true, false}, /* --sendfile + -s */
|
||||
};
|
||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||
Config* c = config_create();
|
||||
EXPECT_NOT_NULL(c);
|
||||
c->send_directory = str_dup("/src");
|
||||
c->receive_root_directory = str_dup("/dst");
|
||||
c->use_incremental = cases[i].incremental;
|
||||
c->use_delta = cases[i].delta;
|
||||
c->use_chunk_serialization = cases[i].chunk;
|
||||
c->use_sendfile = cases[i].sendfile;
|
||||
c->use_compression = cases[i].compression;
|
||||
EXPECT_FALSE(roundtrip_config_ok(c));
|
||||
config_delete(c);
|
||||
}
|
||||
}
|
||||
|
||||
void test_config() {
|
||||
test_config_lifecycle();
|
||||
test_config_ssh_dest();
|
||||
@@ -2095,6 +2245,8 @@ void test_config() {
|
||||
test_config_receive_rejects_copy_as_without_metadata();
|
||||
test_config_receive_rejects_oversized_string_budget();
|
||||
test_config_receive_with_validate_rejects();
|
||||
test_config_invariants_error_all_combinations();
|
||||
test_config_receive_rejects_unified_invariants();
|
||||
}
|
||||
test_identity_copy_as_refused();
|
||||
test_identity_ownership_requested();
|
||||
|
||||
@@ -128,8 +128,7 @@ static void test_fuzz_metadata_from_buf() {
|
||||
EXPECT_EQ_INT((int)(meta_ptr - meta_buf), (int)meta_buf_size);
|
||||
|
||||
/* Deserialize from buffer (simulates fuzz_metadata_from_buf) */
|
||||
char* buf_copy = meta_buf;
|
||||
FileMetadata* deserialized = metadata_from_buf(&buf_copy);
|
||||
FileMetadata* deserialized = metadata_from_buf((const uint8_t*)meta_buf, (size_t)meta_buf_size);
|
||||
EXPECT_NOT_NULL(deserialized);
|
||||
EXPECT_EQ_INT((int)deserialized->mode, (int)meta->mode);
|
||||
EXPECT_EQ_INT((int)deserialized->mtime_sec, (int)meta->mtime_sec);
|
||||
|
||||
+38
-9
@@ -29,8 +29,8 @@ static void test_metadata_to_from_buf_roundtrip() {
|
||||
char* write_ptr = buf;
|
||||
metadata_to_buf(&write_ptr, &original);
|
||||
|
||||
char* read_ptr = buf;
|
||||
FileMetadata* result = metadata_from_buf(&read_ptr);
|
||||
FileMetadata* result =
|
||||
metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
|
||||
EXPECT_NOT_NULL(result);
|
||||
EXPECT_EQ_INT(result->mode, 0755);
|
||||
@@ -45,8 +45,6 @@ static void test_metadata_to_from_buf_roundtrip() {
|
||||
EXPECT_EQ_INT(result->crtime_sec, 1200000000);
|
||||
EXPECT_EQ_INT(result->crtime_nsec, 750000000);
|
||||
|
||||
EXPECT_EQ_INT((int)(read_ptr - buf), (int)FILE_METADATA_WIRE_SIZE + (int)sizeof(int));
|
||||
|
||||
free(result);
|
||||
free(buf);
|
||||
}
|
||||
@@ -71,14 +69,46 @@ static void test_metadata_from_buf_null() {
|
||||
int present = 0;
|
||||
memcpy(buf, &present, sizeof(int));
|
||||
|
||||
char* read_ptr = buf;
|
||||
const FileMetadata* result = metadata_from_buf(&read_ptr);
|
||||
const FileMetadata* result =
|
||||
metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
|
||||
EXPECT_NULL(result);
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* The decoder must reject (never over-read) a present record that is even one
|
||||
* byte shorter than the full int32 flag + FILE_METADATA_WIRE_SIZE body, and
|
||||
* must reject a buffer too short to even hold the present flag. */
|
||||
static void test_metadata_from_buf_bounds() {
|
||||
char* buf = malloc(FILE_METADATA_WIRE_SIZE + sizeof(int));
|
||||
EXPECT_NOT_NULL(buf);
|
||||
FileMetadata original = {.mode = 0644,
|
||||
.uid = 1,
|
||||
.gid = 2,
|
||||
.mtime_sec = 3,
|
||||
.mtime_nsec = 4,
|
||||
.atime_valid = true,
|
||||
.atime_sec = 5,
|
||||
.atime_nsec = 6,
|
||||
.crtime_valid = false};
|
||||
char* write_ptr = buf;
|
||||
metadata_to_buf(&write_ptr, &original);
|
||||
|
||||
EXPECT_NULL(metadata_from_buf((const uint8_t*)buf, 0));
|
||||
EXPECT_NULL(metadata_from_buf((const uint8_t*)buf, sizeof(int)));
|
||||
EXPECT_NULL(metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int) - 1));
|
||||
/* A buffer larger than the record decodes using only the record prefix. */
|
||||
FileMetadata* decoded =
|
||||
metadata_from_buf((const uint8_t*)buf, FILE_METADATA_WIRE_SIZE + sizeof(int) + 16);
|
||||
EXPECT_NOT_NULL(decoded);
|
||||
EXPECT_EQ_INT(decoded->mode, 0644);
|
||||
free(decoded);
|
||||
EXPECT_NULL(metadata_from_buf(NULL, FILE_METADATA_WIRE_SIZE + sizeof(int)));
|
||||
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void test_metadata_send_receive_roundtrip() {
|
||||
io_set_bwlimit(0);
|
||||
int p[2];
|
||||
@@ -169,10 +199,8 @@ static void test_metadata_wire_is_one_packed_frame() {
|
||||
EXPECT_EQ_INT(avail, 0);
|
||||
|
||||
/* The present frame decodes in one shot with the shared codec. */
|
||||
char* cursor = (char*)wire;
|
||||
FileMetadata* decoded = metadata_from_buf(&cursor);
|
||||
FileMetadata* decoded = metadata_from_buf((const uint8_t*)wire, sizeof(wire));
|
||||
EXPECT_NOT_NULL(decoded);
|
||||
EXPECT_EQ_INT((int)(cursor - (char*)wire), (int)sizeof(wire));
|
||||
EXPECT_EQ_INT(decoded->mode, 0640);
|
||||
EXPECT_EQ_INT(decoded->uid, 42);
|
||||
EXPECT_EQ_INT(decoded->gid, 43);
|
||||
@@ -451,6 +479,7 @@ void test_metadata() {
|
||||
test_metadata_to_from_buf_roundtrip();
|
||||
test_metadata_to_buf_null();
|
||||
test_metadata_from_buf_null();
|
||||
test_metadata_from_buf_bounds();
|
||||
test_metadata_send_receive_roundtrip();
|
||||
test_metadata_send_null();
|
||||
test_metadata_wire_is_one_packed_frame();
|
||||
|
||||
Reference in New Issue
Block a user