Release v2.26.0 #284
@@ -608,7 +608,15 @@ typedef struct {
|
|||||||
size_t offset; /* offsetof of the boolean target field in Config */
|
size_t offset; /* offsetof of the boolean target field in Config */
|
||||||
} NegatableOption;
|
} NegatableOption;
|
||||||
|
|
||||||
/* Options that map directly onto a Config field with no side effects. */
|
/* Options that map directly onto a Config field with no side effects.
|
||||||
|
*
|
||||||
|
* NOTE: these CLI tables are intentionally NOT generated from the wire-field
|
||||||
|
* X-macro table in config.h. The two sets only overlap partially: the CLI
|
||||||
|
* surface also carries client-only fields that never cross the wire (rsh,
|
||||||
|
* outbuf, remote-option, batch paths, trust-sender, ...) and needs flag/alias/
|
||||||
|
* negation semantics that the wire table does not model. Keeping them
|
||||||
|
* hand-maintained is deliberate; the shared contract is enforced at the wire
|
||||||
|
* boundary by config.[ch] and the golden test. */
|
||||||
static const OptionEntry OPTION_TABLE[] = {
|
static const OptionEntry OPTION_TABLE[] = {
|
||||||
{"--dry-run", "-n", OPT_FLAG, offsetof(Config, dry_run)},
|
{"--dry-run", "-n", OPT_FLAG, offsetof(Config, dry_run)},
|
||||||
{"--remove-source-files", NULL, OPT_FLAG, offsetof(Config, remove_source_files)},
|
{"--remove-source-files", NULL, OPT_FLAG, offsetof(Config, remove_source_files)},
|
||||||
|
|||||||
@@ -100,6 +100,12 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF
|
|||||||
* (STR_MODULE), repeated count+array blocks (BLOCK_*), --copy-as presence
|
* (STR_MODULE), repeated count+array blocks (BLOCK_*), --copy-as presence
|
||||||
* (COPY_AS_*), and the derived --delta / use_xattrs bits (DERIVED_DELTA,
|
* (COPY_AS_*), and the derived --delta / use_xattrs bits (DERIVED_DELTA,
|
||||||
* BOOL_XATTR_DERIVE).
|
* BOOL_XATTR_DERIVE).
|
||||||
|
*
|
||||||
|
* SCOPE: this table covers ONLY the serialized wire frame. The client CLI
|
||||||
|
* option tables in client_cli.c (OPTION_TABLE / NEGATABLE_OPTIONS) are still
|
||||||
|
* hand-maintained and are deliberately NOT generated from this table: the CLI
|
||||||
|
* surface carries client-only fields and flag/alias/negation semantics that
|
||||||
|
* have no wire representation. Do not assume the two are folded together.
|
||||||
* =========================================================================== */
|
* =========================================================================== */
|
||||||
#define CONFIG_WIRE_HEADER_FIELDS(X) X(version, char*, str_dup(PROTOCOL_VERSION), STR)
|
#define CONFIG_WIRE_HEADER_FIELDS(X) X(version, char*, str_dup(PROTOCOL_VERSION), STR)
|
||||||
|
|
||||||
|
|||||||
+274
-51
@@ -2320,75 +2320,74 @@ static void test_config_wire_roundtrip_all_fields() {
|
|||||||
|
|
||||||
Config* populated = config_create();
|
Config* populated = config_create();
|
||||||
EXPECT_NOT_NULL(populated);
|
EXPECT_NOT_NULL(populated);
|
||||||
|
/* The golden fixture is already receiver-valid, so the same fully-populated
|
||||||
|
* config that backs the byte-exact golden also round-trips unchanged. */
|
||||||
golden_config_populate(populated);
|
golden_config_populate(populated);
|
||||||
/* Keep the populated config within the server-side validation bounds. */
|
|
||||||
populated->delta_max_file_size = DELTA_MAX_FILE_SIZE;
|
|
||||||
populated->whole_file = false;
|
|
||||||
/* "X" is not part of FastSync's chmod grammar (see parse_clause), so use a
|
|
||||||
* spec the receiver-side validator accepts. */
|
|
||||||
free(populated->chmod_spec);
|
|
||||||
populated->chmod_spec = str_dup("u=rw,go=r");
|
|
||||||
EXPECT_TRUE(roundtrip_and_compare(populated));
|
EXPECT_TRUE(roundtrip_and_compare(populated));
|
||||||
config_delete(populated);
|
config_delete(populated);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Populate every serialized field with a non-default value so the wire frame
|
/* Populate every serialized field with a non-default value so the wire frame
|
||||||
* exercises each table entry. The values are deterministic. */
|
* exercises each table entry. Boolean runs deliberately alternate true/false:
|
||||||
|
* a run of identical booleans would make an adjacent swap (same KIND) produce
|
||||||
|
* the same byte stream, hiding a table reorder from the golden hash. The whole
|
||||||
|
* frame stays receiver-valid so the receive-side golden can feed it straight
|
||||||
|
* through config_receive() (hence the valid chmod grammar and delta bound). */
|
||||||
static void golden_config_populate(Config* c) {
|
static void golden_config_populate(Config* c) {
|
||||||
c->eight_bit_output = true;
|
c->eight_bit_output = true;
|
||||||
c->max_alloc = 123456789ULL;
|
c->max_alloc = 123456789ULL;
|
||||||
c->send_directory = str_dup("/golden/src");
|
c->send_directory = str_dup("/golden/src");
|
||||||
c->receive_root_directory = str_dup("/golden/dst");
|
c->receive_root_directory = str_dup("/golden/dst");
|
||||||
c->save_to_disk = true;
|
c->save_to_disk = true;
|
||||||
c->use_multithreading = true;
|
c->use_multithreading = false;
|
||||||
c->use_chunk_serialization = false;
|
c->use_chunk_serialization = false;
|
||||||
c->use_compression = false;
|
c->use_compression = true;
|
||||||
c->use_metadata = true;
|
c->use_metadata = true;
|
||||||
c->use_executability = true;
|
c->use_executability = false;
|
||||||
c->compression_level = 7;
|
c->compression_level = 7;
|
||||||
c->chunk_size = 65536;
|
c->chunk_size = 65536;
|
||||||
c->use_sendfile = false;
|
c->use_sendfile = false;
|
||||||
c->use_delete = true;
|
c->use_delete = true;
|
||||||
c->use_incremental = true;
|
c->use_incremental = true;
|
||||||
c->size_only = true;
|
c->size_only = false;
|
||||||
c->ignore_times = true;
|
c->ignore_times = true;
|
||||||
c->use_delta = true;
|
c->use_delta = true;
|
||||||
c->whole_file = false;
|
c->whole_file = false;
|
||||||
c->delta_block_size = 4096;
|
c->delta_block_size = 4096;
|
||||||
c->delta_max_file_size = 987654321ULL;
|
c->delta_max_file_size = 200000000ULL;
|
||||||
c->backup = true;
|
c->backup = true;
|
||||||
c->backup_dir = str_dup("/golden/backup");
|
c->backup_dir = str_dup("/golden/backup");
|
||||||
c->remove_source_files = true;
|
c->remove_source_files = false;
|
||||||
c->follow_symlinks = true;
|
c->follow_symlinks = true;
|
||||||
c->copy_links = true;
|
c->copy_links = false;
|
||||||
c->safe_links = true;
|
c->safe_links = true;
|
||||||
c->copy_unsafe_links = true;
|
c->copy_unsafe_links = false;
|
||||||
c->preserve_hard_links = true;
|
c->preserve_hard_links = true;
|
||||||
c->preserve_acls = true;
|
c->preserve_acls = false;
|
||||||
c->preserve_xattrs = true;
|
c->preserve_xattrs = true;
|
||||||
c->preserve_devices = true;
|
c->preserve_devices = false;
|
||||||
c->preserve_sparse = true;
|
c->preserve_sparse = true;
|
||||||
c->preserve_specials = true;
|
c->preserve_specials = false;
|
||||||
c->copy_devices = true;
|
c->copy_devices = true;
|
||||||
c->write_devices = true;
|
c->write_devices = false;
|
||||||
c->ignore_existing = true;
|
c->ignore_existing = true;
|
||||||
c->existing = true;
|
c->existing = false;
|
||||||
c->update = true;
|
c->update = true;
|
||||||
c->inplace = false;
|
c->inplace = false;
|
||||||
c->delay_updates = false;
|
c->delay_updates = true;
|
||||||
c->append = false;
|
c->append = false;
|
||||||
c->use_fsync = true;
|
c->use_fsync = true;
|
||||||
c->append_verify = false;
|
c->append_verify = false;
|
||||||
c->delete_excluded = true;
|
c->delete_excluded = true;
|
||||||
c->force_delete = true;
|
c->force_delete = false;
|
||||||
c->delete_missing_args = true;
|
c->delete_missing_args = true;
|
||||||
c->delete_after = true;
|
c->delete_after = false;
|
||||||
c->preallocate = true;
|
c->preallocate = true;
|
||||||
c->max_delete = 42;
|
c->max_delete = 42;
|
||||||
c->relative = true;
|
c->relative = false;
|
||||||
c->prune_empty_dirs = true;
|
c->prune_empty_dirs = true;
|
||||||
c->mkpath = true;
|
c->mkpath = false;
|
||||||
c->delete_during = false;
|
c->delete_during = true;
|
||||||
c->delete_delay = false;
|
c->delete_delay = false;
|
||||||
c->temp_dir = str_dup("/golden/tmp");
|
c->temp_dir = str_dup("/golden/tmp");
|
||||||
c->partial = true;
|
c->partial = true;
|
||||||
@@ -2398,7 +2397,10 @@ static void golden_config_populate(Config* c) {
|
|||||||
c->checksum = true;
|
c->checksum = true;
|
||||||
c->modify_window = 3;
|
c->modify_window = 3;
|
||||||
c->compress_choice = str_dup("zstd");
|
c->compress_choice = str_dup("zstd");
|
||||||
c->chmod_spec = str_dup("u=rwX,go=rX");
|
/* "u=rwx,go=rx" is the same 11 bytes as the original "u=rwX,go=rX" (so the
|
||||||
|
* frame stays 633 bytes) but X is not in FastSync's chmod grammar, and the
|
||||||
|
* receive-side golden validates the frame. */
|
||||||
|
c->chmod_spec = str_dup("u=rwx,go=rx");
|
||||||
c->skip_compress_set = true;
|
c->skip_compress_set = true;
|
||||||
c->skip_compress_count = 2;
|
c->skip_compress_count = 2;
|
||||||
c->skip_compress_suffixes = calloc(2, sizeof(char*));
|
c->skip_compress_suffixes = calloc(2, sizeof(char*));
|
||||||
@@ -2410,7 +2412,7 @@ static void golden_config_populate(Config* c) {
|
|||||||
c->checksum_algo = CHECKSUM_ALGO_MD5;
|
c->checksum_algo = CHECKSUM_ALGO_MD5;
|
||||||
c->checksum_seed = 0x1122334455667788ULL;
|
c->checksum_seed = 0x1122334455667788ULL;
|
||||||
c->numeric_ids = true;
|
c->numeric_ids = true;
|
||||||
c->chown_uid_set = true;
|
c->chown_uid_set = false;
|
||||||
c->chown_uid = 1234;
|
c->chown_uid = 1234;
|
||||||
c->chown_gid_set = true;
|
c->chown_gid_set = true;
|
||||||
c->chown_gid = 5678;
|
c->chown_gid = 5678;
|
||||||
@@ -2425,11 +2427,11 @@ static void golden_config_populate(Config* c) {
|
|||||||
c->groupmap[0].from = 7;
|
c->groupmap[0].from = 7;
|
||||||
c->groupmap[0].to = 8;
|
c->groupmap[0].to = 8;
|
||||||
c->preserve_atimes = true;
|
c->preserve_atimes = true;
|
||||||
c->preserve_crtimes = true;
|
c->preserve_crtimes = false;
|
||||||
c->omit_dir_times = true;
|
c->omit_dir_times = true;
|
||||||
c->omit_link_times = true;
|
c->omit_link_times = false;
|
||||||
c->munge_links = true;
|
c->munge_links = true;
|
||||||
c->keep_dirlinks = true;
|
c->keep_dirlinks = false;
|
||||||
c->fake_super = true;
|
c->fake_super = true;
|
||||||
c->module = str_dup("goldenmod");
|
c->module = str_dup("goldenmod");
|
||||||
c->auth_user = str_dup("goldenuser");
|
c->auth_user = str_dup("goldenuser");
|
||||||
@@ -2441,13 +2443,27 @@ static void golden_config_populate(Config* c) {
|
|||||||
c->copy_as_gid = 222;
|
c->copy_as_gid = 222;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FNV-1a 64 over the exact config-frame bytes emitted by
|
/* The pinned golden frame (protocol 2.20.0). The values below are the only
|
||||||
* config_send_wire_block(). This pins field order and width: any reorder or
|
* thing that ties the generated table to the historical wire format; update
|
||||||
* resize changes the hash. */
|
* them ONLY with a PROTOCOL_VERSION bump and a documented reason. */
|
||||||
static unsigned long long capture_wire_hash(const Config* cfg, size_t* out_len) {
|
#define GOLDEN_WIRE_LEN 633
|
||||||
|
#define GOLDEN_WIRE_HASH 9160991280011164139ULL
|
||||||
|
|
||||||
|
static unsigned long long fnv1a_64(const unsigned char* buf, size_t len) {
|
||||||
|
unsigned long long h = 1469598103934665603ULL;
|
||||||
|
for (size_t i = 0; i < len; i++) {
|
||||||
|
h ^= (unsigned long long)buf[i];
|
||||||
|
h *= 1099511628211ULL;
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Capture the exact config-frame body emitted by config_send_wire_block() into
|
||||||
|
* a heap buffer. Returns NULL on any failure. */
|
||||||
|
static unsigned char* capture_wire_bytes(const Config* cfg, size_t* out_len) {
|
||||||
int p[2];
|
int p[2];
|
||||||
if (socketpair(AF_UNIX, SOCK_STREAM, 0, p) != 0)
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, p) != 0)
|
||||||
return 0;
|
return NULL;
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
if (pid == 0) {
|
if (pid == 0) {
|
||||||
close(p[1]);
|
close(p[1]);
|
||||||
@@ -2458,29 +2474,63 @@ static unsigned long long capture_wire_hash(const Config* cfg, size_t* out_len)
|
|||||||
_exit(ok ? 0 : 1);
|
_exit(ok ? 0 : 1);
|
||||||
}
|
}
|
||||||
close(p[0]);
|
close(p[0]);
|
||||||
unsigned long long h = 1469598103934665603ULL;
|
size_t capacity = 1024;
|
||||||
unsigned char buf[4096];
|
|
||||||
ssize_t n;
|
|
||||||
size_t total = 0;
|
size_t total = 0;
|
||||||
while ((n = read(p[1], buf, sizeof(buf))) > 0) {
|
unsigned char* bytes = malloc(capacity);
|
||||||
for (ssize_t i = 0; i < n; i++) {
|
if (!bytes) {
|
||||||
h ^= (unsigned long long)buf[i];
|
close(p[1]);
|
||||||
h *= 1099511628211ULL;
|
waitpid(pid, NULL, 0);
|
||||||
|
return NULL;
|
||||||
}
|
}
|
||||||
|
for (;;) {
|
||||||
|
if (total == capacity) {
|
||||||
|
size_t grown_capacity = capacity * 2;
|
||||||
|
unsigned char* grown = realloc(bytes, grown_capacity);
|
||||||
|
if (!grown) {
|
||||||
|
free(bytes);
|
||||||
|
close(p[1]);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
bytes = grown;
|
||||||
|
capacity = grown_capacity;
|
||||||
|
}
|
||||||
|
ssize_t n = read(p[1], bytes + total, capacity - total);
|
||||||
|
if (n < 0) {
|
||||||
|
free(bytes);
|
||||||
|
close(p[1]);
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (n == 0)
|
||||||
|
break;
|
||||||
total += (size_t)n;
|
total += (size_t)n;
|
||||||
}
|
}
|
||||||
close(p[1]);
|
close(p[1]);
|
||||||
int status = 0;
|
int status = 0;
|
||||||
waitpid(pid, &status, 0);
|
waitpid(pid, &status, 0);
|
||||||
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0)
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||||
return 0;
|
free(bytes);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
*out_len = total;
|
*out_len = total;
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FNV-1a 64 over the exact config-frame bytes emitted by
|
||||||
|
* config_send_wire_block(). This pins field order and width: any reorder or
|
||||||
|
* resize changes the hash. */
|
||||||
|
static unsigned long long capture_wire_hash(const Config* cfg, size_t* out_len) {
|
||||||
|
unsigned char* bytes = capture_wire_bytes(cfg, out_len);
|
||||||
|
if (!bytes)
|
||||||
|
return 0;
|
||||||
|
unsigned long long h = fnv1a_64(bytes, *out_len);
|
||||||
|
free(bytes);
|
||||||
return h;
|
return h;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Byte-for-byte wire compatibility guard (protocol 2.20.0). The expected hash
|
/* Byte-for-byte wire compatibility guard (protocol 2.20.0). The expected hash
|
||||||
* was captured from the pre-X-macro implementation; the refactor MUST NOT
|
* pins the pre-X-macro byte stream; the refactor MUST NOT change it. */
|
||||||
* change it. */
|
|
||||||
static void test_config_wire_golden() {
|
static void test_config_wire_golden() {
|
||||||
if (is_running_under_valgrind())
|
if (is_running_under_valgrind())
|
||||||
return;
|
return;
|
||||||
@@ -2490,9 +2540,180 @@ static void test_config_wire_golden() {
|
|||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
unsigned long long h = capture_wire_hash(c, &len);
|
unsigned long long h = capture_wire_hash(c, &len);
|
||||||
printf(" wire golden: len=%zu hash=%llu\n", len, h);
|
printf(" wire golden: len=%zu hash=%llu\n", len, h);
|
||||||
/* Captured from the pre-X-macro (protocol 2.20.0) implementation. */
|
EXPECT_TRUE(len == GOLDEN_WIRE_LEN);
|
||||||
EXPECT_TRUE(len == 633);
|
EXPECT_TRUE(h == GOLDEN_WIRE_HASH);
|
||||||
EXPECT_TRUE(h == 6163263374908258816ULL);
|
config_delete(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Receive-side oracle. Hashing the sender alone cannot catch a RECV KIND that
|
||||||
|
* reads a different width/order yet still round-trips symmetrically, so feed
|
||||||
|
* the SAME hash-pinned golden bytes through config_receive() and assert both
|
||||||
|
* the decoded struct fields and the derived bits. Because the bytes are
|
||||||
|
* anchored to the send golden, a divergence on either side fails here. */
|
||||||
|
static void test_config_wire_golden_receive() {
|
||||||
|
if (is_running_under_valgrind())
|
||||||
|
return;
|
||||||
|
Config* c = config_create();
|
||||||
|
EXPECT_NOT_NULL(c);
|
||||||
|
golden_config_populate(c);
|
||||||
|
|
||||||
|
size_t len = 0;
|
||||||
|
unsigned char* bytes = capture_wire_bytes(c, &len);
|
||||||
|
EXPECT_NOT_NULL(bytes);
|
||||||
|
EXPECT_TRUE(len == GOLDEN_WIRE_LEN);
|
||||||
|
EXPECT_TRUE(fnv1a_64(bytes, len) == GOLDEN_WIRE_HASH);
|
||||||
|
|
||||||
|
int p[2];
|
||||||
|
EXPECT_EQ_INT(socketpair(AF_UNIX, SOCK_STREAM, 0, p), 0);
|
||||||
|
io_set_fds(p[0], p[1]);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
close(p[1]);
|
||||||
|
io_set_fds(p[0], p[0]);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
Config* recv = config_receive(p[0]);
|
||||||
|
bool ok = recv != NULL;
|
||||||
|
if (ok) {
|
||||||
|
/* Full field-by-field comparison (generated from CONFIG_WIRE_FIELDS). */
|
||||||
|
ok = config_wire_equal(c, recv);
|
||||||
|
/* Explicit spot checks of the decoded struct, including derived bits. */
|
||||||
|
ok = ok && recv->eight_bit_output && recv->use_compression && recv->use_metadata &&
|
||||||
|
!recv->use_multithreading;
|
||||||
|
ok = ok && recv->compression_level == 7 && recv->chunk_size == 65536;
|
||||||
|
ok = ok && recv->use_delta && !recv->whole_file && recv->use_xattrs;
|
||||||
|
/* Bounded/validated KINDs decoded from the pinned bytes. */
|
||||||
|
ok = ok && recv->checksum_algo == CHECKSUM_ALGO_MD5;
|
||||||
|
ok = ok && recv->super_mode == SUPER_MODE_ON;
|
||||||
|
ok = ok && recv->chown_uid == 1234 && recv->chown_gid == 5678;
|
||||||
|
ok = ok && recv->usermap_count == 2 && recv->usermap[0].from == IDENTITY_MATCH_ANY &&
|
||||||
|
recv->usermap[0].to == 1000 && recv->usermap[1].from == 5 && recv->usermap[1].to == 6;
|
||||||
|
ok = ok && recv->basis_count == 2 && recv->basis_dirs[0].type == BASIS_DEST_COMPARE &&
|
||||||
|
recv->basis_dirs[1].type == BASIS_DEST_LINK;
|
||||||
|
ok = ok && recv->module != NULL && strcmp(recv->module, "goldenmod") == 0;
|
||||||
|
ok = ok && recv->copy_as_set && recv->copy_as_uid == 111 && recv->copy_as_gid == 222;
|
||||||
|
}
|
||||||
|
config_delete(recv);
|
||||||
|
close(p[0]);
|
||||||
|
_exit(ok ? 0 : 1);
|
||||||
|
}
|
||||||
|
close(p[0]);
|
||||||
|
io_set_fds(p[1], p[1]);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
size_t written = 0;
|
||||||
|
bool wrote = true;
|
||||||
|
while (written < len) {
|
||||||
|
ssize_t n = write(p[1], bytes + written, len - written);
|
||||||
|
if (n <= 0) {
|
||||||
|
wrote = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
written += (size_t)n;
|
||||||
|
}
|
||||||
|
Status status = STATUS_ERROR;
|
||||||
|
bool got_status = wrote && receive_status(p[1], &status);
|
||||||
|
close(p[1]);
|
||||||
|
free(bytes);
|
||||||
|
int child_status = 0;
|
||||||
|
waitpid(pid, &child_status, 0);
|
||||||
|
EXPECT_TRUE(got_status && status == STATUS_OK);
|
||||||
|
EXPECT_TRUE(WIFEXITED(child_status) && WEXITSTATUS(child_status) == 0);
|
||||||
|
config_delete(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hand-build a frame that is valid up to the first core BOOL, then write an
|
||||||
|
* out-of-range boolean (2): a BOOL receiver must reject anything but 0/1. */
|
||||||
|
static void write_frame_with_invalid_bool(int fd) {
|
||||||
|
send_str(fd, PROTOCOL_VERSION);
|
||||||
|
send_int(fd, 1); /* eight_bit_output */
|
||||||
|
unsigned long long max_alloc = DEFAULT_MAX_ALLOC;
|
||||||
|
send_n_data(fd, &max_alloc, sizeof(max_alloc));
|
||||||
|
send_str(fd, "/src");
|
||||||
|
send_str(fd, "/dst");
|
||||||
|
send_int(fd, 2); /* save_to_disk: not 0/1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feed a caller-built frame into config_receive() and report whether the
|
||||||
|
* receiver rejected it. The writer runs in a child (SIGPIPE ignored) so a
|
||||||
|
* mid-frame rejection cannot kill the test process. */
|
||||||
|
static bool receive_hand_built_frame_rejected(void (*write_frame)(int fd)) {
|
||||||
|
int p[2];
|
||||||
|
if (socketpair(AF_UNIX, SOCK_STREAM, 0, p) != 0)
|
||||||
|
return false;
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
(void)signal(SIGPIPE, SIG_IGN);
|
||||||
|
close(p[0]);
|
||||||
|
io_set_fds(p[1], p[1]);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
write_frame(p[1]);
|
||||||
|
close(p[1]);
|
||||||
|
_exit(0);
|
||||||
|
}
|
||||||
|
close(p[1]);
|
||||||
|
io_set_fds(p[0], p[0]);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
Config* recv = config_receive(p[0]);
|
||||||
|
bool rejected = recv == NULL;
|
||||||
|
config_delete(recv);
|
||||||
|
close(p[0]);
|
||||||
|
int status = 0;
|
||||||
|
waitpid(pid, &status, 0);
|
||||||
|
return rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Receive-side bounds for the bounded/validated KINDs that the round-trip
|
||||||
|
* helper cannot exercise (an illegal value has no symmetric sender). */
|
||||||
|
static void test_config_wire_receive_bounds() {
|
||||||
|
if (is_running_under_valgrind())
|
||||||
|
return;
|
||||||
|
|
||||||
|
/* BOOL: only 0/1 is a legal wire value. */
|
||||||
|
EXPECT_TRUE(receive_hand_built_frame_rejected(write_frame_with_invalid_bool));
|
||||||
|
|
||||||
|
/* RAW_MAXALLOC: zero is rejected before it can become the session ceiling. */
|
||||||
|
Config* c = config_create();
|
||||||
|
EXPECT_NOT_NULL(c);
|
||||||
|
c->send_directory = str_dup("/src");
|
||||||
|
c->receive_root_directory = str_dup("/dst");
|
||||||
|
c->max_alloc = 0;
|
||||||
|
EXPECT_TRUE(roundtrip_config_rejected(c));
|
||||||
|
config_delete(c);
|
||||||
|
|
||||||
|
/* STR_MODULE: a name outside [A-Za-z0-9._-] is refused. */
|
||||||
|
c = config_create();
|
||||||
|
EXPECT_NOT_NULL(c);
|
||||||
|
c->send_directory = str_dup("/src");
|
||||||
|
c->receive_root_directory = str_dup("/dst");
|
||||||
|
c->module = str_dup("bad module");
|
||||||
|
EXPECT_TRUE(roundtrip_config_rejected(c));
|
||||||
|
config_delete(c);
|
||||||
|
|
||||||
|
/* INT_IDMAPCOUNT: one past the identity-map cap is refused at the count. */
|
||||||
|
c = config_create();
|
||||||
|
EXPECT_NOT_NULL(c);
|
||||||
|
c->send_directory = str_dup("/src");
|
||||||
|
c->receive_root_directory = str_dup("/dst");
|
||||||
|
c->usermap_count = MAX_IDENTITY_MAP + 1;
|
||||||
|
c->usermap = calloc((size_t)c->usermap_count, sizeof(IdentityMap));
|
||||||
|
if (c->usermap) {
|
||||||
|
for (int i = 0; i < c->usermap_count; i++) {
|
||||||
|
c->usermap[i].from = 0;
|
||||||
|
c->usermap[i].to = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EXPECT_TRUE(roundtrip_config_rejected(c));
|
||||||
|
config_delete(c);
|
||||||
|
|
||||||
|
/* INT_IDENTITY: an out-of-range chown_uid (below IDENTITY_MATCH_ANY) is
|
||||||
|
* refused by the identity validator. */
|
||||||
|
c = config_create();
|
||||||
|
EXPECT_NOT_NULL(c);
|
||||||
|
c->send_directory = str_dup("/src");
|
||||||
|
c->receive_root_directory = str_dup("/dst");
|
||||||
|
c->chown_uid_set = true;
|
||||||
|
c->chown_uid = IDENTITY_MATCH_ANY - 1;
|
||||||
|
EXPECT_TRUE(roundtrip_config_rejected(c));
|
||||||
config_delete(c);
|
config_delete(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2552,6 +2773,8 @@ void test_config() {
|
|||||||
test_config_invariants_error_all_combinations();
|
test_config_invariants_error_all_combinations();
|
||||||
test_config_receive_rejects_unified_invariants();
|
test_config_receive_rejects_unified_invariants();
|
||||||
test_config_wire_golden();
|
test_config_wire_golden();
|
||||||
|
test_config_wire_golden_receive();
|
||||||
|
test_config_wire_receive_bounds();
|
||||||
test_config_wire_roundtrip_all_fields();
|
test_config_wire_roundtrip_all_fields();
|
||||||
}
|
}
|
||||||
test_identity_copy_as_refused();
|
test_identity_copy_as_refused();
|
||||||
|
|||||||
Reference in New Issue
Block a user