From 87f6cb02431d6682bc9592a90d458eee58625594 Mon Sep 17 00:00:00 2001 From: TapTap Date: Sun, 13 Sep 2026 10:28:26 +0200 Subject: [PATCH] refactor(config): single X-macro table for serialized fields Every Config field that crosses the wire was declared in up to six places (struct member, config_set_defaults, send_*, receive_*, and the two CLI option tables) and could drift silently. Add CONFIG_WIRE_FIELDS in config.h: one ordered per-segment table where each serialized field is declared once with its C type, default and wire codec (KIND). config.h now expands the table to declare the struct members; config_set_defaults() expands it to assign the defaults; and config_send_wire_block()/config_receive() expand the per-segment lists to emit/consume the frame. The per-segment function names, call order and segment boundaries are preserved exactly. Fields with genuinely custom logic keep dedicated helpers but are still declared once in the table: the protocol-version handshake (HEADER), daemon SCRAM auth (STR_REDACTED_AUTH), the daemon module name (STR_MODULE), the repeated count+array blocks (BLOCK_SKIP_SUFFIXES/BLOCK_BASIS/BLOCK_IDMAP), --copy-as presence/ids (COPY_AS_*), and the derived --delta / use_xattrs bits (DERIVED_DELTA, BOOL_XATTR_DERIVE). The version field remains a special header (validated before any other field is parsed) and is sent by config_send_wire_block() explicitly. No public field is renamed and PROTOCOL_VERSION stays "2.20.0". Because the struct declaration order is no longer the wire order, the wire order is now enforced solely by the table and by a byte-exact golden test (follow-up commit). Add config_send_wire_block() so that test can hash the frame body without the STATUS_OK handshake. --- src/shared/config.c | 966 +++++++++++++++++--------------------------- src/shared/config.h | 635 +++++++++++++++++------------ 2 files changed, 735 insertions(+), 866 deletions(-) diff --git a/src/shared/config.c b/src/shared/config.c index 64adf10..24ccc1f 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -18,33 +18,16 @@ #include static void config_set_defaults(Config* config) { - config->version = str_dup(PROTOCOL_VERSION); - config->send_directory = NULL; - config->receive_root_directory = NULL; - config->save_to_disk = false; - config->use_multithreading = false; config->scanner_threads = 0; - config->use_chunk_serialization = false; - config->use_compression = false; - config->use_metadata = false; - config->use_executability = false; config->metadata_explicitly_disabled = false; config->show_progress = false; config->dry_run = false; - config->remove_source_files = false; - config->use_delete = false; - config->compression_level = 5; config->compression_threads = 0; - config->use_sendfile = false; - config->chunk_size = DEFAULT_CHUNK_SIZE; config->ssh_port = 22; config->transport = TRANSPORT_TCP; config->ssh_destination = NULL; - config->module = NULL; - config->auth_user = NULL; config->auth_password = NULL; config->password_file = NULL; - config->iconv_spec = NULL; config->fastsync_server_path = NULL; config->exclude_patterns = NULL; config->exclude_count = 0; @@ -52,16 +35,7 @@ static void config_set_defaults(Config* config) { config->include_count = 0; config->max_size = 0; config->min_size = 0; - config->max_alloc = DEFAULT_MAX_ALLOC; - config->use_incremental = false; - config->ignore_times = false; - config->size_only = false; - config->use_delta = false; config->whole_file = false; - config->fuzzy = false; - config->modify_window = 0; - config->delta_block_size = DELTA_BLOCK_SIZE_DEFAULT; - config->delta_max_file_size = DELTA_MAX_FILE_SIZE; config->use_tls = false; config->tls_cert = NULL; config->tls_key = NULL; @@ -75,27 +49,10 @@ static void config_set_defaults(Config* config) { config->timeout = 0; config->contimeout = 10; config->quiet = false; - config->backup = false; - config->backup_dir = NULL; config->stats = false; config->max_depth = 0; config->log_file = NULL; - config->follow_symlinks = false; - config->partial = false; - config->copy_links = false; - config->safe_links = false; - config->copy_unsafe_links = false; config->copy_dirlinks = false; - config->munge_links = false; - config->keep_dirlinks = false; - config->preserve_hard_links = false; - config->preserve_acls = false; - config->preserve_xattrs = false; - config->preserve_devices = false; - config->preserve_sparse = false; - config->preserve_specials = false; - config->copy_devices = false; - config->write_devices = false; config->itemize_changes = false; config->out_format = NULL; config->log_file_format = NULL; @@ -103,49 +60,23 @@ static void config_set_defaults(Config* config) { config->debug_level = 0; config->list_only = false; config->human_readable = false; - config->eight_bit_output = false; - config->existing = false; - config->ignore_existing = false; - config->update = false; - config->inplace = false; - config->delay_updates = false; - config->use_fsync = false; - config->append = false; - config->append_verify = false; - config->preallocate = false; - config->delete_excluded = false; - config->delete_after = false; - config->max_delete = -1; config->ignore_errors = false; - config->force_delete = false; config->ignore_missing_args = false; - config->delete_missing_args = false; config->filters = NULL; config->files_from = NULL; config->files_from_set = NULL; config->from0 = false; config->cvs_exclude = false; config->per_dir_filter = false; - config->prune_empty_dirs = false; config->one_file_system = false; - config->relative = false; config->no_implied_dirs = false; config->dirs = false; - config->mkpath = false; config->rsh_command = NULL; config->blocking_io = false; config->outbuf = OUTBUF_BLOCK; config->old_args = false; - config->temp_dir = NULL; config->remote_options = NULL; config->remote_option_count = 0; - config->basis_dirs = NULL; - config->basis_count = 0; - config->partial_dir = NULL; - config->suffix = NULL; - config->delete_before = false; - config->delete_during = false; - config->delete_delay = false; config->address = NULL; config->ipv6 = false; config->ipv4 = false; @@ -153,35 +84,9 @@ static void config_set_defaults(Config* config) { config->sockopt_count = 0; config->daemon = false; config->no_motd = false; - config->checksum = false; - config->checksum_algo = CHECKSUM_ALGO_XXH64; - config->checksum_seed = 0; - config->compress_choice = NULL; - config->chmod_spec = NULL; - config->skip_compress_suffixes = NULL; - config->skip_compress_count = 0; - config->skip_compress_set = false; - config->numeric_ids = false; - config->chown_uid_set = false; - config->chown_uid = 0; - config->chown_gid_set = false; - config->chown_gid = 0; - config->usermap = NULL; - config->usermap_count = 0; - config->groupmap = NULL; - config->groupmap_count = 0; - config->super_mode = SUPER_MODE_AUTO; config->delay_context = NULL; - config->preserve_atimes = false; - config->preserve_crtimes = false; - config->omit_dir_times = false; - config->omit_link_times = false; config->open_noatime = false; config->use_xattrs = false; - config->fake_super = false; - config->copy_as_set = false; - config->copy_as_uid = 0; - config->copy_as_gid = 0; config->trust_sender = false; config->stop_after_mins = 0; config->stop_at = 0; @@ -189,6 +94,12 @@ static void config_set_defaults(Config* config) { config->write_batch = NULL; config->only_write_batch = NULL; config->read_batch = NULL; + + /* Serialized fields: defaults come from the CONFIG_WIRE_FIELDS table so the + * member declaration, default and wire codec can never drift apart. */ +#define CONFIG_DEFAULT_FIELD(name, ctype, def, kind) config->name = def; + CONFIG_WIRE_FIELDS(CONFIG_DEFAULT_FIELD) +#undef CONFIG_DEFAULT_FIELD } static bool valid_wire_bool(int value) { @@ -812,408 +723,52 @@ void config_delete(Config* config) { free(config); } -/* Each helper is deliberately ordered to match the wire format. Keep the - * helper call order in config_send and config_receive unchanged when adding - * fields. */ -static bool send_core_fields(int fd, const Config* c) { - if (!send_str(fd, c->version) || !send_int(fd, c->eight_bit_output)) +/* --------------------------------------------------------------------------- + * Wire codec helpers. + * + * The CONFIG_WIRE_*_FIELDS tables in config.h drive the send/receive + * sequences below. Each field's KIND names a CONFIG_SEND_ / + * CONFIG_RECV_ macro (defined after the helpers) that expands to the + * exact primitive call the previous hand-written code used, so the byte + * stream is unchanged. Fields whose per-field logic is not a plain scalar + * (bounded enums, redacted auth, repeated count+array blocks) delegate to a + * dedicated helper here. + * ------------------------------------------------------------------------- */ + +/* --max-alloc: raw 64-bit value, clamped server-side and installed as the + * session allocation ceiling. A zero value is rejected. */ +static bool config_receive_max_alloc(int fd, unsigned long long* value) { + if (!receive_n_data(fd, value, sizeof(*value)) || *value == 0) return false; - protocol_set_8_bit_output(c->eight_bit_output); - if (!send_n_data(fd, &c->max_alloc, sizeof(c->max_alloc))) - return false; - return send_str(fd, c->send_directory) && send_str(fd, c->receive_root_directory) && - send_int(fd, c->save_to_disk) && send_int(fd, c->use_multithreading) && - send_int(fd, c->use_chunk_serialization) && send_int(fd, c->use_compression) && - send_int(fd, c->use_metadata) && send_int(fd, c->use_executability) && - send_int(fd, c->compression_level) && - send_n_data(fd, &c->chunk_size, sizeof(c->chunk_size)) && send_int(fd, c->use_sendfile); -} - -static bool send_delta_fields(int fd, const Config* c) { - return send_int(fd, c->use_delete) && send_int(fd, c->use_incremental) && - send_int(fd, c->size_only) && send_int(fd, c->ignore_times) && - send_int(fd, c->use_delta && !c->whole_file) && - send_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) && - send_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long)); -} - -static bool send_file_options(int fd, const Config* c) { - /* Device/special preservation flags cross the wire so the receiver knows a - * special/device entry must be recreated. Trailing fields; protocol 2.13.0. */ - return send_int(fd, c->backup) && send_str(fd, c->backup_dir ? c->backup_dir : "") && - send_int(fd, c->remove_source_files) && send_int(fd, c->follow_symlinks) && - send_int(fd, c->copy_links) && send_int(fd, c->safe_links) && - send_int(fd, c->copy_unsafe_links) && send_int(fd, c->preserve_hard_links) && - send_int(fd, c->preserve_acls) && send_int(fd, c->preserve_xattrs) && - send_int(fd, c->preserve_devices) && send_int(fd, c->preserve_sparse) && - send_int(fd, c->preserve_specials) && send_int(fd, c->copy_devices) && - send_int(fd, c->write_devices); -} - -static bool send_selection_options(int fd, const Config* c) { - return send_int(fd, c->ignore_existing) && send_int(fd, c->existing) && send_int(fd, c->update) && - send_int(fd, c->inplace) && send_int(fd, c->delay_updates) && send_int(fd, c->append) && - send_int(fd, c->use_fsync) && send_int(fd, c->append_verify) && - send_int(fd, c->delete_excluded) && send_int(fd, c->force_delete) && - send_int(fd, c->delete_missing_args) && send_int(fd, c->delete_after) && - send_int(fd, c->preallocate) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && - send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs) && - send_int(fd, c->mkpath) && send_int(fd, c->delete_during) && send_int(fd, c->delete_delay); -} - -static bool send_skip_compress_options(int fd, const Config* c) { - if (!send_int(fd, c->skip_compress_set) || !send_int(fd, c->skip_compress_count)) - return false; - for (int i = 0; i < c->skip_compress_count; i++) { - if (!send_str(fd, c->skip_compress_suffixes[i])) - return false; - } + if (*value > MAX_SERVER_ALLOC) + *value = MAX_SERVER_ALLOC; + protocol_session_set_max_alloc(NULL, *value); return true; } -static bool send_resume_options(int fd, const Config* c) { - return send_str(fd, c->temp_dir ? c->temp_dir : "") && send_int(fd, c->partial) && - send_str(fd, c->partial_dir ? c->partial_dir : "") && - send_str(fd, c->suffix ? c->suffix : "") && send_int(fd, c->delete_before) && - send_int(fd, c->checksum) && send_int(fd, c->modify_window) && - send_str(fd, c->compress_choice ? c->compress_choice : "") && - send_str(fd, c->chmod_spec ? c->chmod_spec : "") && send_skip_compress_options(fd, c); -} - -static bool send_basis_options(int fd, const Config* c) { - if (!send_int(fd, c->basis_count)) +/* Optional string: the sender serializes an unset (NULL) string as "", so the + * receiver canonicalizes the empty wire value back to NULL to preserve + * NULL-vs-empty semantics. */ +static bool config_receive_optional_str(int fd, ConfigStringBudget* budget, char** out) { + char* value = config_receive_str(fd, budget); + if (!value) return false; - for (int i = 0; i < c->basis_count; i++) { - if (!send_int(fd, (int)c->basis_dirs[i].type) || - !send_str(fd, c->basis_dirs[i].path ? c->basis_dirs[i].path : "")) - return false; + if (*value == '\0') { + free(value); + *out = NULL; + return true; } + *out = value; return true; } -/* -y/--fuzzy (receiver-side similar-file basis selection). Trailing field on - * the config frame; protocol 2.9.0. */ -static bool send_fuzzy_option(int fd, const Config* c) { - return send_int(fd, c->fuzzy); -} - -/* --checksum-choice/--cc + --checksum-seed. The algorithm id and seed travel - * with the config so the receiver hashes the on-disk old file with the same - * parameters the sender used for its digest (see checksum.h). Trailing fields - * on the config frame; protocol 2.10.0. */ -static bool send_checksum_options(int fd, const Config* c) { - return send_int(fd, c->checksum_algo) && - send_n_data(fd, &c->checksum_seed, sizeof(c->checksum_seed)); -} - -static bool receive_core_fields(int fd, Config* c, ConfigStringBudget* budget) { - int value; - if (!receive_wire_bool(fd, &c->eight_bit_output)) - return false; - protocol_set_8_bit_output(c->eight_bit_output); - if (!receive_n_data(fd, &c->max_alloc, sizeof(c->max_alloc)) || c->max_alloc == 0) - return false; - if (c->max_alloc > MAX_SERVER_ALLOC) - c->max_alloc = MAX_SERVER_ALLOC; - protocol_session_set_max_alloc(NULL, c->max_alloc); - c->send_directory = config_receive_str(fd, budget); - c->receive_root_directory = config_receive_str(fd, budget); - if (!c->send_directory || !c->receive_root_directory) - return false; - if (!receive_wire_bool(fd, &c->save_to_disk) || !receive_wire_bool(fd, &c->use_multithreading) || - !receive_wire_bool(fd, &c->use_chunk_serialization) || - !receive_wire_bool(fd, &c->use_compression) || !receive_wire_bool(fd, &c->use_metadata) || - !receive_wire_bool(fd, &c->use_executability)) - return false; - if (!receive_int(fd, &value)) - return false; - c->compression_level = value; - if (!receive_n_data(fd, &c->chunk_size, sizeof(c->chunk_size))) - return false; - if (!receive_wire_bool(fd, &c->use_sendfile)) - return false; - return true; -} - -static bool receive_delta_fields(int fd, Config* c) { - if (!receive_wire_bool(fd, &c->use_delete)) - return false; - if (!receive_wire_bool(fd, &c->use_incremental)) - return false; - if (!receive_wire_bool(fd, &c->size_only)) - return false; - if (!receive_wire_bool(fd, &c->ignore_times)) - return false; - if (!receive_wire_bool(fd, &c->use_delta)) - return false; - return receive_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) && - receive_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long)); -} - -static bool receive_file_options(int fd, Config* c, ConfigStringBudget* budget) { - if (!receive_wire_bool(fd, &c->backup)) - return false; - char* backup_dir = config_receive_str(fd, budget); - if (!backup_dir) - return false; - if (*backup_dir != '\0') { - c->backup_dir = backup_dir; - } else { - /* The sender serializes an unset (NULL) string as "", so canonicalize the - empty wire value back to NULL to preserve NULL-vs-empty semantics. */ - free(backup_dir); - } - if (!receive_wire_bool(fd, &c->remove_source_files)) - return false; - bool* flags[] = {&c->follow_symlinks, &c->copy_links, &c->safe_links, - &c->copy_unsafe_links, &c->preserve_hard_links, &c->preserve_acls, - &c->preserve_xattrs, &c->preserve_devices, &c->preserve_sparse, - &c->preserve_specials, &c->copy_devices, &c->write_devices}; - for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { - if (!receive_wire_bool(fd, flags[i])) - return false; - } - return true; -} - -static bool receive_selection_options(int fd, Config* c) { - bool* flags[] = {&c->ignore_existing, - &c->existing, - &c->update, - &c->inplace, - &c->delay_updates, - &c->append, - &c->use_fsync, - &c->append_verify, - &c->delete_excluded, - &c->force_delete, - &c->delete_missing_args, - &c->delete_after, - &c->preallocate}; - for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { - if (!receive_wire_bool(fd, flags[i])) - return false; - } - if (!receive_n_data(fd, &c->max_delete, sizeof(c->max_delete))) - return false; - if (!receive_wire_bool(fd, &c->relative)) - return false; - if (!receive_wire_bool(fd, &c->prune_empty_dirs)) - return false; - if (!receive_wire_bool(fd, &c->mkpath)) - return false; - if (!receive_wire_bool(fd, &c->delete_during)) - return false; - return receive_wire_bool(fd, &c->delete_delay); -} - -static bool receive_resume_options(int fd, Config* c, ConfigStringBudget* budget) { - char* temp_dir = config_receive_str(fd, budget); - if (!temp_dir) - return false; - if (*temp_dir != '\0') { - c->temp_dir = temp_dir; - } else { - free(temp_dir); - } - if (!receive_wire_bool(fd, &c->partial)) - return false; - /* These options have NULL client defaults, so the sender transmits an empty - string for "unset". Canonicalize the empty wire value back to NULL so - receivers observe exactly what the client configured (plain --backup, for - example, must not look like --backup-dir ""). */ - char* partial_dir = config_receive_str(fd, budget); - if (!partial_dir) - return false; - if (*partial_dir != '\0') { - c->partial_dir = partial_dir; - } else { - free(partial_dir); - } - char* suffix = config_receive_str(fd, budget); - if (!suffix) - return false; - if (*suffix != '\0') { - c->suffix = suffix; - } else { - free(suffix); - } - if (!receive_wire_bool(fd, &c->delete_before)) - return false; - if (!receive_wire_bool(fd, &c->checksum)) - return false; - if (!receive_n_data(fd, &c->modify_window, sizeof(c->modify_window))) - return false; - c->compress_choice = config_receive_str(fd, budget); - if (!c->compress_choice) - return false; - c->chmod_spec = config_receive_str(fd, budget); - if (!c->chmod_spec || !receive_wire_bool(fd, &c->skip_compress_set) || - !receive_int(fd, &c->skip_compress_count) || c->skip_compress_count < 0 || - c->skip_compress_count > MAX_SKIP_COMPRESS_SUFFIXES) - return false; - if (c->skip_compress_count > 0) { - c->skip_compress_suffixes = calloc((size_t)c->skip_compress_count, sizeof(char*)); - if (!c->skip_compress_suffixes) - return false; - for (int i = 0; i < c->skip_compress_count; i++) { - c->skip_compress_suffixes[i] = config_receive_str(fd, budget); - if (!c->skip_compress_suffixes[i]) - return false; - } - } - return true; -} - -static bool receive_basis_options(int fd, Config* c, ConfigStringBudget* budget) { - int count; - if (!receive_int(fd, &count)) - return false; - if (count < 0 || count > MAX_BASIS_DIRS) - return false; - for (int i = 0; i < count; i++) { - int type; - if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK) - return false; - char* path = config_receive_str(fd, budget); - if (!path) - return false; - /* config_basis_append validates and canonicalizes the path; a rejected - path (absolute / traversal / empty) drops the whole connection. */ - bool ok = config_basis_append(c, (BasisDestType)type, path) == 0; - free(path); - if (!ok) - return false; - } - return true; -} - -static bool receive_fuzzy_option(int fd, Config* c) { - return receive_wire_bool(fd, &c->fuzzy); -} - -static bool receive_checksum_options(int fd, Config* c) { - int algo; - if (!receive_int(fd, &algo) || !checksum_algo_valid(algo)) - return false; - c->checksum_algo = algo; - return receive_n_data(fd, &c->checksum_seed, sizeof(c->checksum_seed)); -} - -/* --numeric-ids / --usermap / --groupmap / --chown (identity mapping). The - * receiver needs these to apply the ownership the client requested, so they - * cross the config frame. Trailing fields; protocol 2.11.0. */ -static bool send_identity_map(int fd, const IdentityMap* map, int count) { - if (!send_int(fd, count)) - return false; - for (int i = 0; i < count; i++) { - if (!send_int(fd, map[i].from) || !send_int(fd, map[i].to)) - return false; - } - return true; -} - -static bool send_identity_options(int fd, const Config* c) { - return send_int(fd, c->numeric_ids) && send_int(fd, c->chown_uid_set) && - send_int(fd, c->chown_uid) && send_int(fd, c->chown_gid_set) && - send_int(fd, c->chown_gid) && send_identity_map(fd, c->usermap, c->usermap_count) && - send_identity_map(fd, c->groupmap, c->groupmap_count); -} - -static bool receive_identity_map(int fd, int* pcount, IdentityMap** pmap) { - int count; - if (!receive_int(fd, &count) || count < 0 || count > MAX_IDENTITY_MAP) - return false; - if (count > 0) { - IdentityMap* map = calloc((size_t)count, sizeof(IdentityMap)); - if (!map) - return false; - for (int i = 0; i < count; i++) { - if (!receive_int(fd, &map[i].from) || !receive_int(fd, &map[i].to)) { - free(map); - return false; - } - } - *pmap = map; - } - *pcount = count; - return true; -} - -static bool receive_identity_options(int fd, Config* c) { - int numeric_ids; - if (!receive_int(fd, &numeric_ids) || !valid_wire_bool(numeric_ids)) - return false; - c->numeric_ids = numeric_ids != 0; - if (!receive_wire_bool(fd, &c->chown_uid_set) || !receive_int(fd, &c->chown_uid) || - !receive_wire_bool(fd, &c->chown_gid_set) || !receive_int(fd, &c->chown_gid)) - return false; - if (c->chown_uid < IDENTITY_MATCH_ANY || c->chown_gid < IDENTITY_MATCH_ANY) - return false; - return receive_identity_map(fd, &c->usermap_count, &c->usermap) && - receive_identity_map(fd, &c->groupmap_count, &c->groupmap); -} - -/* -U/--atimes, -N/--crtimes (affect both sender capture and receiver apply) - * and -O/--omit-dir-times, -J/--omit-link-times (receiver-side prefs) all cross - * the wire so the receiver knows what to apply / suppress. --open-noatime is - * client-only (it only governs the sender's source reads) and is never - * serialized. Trailing fields; protocol 2.12.0. */ -static bool send_metadata_times_options(int fd, const Config* c) { - return send_int(fd, c->preserve_atimes) && send_int(fd, c->preserve_crtimes) && - send_int(fd, c->omit_dir_times) && send_int(fd, c->omit_link_times); -} - -static bool receive_metadata_times_options(int fd, Config* c) { - return receive_wire_bool(fd, &c->preserve_atimes) && - receive_wire_bool(fd, &c->preserve_crtimes) && receive_wire_bool(fd, &c->omit_dir_times) && - receive_wire_bool(fd, &c->omit_link_times); -} - -/* Phase 4 symlink-trust: --munge-links and -K/--keep-dirlinks. Both CROSS the - * wire (the receiver unmunges symlink targets and, with -K, follows an in-root - * destination symlink-to-directory). -k/--copy-dirlinks is sender-only and is - * never serialized. Trailing fields; protocol 2.13.0. */ -static bool send_symlink_trust_options(int fd, const Config* c) { - return send_int(fd, c->munge_links) && send_int(fd, c->keep_dirlinks); -} - -static bool receive_symlink_trust_options(int fd, Config* c) { - return receive_wire_bool(fd, &c->munge_links) && receive_wire_bool(fd, &c->keep_dirlinks); -} - -/* -X/--xattrs, -A/--acls, --fake-super (Phase-4). The receiver learns - * preserve_xattrs/preserve_acls from the earlier file-options block and - * recomputes the derived use_xattrs there; only --fake-super (receiver-side - * behavior) needs an extra wire bit. Trailing field; protocol 2.13.0. */ -static bool send_phase4_xattr_options(int fd, const Config* c) { - return send_int(fd, c->fake_super); -} - -static bool receive_phase4_xattr_options(int fd, Config* c) { - if (!receive_wire_bool(fd, &c->fake_super)) - return false; - c->use_xattrs = c->preserve_acls || c->preserve_xattrs; - return true; -} - -/* Daemon module selection (Wave A, protocol 2.15.0). Trailing string on the - * config frame, sent after the Phase-4 xattr block and before the ack. The - * client composes it from a host::module/path destination; an unset module is - * serialized as "" and canonicalized back to NULL on receive so the two never - * look different to a peer. */ -static bool send_daemon_module(int fd, const Config* c) { - return send_str(fd, c->module ? c->module : ""); -} - -static bool receive_daemon_module(int fd, Config* c, ConfigStringBudget* budget) { +/* Daemon module name (Wave A, protocol 2.15.0): an unset module is "" (-> NULL + * on receive). A hostile over-long/invalid name is rejected with an explicit + * STATUS_ERROR rather than logged and accepted. */ +static bool config_receive_module(int fd, Config* c, ConfigStringBudget* budget) { char* module = config_receive_str(fd, budget); if (!module) return false; - /* Guard against a hostile client flooding the log with an over-long module - * name: only an empty string (module-less) or a valid module name - * (bounded by DAEMON_MAX_MODULE_NAME) is accepted. This is an input - * guard, not a wire-format change. */ if (*module != '\0' && !daemon_module_name_valid(module)) { log_message(LOG_LEVEL_WARNING, "Daemon client sent an invalid or over-long module name"); free(module); @@ -1228,27 +783,15 @@ static bool receive_daemon_module(int fd, Config* c, ConfigStringBudget* budget) return true; } -/* Daemon password credentials (A7 remediation, protocol 2.19.0). A single - * presence int is followed, when set, by ONLY the username; the password is - * never serialized. The daemon answers an auth-required module with the SCRAM - * challenge (see the auth exchange below). */ -static bool send_daemon_auth(int fd, const Config* c) { - bool present = c->auth_user != NULL && c->auth_user[0] != '\0'; - if (!send_int(fd, present ? 1 : 0)) - return false; - if (!present) - return true; - /* Redacted send: the username must never reach a --verbose debug log. */ - return send_str_redacted(fd, c->auth_user); -} - -static bool receive_daemon_auth(int fd, Config* c, ConfigStringBudget* budget) { +/* Daemon auth username (A7 remediation, protocol 2.19.0): a presence int is + * followed, when set, by ONLY the redacted username; the password is never + * serialized. */ +static bool config_receive_auth_user(int fd, Config* c, ConfigStringBudget* budget) { int present; if (!receive_int(fd, &present) || !valid_wire_bool(present)) return false; if (!present) return true; - /* Redacted receive: never log the incoming username body. */ char* user = config_receive_str_redacted(fd, budget); if (!user) return false; @@ -1261,6 +804,296 @@ static bool receive_daemon_auth(int fd, Config* c, ConfigStringBudget* budget) { return true; } +static bool config_send_auth_user(int fd, const Config* c) { + bool present = c->auth_user != NULL && c->auth_user[0] != '\0'; + if (!send_int(fd, present ? 1 : 0)) + return false; + if (!present) + return true; + /* Redacted send: the username must never reach a --verbose debug log. */ + return send_str_redacted(fd, c->auth_user); +} + +static bool config_receive_checksum_algo(int fd, int* value) { + int algo; + if (!receive_int(fd, &algo) || !checksum_algo_valid(algo)) + return false; + *value = algo; + return true; +} + +static bool config_receive_super_mode(int fd, SuperMode* value) { + int mode; + if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF) + return false; + *value = (SuperMode)mode; + return true; +} + +/* chown override ids: IDENTITY_MATCH_ANY (-1) is the lowest legal value. */ +static bool config_receive_identity_id(int fd, int32_t* value) { + int v; + if (!receive_int(fd, &v) || v < IDENTITY_MATCH_ANY) + return false; + *value = v; + return true; +} + +static bool config_receive_skip_count(int fd, int* value) { + if (!receive_int(fd, value) || *value < 0 || *value > MAX_SKIP_COMPRESS_SUFFIXES) + return false; + return true; +} + +static bool config_receive_basis_count(int fd, int* value) { + if (!receive_int(fd, value) || *value < 0 || *value > MAX_BASIS_DIRS) + return false; + return true; +} + +static bool config_receive_idmap_count(int fd, int* value) { + if (!receive_int(fd, value) || *value < 0 || *value > MAX_IDENTITY_MAP) + return false; + return true; +} + +static bool config_receive_copy_as_presence(int fd, bool* value) { + int present; + if (!receive_int(fd, &present) || !valid_wire_bool(present)) + return false; + *value = present != 0; + return true; +} + +/* --copy-as ids are forced onto the ownership path, so a hostile peer must not + * smuggle a negative sentinel. */ +static bool config_receive_copy_as_id(int fd, int32_t* value) { + int v; + if (!receive_int(fd, &v) || v < 0) + return false; + *value = v; + return true; +} + +static bool send_skip_compress_suffixes(int fd, const Config* c) { + for (int i = 0; i < c->skip_compress_count; i++) { + if (!send_str(fd, c->skip_compress_suffixes[i])) + return false; + } + return true; +} + +static bool receive_skip_compress_suffixes(int fd, Config* c, ConfigStringBudget* budget) { + if (c->skip_compress_count <= 0) + return true; + c->skip_compress_suffixes = calloc((size_t)c->skip_compress_count, sizeof(char*)); + if (!c->skip_compress_suffixes) + return false; + for (int i = 0; i < c->skip_compress_count; i++) { + c->skip_compress_suffixes[i] = config_receive_str(fd, budget); + if (!c->skip_compress_suffixes[i]) + return false; + } + return true; +} + +static bool send_basis_entries(int fd, const Config* c) { + for (int i = 0; i < c->basis_count; i++) { + if (!send_int(fd, (int)c->basis_dirs[i].type) || + !send_str(fd, c->basis_dirs[i].path ? c->basis_dirs[i].path : "")) + return false; + } + return true; +} + +static bool receive_basis_entries(int fd, Config* c, ConfigStringBudget* budget) { + /* The count was read by the preceding INT_BASISCOUNT entry; config_basis_append + * rebuilds basis_count as it validates and canonicalizes each path. */ + int count = c->basis_count; + c->basis_count = 0; + for (int i = 0; i < count; i++) { + int type; + if (!receive_int(fd, &type) || type <= BASIS_DEST_NONE || type > BASIS_DEST_LINK) + return false; + char* path = config_receive_str(fd, budget); + if (!path) + return false; + bool ok = config_basis_append(c, (BasisDestType)type, path) == 0; + free(path); + if (!ok) + return false; + } + return true; +} + +static bool send_identity_entries(int fd, const IdentityMap* map, int count) { + for (int i = 0; i < count; i++) { + if (!send_int(fd, map[i].from) || !send_int(fd, map[i].to)) + return false; + } + return true; +} + +static bool receive_identity_entries(int fd, ConfigStringBudget* budget, int count, + IdentityMap** out) { + (void)budget; + if (count <= 0) + return true; + IdentityMap* map = calloc((size_t)count, sizeof(IdentityMap)); + if (!map) + return false; + for (int i = 0; i < count; i++) { + if (!receive_int(fd, &map[i].from) || !receive_int(fd, &map[i].to)) { + free(map); + return false; + } + } + *out = map; + return true; +} + +/* --------------------------------------------------------------------------- + * KIND dispatch. A table entry X(member, ctype, def, KIND) expands to + * CONFIG_SEND_(member) in a sender and CONFIG_RECV_(member) in a + * receiver. Send macros are bool expressions; receive macros are bool + * expressions too (strings allocate through `budget`). + * ------------------------------------------------------------------------- */ +#define CONFIG_SEND_BOOL(name) send_int(fd, c->name) +#define CONFIG_RECV_BOOL(name) receive_wire_bool(fd, &c->name) + +#define CONFIG_SEND_INT(name) send_int(fd, c->name) +#define CONFIG_RECV_INT(name) receive_int(fd, &c->name) + +#define CONFIG_SEND_RAW(name) send_n_data(fd, &c->name, sizeof(c->name)) +#define CONFIG_RECV_RAW(name) receive_n_data(fd, &c->name, sizeof(c->name)) + +#define CONFIG_SEND_BOOL_8BIT(name) \ + (send_int(fd, c->name) && (protocol_set_8_bit_output(c->name), true)) +#define CONFIG_RECV_BOOL_8BIT(name) \ + (receive_wire_bool(fd, &c->name) && (protocol_set_8_bit_output(c->name), true)) + +#define CONFIG_SEND_RAW_MAXALLOC(name) send_n_data(fd, &c->name, sizeof(c->name)) +#define CONFIG_RECV_RAW_MAXALLOC(name) config_receive_max_alloc(fd, &c->name) + +/* --delta is sent as (use_delta && !whole_file); whole_file never crosses the + * wire, so the receiver observes the effective bit. */ +#define CONFIG_SEND_DERIVED_DELTA(name) send_int(fd, c->name && !c->whole_file) +#define CONFIG_RECV_DERIVED_DELTA(name) receive_wire_bool(fd, &c->name) + +#define CONFIG_SEND_STR(name) send_str(fd, c->name) +#define CONFIG_RECV_STR(name) ((c->name = config_receive_str(fd, budget)) != NULL) + +#define CONFIG_SEND_STR_OPT(name) send_str(fd, c->name ? c->name : "") +#define CONFIG_RECV_STR_OPT(name) config_receive_optional_str(fd, budget, &c->name) + +#define CONFIG_SEND_STR_KEEP(name) send_str(fd, c->name ? c->name : "") +#define CONFIG_RECV_STR_KEEP(name) ((c->name = config_receive_str(fd, budget)) != NULL) + +#define CONFIG_SEND_STR_MODULE(name) send_str(fd, c->name ? c->name : "") +#define CONFIG_RECV_STR_MODULE(name) config_receive_module(fd, c, budget) + +#define CONFIG_SEND_STR_REDACTED_AUTH(name) config_send_auth_user(fd, c) +#define CONFIG_RECV_STR_REDACTED_AUTH(name) config_receive_auth_user(fd, c, budget) + +#define CONFIG_SEND_INT_CHECKSUM_ALGO(name) send_int(fd, c->name) +#define CONFIG_RECV_INT_CHECKSUM_ALGO(name) config_receive_checksum_algo(fd, &c->name) + +#define CONFIG_SEND_SUPERMODE(name) send_int(fd, (int)c->name) +#define CONFIG_RECV_SUPERMODE(name) config_receive_super_mode(fd, &c->name) + +#define CONFIG_SEND_INT_IDENTITY(name) send_int(fd, c->name) +#define CONFIG_RECV_INT_IDENTITY(name) config_receive_identity_id(fd, &c->name) + +#define CONFIG_SEND_INT_SKIPCOUNT(name) send_int(fd, c->name) +#define CONFIG_RECV_INT_SKIPCOUNT(name) config_receive_skip_count(fd, &c->name) + +#define CONFIG_SEND_INT_BASISCOUNT(name) send_int(fd, c->name) +#define CONFIG_RECV_INT_BASISCOUNT(name) config_receive_basis_count(fd, &c->name) + +#define CONFIG_SEND_INT_IDMAPCOUNT(name) send_int(fd, c->name) +#define CONFIG_RECV_INT_IDMAPCOUNT(name) config_receive_idmap_count(fd, &c->name) + +/* use_xattrs is derived receiver-side from the xattr/acl preservation flags + * that crossed the wire in the file-options block. */ +#define CONFIG_SEND_BOOL_XATTR_DERIVE(name) send_int(fd, c->name) +#define CONFIG_RECV_BOOL_XATTR_DERIVE(name) \ + (receive_wire_bool(fd, &c->name) && \ + (c->use_xattrs = (c->preserve_acls || c->preserve_xattrs), true)) + +#define CONFIG_SEND_COPY_AS_PRESENCE(name) send_int(fd, c->name ? 1 : 0) +#define CONFIG_RECV_COPY_AS_PRESENCE(name) config_receive_copy_as_presence(fd, &c->name) + +/* The uid/gid follow the presence int only when --copy-as is set. */ +#define CONFIG_SEND_COPY_AS_ID(name) (!c->copy_as_set || send_int(fd, c->name)) +#define CONFIG_RECV_COPY_AS_ID(name) (!c->copy_as_set || config_receive_copy_as_id(fd, &c->name)) + +#define CONFIG_SEND_BLOCK_SKIP_SUFFIXES(name) send_skip_compress_suffixes(fd, c) +#define CONFIG_RECV_BLOCK_SKIP_SUFFIXES(name) receive_skip_compress_suffixes(fd, c, budget) + +#define CONFIG_SEND_BLOCK_BASIS(name) send_basis_entries(fd, c) +#define CONFIG_RECV_BLOCK_BASIS(name) receive_basis_entries(fd, c, budget) + +#define CONFIG_SEND_BLOCK_IDMAP(name) send_identity_entries(fd, c->name, c->name##_count) +#define CONFIG_RECV_BLOCK_IDMAP(name) \ + receive_identity_entries(fd, budget, c->name##_count, &c->name) + +/* One table entry, applied in sequence. XSEND/XRECV are statement macros so + * consecutive entries read as a plain sequence of assignments. */ +#define XSEND(name, ctype, def, kind) ok = ok && (CONFIG_SEND_##kind(name)); +#define XRECV(name, ctype, def, kind) ok = ok && (CONFIG_RECV_##kind(name)); + +#define CONFIG_DEFINE_SEND(fn, fields) \ + static bool fn(int fd, const Config* c) { \ + bool ok = true; \ + fields(XSEND) return ok; \ + } + +#define CONFIG_DEFINE_RECV(fn, fields) \ + static bool fn(int fd, Config* c, ConfigStringBudget* budget) { \ + (void)budget; \ + bool ok = true; \ + fields(XRECV) return ok; \ + } + +CONFIG_DEFINE_SEND(send_core_fields, CONFIG_WIRE_CORE_FIELDS) +CONFIG_DEFINE_SEND(send_delta_fields, CONFIG_WIRE_DELTA_FIELDS) +CONFIG_DEFINE_SEND(send_file_options, CONFIG_WIRE_FILE_OPTIONS_FIELDS) +CONFIG_DEFINE_SEND(send_selection_options, CONFIG_WIRE_SELECTION_FIELDS) +CONFIG_DEFINE_SEND(send_resume_options, CONFIG_WIRE_RESUME_FIELDS) +CONFIG_DEFINE_SEND(send_basis_options, CONFIG_WIRE_BASIS_FIELDS) +CONFIG_DEFINE_SEND(send_fuzzy_option, CONFIG_WIRE_FUZZY_FIELDS) +CONFIG_DEFINE_SEND(send_checksum_options, CONFIG_WIRE_CHECKSUM_FIELDS) +CONFIG_DEFINE_SEND(send_identity_options, CONFIG_WIRE_IDENTITY_FIELDS) +CONFIG_DEFINE_SEND(send_metadata_times_options, CONFIG_WIRE_METADATA_TIMES_FIELDS) +CONFIG_DEFINE_SEND(send_symlink_trust_options, CONFIG_WIRE_SYMLINK_TRUST_FIELDS) +CONFIG_DEFINE_SEND(send_phase4_xattr_options, CONFIG_WIRE_XATTR_FIELDS) +CONFIG_DEFINE_SEND(send_daemon_module, CONFIG_WIRE_MODULE_FIELDS) +CONFIG_DEFINE_SEND(send_daemon_auth, CONFIG_WIRE_DAEMON_AUTH_FIELDS) +CONFIG_DEFINE_SEND(send_iconv_spec, CONFIG_WIRE_ICONV_FIELDS) +CONFIG_DEFINE_SEND(send_privilege_options, CONFIG_WIRE_PRIVILEGE_FIELDS) +CONFIG_DEFINE_SEND(send_copy_as_options, CONFIG_WIRE_COPY_AS_FIELDS) + +CONFIG_DEFINE_RECV(receive_core_fields, CONFIG_WIRE_CORE_FIELDS) +CONFIG_DEFINE_RECV(receive_delta_fields, CONFIG_WIRE_DELTA_FIELDS) +CONFIG_DEFINE_RECV(receive_file_options, CONFIG_WIRE_FILE_OPTIONS_FIELDS) +CONFIG_DEFINE_RECV(receive_selection_options, CONFIG_WIRE_SELECTION_FIELDS) +CONFIG_DEFINE_RECV(receive_resume_options, CONFIG_WIRE_RESUME_FIELDS) +CONFIG_DEFINE_RECV(receive_basis_options, CONFIG_WIRE_BASIS_FIELDS) +CONFIG_DEFINE_RECV(receive_fuzzy_option, CONFIG_WIRE_FUZZY_FIELDS) +CONFIG_DEFINE_RECV(receive_checksum_options, CONFIG_WIRE_CHECKSUM_FIELDS) +CONFIG_DEFINE_RECV(receive_identity_options, CONFIG_WIRE_IDENTITY_FIELDS) +CONFIG_DEFINE_RECV(receive_metadata_times_options, CONFIG_WIRE_METADATA_TIMES_FIELDS) +CONFIG_DEFINE_RECV(receive_symlink_trust_options, CONFIG_WIRE_SYMLINK_TRUST_FIELDS) +CONFIG_DEFINE_RECV(receive_phase4_xattr_options, CONFIG_WIRE_XATTR_FIELDS) +CONFIG_DEFINE_RECV(receive_daemon_module, CONFIG_WIRE_MODULE_FIELDS) +CONFIG_DEFINE_RECV(receive_daemon_auth, CONFIG_WIRE_DAEMON_AUTH_FIELDS) +CONFIG_DEFINE_RECV(receive_iconv_spec, CONFIG_WIRE_ICONV_FIELDS) +CONFIG_DEFINE_RECV(receive_privilege_options, CONFIG_WIRE_PRIVILEGE_FIELDS) +CONFIG_DEFINE_RECV(receive_copy_as_options, CONFIG_WIRE_COPY_AS_FIELDS) + +#undef XSEND +#undef XRECV + /* Client half of the SCRAM challenge/response (A7 remediation). Called by * config_send after the config frame is written and the server answered * STATUS_AUTH_CHALLENGE. The plaintext password lives only in @@ -1349,96 +1182,35 @@ static bool client_auth_exchange(int fd, const Config* c) { return ok; } -/* --iconv CONVERT_SPEC (protocol 2.16.0). Trailing string on the config frame, - * sent after the Wave A/B daemon-auth block and before the ack, so the - * receiver knows the wire charset before the first file name arrives. The full - * spec travels (LOCAL,REMOTE) and each end derives its own LOCAL and the wire - * (REMOTE) charset symmetrically; an unset spec is serialized as "" and - * canonicalized back to NULL on receive. */ -static bool send_iconv_spec(int fd, const Config* c) { - return send_str(fd, c->iconv_spec ? c->iconv_spec : ""); -} +/* The --iconv, --super/--no-super and --copy-as segment functions are + * generated above from CONFIG_WIRE_ICONV_FIELDS, CONFIG_WIRE_PRIVILEGE_FIELDS + * and CONFIG_WIRE_COPY_AS_FIELDS. */ -static bool receive_iconv_spec(int fd, Config* c, ConfigStringBudget* budget) { - char* spec = config_receive_str(fd, budget); - if (!spec) - return false; - if (*spec == '\0') { - free(spec); - c->iconv_spec = NULL; - return true; - } - c->iconv_spec = spec; - return true; -} - -/* --super / --no-super privilege policy (P7 Wave E, protocol 2.18.0). One - * trailing int on the config frame, sent after the --iconv spec and before the - * STATUS_OK ack, so the receiver knows whether it may attempt super-user - * activities (ownership application, char/block device-node creation) that are - * already confined below the authorized receive root. The received value is - * validated to the SUPER_MODE_AUTO..SUPER_MODE_OFF range (also re-checked by - * validate_received_config). */ -static bool send_privilege_options(int fd, const Config* c) { - return send_int(fd, (int)c->super_mode); -} - -static bool receive_privilege_options(int fd, Config* c) { - int mode; - if (!receive_int(fd, &mode) || mode < SUPER_MODE_AUTO || mode > SUPER_MODE_OFF) - return false; - c->super_mode = (SuperMode)mode; - return true; -} - -/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Trailing block on the - * config frame, sent after the --super int and before the ack: a presence int, - * then (when set) the target uid and gid as int32. The receiver forces the - * ownership of every entry it writes to these ids through the confined - * fd-relative identity path and requires privilege; both ids are validated - * `>= 0` on receive so a hostile peer cannot smuggle a negative (sentinel) - * value into the ownership path. */ -static bool send_copy_as_options(int fd, const Config* c) { - if (!send_int(fd, c->copy_as_set ? 1 : 0)) - return false; - if (!c->copy_as_set) - return true; - return send_int(fd, c->copy_as_uid) && send_int(fd, c->copy_as_gid); -} - -static bool receive_copy_as_options(int fd, Config* c) { - int present; - if (!receive_int(fd, &present) || !valid_wire_bool(present)) - return false; - if (!present) { - c->copy_as_set = false; - return true; - } - int uid, gid; - if (!receive_int(fd, &uid) || !receive_int(fd, &gid) || uid < 0 || gid < 0) - return false; - c->copy_as_set = true; - c->copy_as_uid = uid; - c->copy_as_gid = gid; - return true; +bool config_send_wire_block(int file_descriptor, const Config* config) { + protocol_session_set_max_alloc(NULL, config->max_alloc); + /* The version is the frame header: the receiver validates it before parsing + * any other field (see config_receive_with_validate), so it is not part of + * the generated segment sequence. It is still declared once, in + * CONFIG_WIRE_HEADER_FIELDS. */ + return send_str(file_descriptor, config->version) && send_core_fields(file_descriptor, config) && + send_delta_fields(file_descriptor, config) && send_file_options(file_descriptor, config) && + send_selection_options(file_descriptor, config) && + send_resume_options(file_descriptor, config) && + send_basis_options(file_descriptor, config) && + send_fuzzy_option(file_descriptor, config) && + send_checksum_options(file_descriptor, config) && + send_identity_options(file_descriptor, config) && + send_metadata_times_options(file_descriptor, config) && + send_symlink_trust_options(file_descriptor, config) && + send_phase4_xattr_options(file_descriptor, config) && + send_daemon_module(file_descriptor, config) && send_daemon_auth(file_descriptor, config) && + send_iconv_spec(file_descriptor, config) && + send_privilege_options(file_descriptor, config) && + send_copy_as_options(file_descriptor, config); } bool config_send(int file_descriptor, const Config* config) { - protocol_session_set_max_alloc(NULL, config->max_alloc); - if (!send_core_fields(file_descriptor, config) || !send_delta_fields(file_descriptor, config) || - !send_file_options(file_descriptor, config) || - !send_selection_options(file_descriptor, config) || - !send_resume_options(file_descriptor, config) || - !send_basis_options(file_descriptor, config) || !send_fuzzy_option(file_descriptor, config) || - !send_checksum_options(file_descriptor, config) || - !send_identity_options(file_descriptor, config) || - !send_metadata_times_options(file_descriptor, config) || - !send_symlink_trust_options(file_descriptor, config) || - !send_phase4_xattr_options(file_descriptor, config) || - !send_daemon_module(file_descriptor, config) || !send_daemon_auth(file_descriptor, config) || - !send_iconv_spec(file_descriptor, config) || - !send_privilege_options(file_descriptor, config) || - !send_copy_as_options(file_descriptor, config)) + if (!config_send_wire_block(file_descriptor, config)) return false; Status status; if (!receive_status(file_descriptor, &status)) @@ -1477,22 +1249,22 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val goto error; } if (!receive_core_fields(file_descriptor, config, &budget) || - !receive_delta_fields(file_descriptor, config) || + !receive_delta_fields(file_descriptor, config, &budget) || !receive_file_options(file_descriptor, config, &budget) || - !receive_selection_options(file_descriptor, config) || + !receive_selection_options(file_descriptor, config, &budget) || !receive_resume_options(file_descriptor, config, &budget) || !receive_basis_options(file_descriptor, config, &budget) || - !receive_fuzzy_option(file_descriptor, config) || - !receive_checksum_options(file_descriptor, config) || - !receive_identity_options(file_descriptor, config) || - !receive_metadata_times_options(file_descriptor, config) || - !receive_symlink_trust_options(file_descriptor, config) || - !receive_phase4_xattr_options(file_descriptor, config) || + !receive_fuzzy_option(file_descriptor, config, &budget) || + !receive_checksum_options(file_descriptor, config, &budget) || + !receive_identity_options(file_descriptor, config, &budget) || + !receive_metadata_times_options(file_descriptor, config, &budget) || + !receive_symlink_trust_options(file_descriptor, config, &budget) || + !receive_phase4_xattr_options(file_descriptor, config, &budget) || !receive_daemon_module(file_descriptor, config, &budget) || !receive_daemon_auth(file_descriptor, config, &budget) || !receive_iconv_spec(file_descriptor, config, &budget) || - !receive_privilege_options(file_descriptor, config) || - !receive_copy_as_options(file_descriptor, config)) + !receive_privilege_options(file_descriptor, config, &budget) || + !receive_copy_as_options(file_descriptor, config, &budget)) goto error; if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 && strcmp(config->compress_choice, "none") != 0) { diff --git a/src/shared/config.h b/src/shared/config.h index 0aba9d6..f7c5998 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -75,87 +75,203 @@ typedef struct { * privilege_super_mode_permitted() in identity.h. */ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode; +/* =========================================================================== + * Config wire-field table (single source of truth for protocol 2.20.0). + * + * Every field below crosses the wire. The table is the ONLY place a + * serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare + * the struct member, config_set_defaults() expands it to assign the default, + * and config_send_wire_block()/config_receive_with_validate() expand the + * per-segment lists to emit/consume the frame in exactly this order. Do NOT + * reorder entries and do NOT change a field's segment/KIND without a + * PROTOCOL_VERSION bump: the resulting byte stream is pinned by + * test_config_wire_golden(). + * + * Entry layout: X(MEMBER, CTYPE, DEFAULT, KIND) + * MEMBER struct member name (public; never rename) + * CTYPE C type of the member + * DEFAULT default-value expression used by config_set_defaults() + * KIND wire codec, dispatched to CONFIG_SEND_/CONFIG_RECV_ + * in config.c (strings receive through a ConfigStringBudget). + * + * Fields with genuinely custom logic keep dedicated helpers but are still + * declared here exactly once: the protocol-version handshake (HEADER), the + * daemon SCRAM auth username (STR_REDACTED_AUTH), the daemon module name + * (STR_MODULE), repeated count+array blocks (BLOCK_*), --copy-as presence + * (COPY_AS_*), and the derived --delta / use_xattrs bits (DERIVED_DELTA, + * BOOL_XATTR_DERIVE). + * =========================================================================== */ +#define CONFIG_WIRE_HEADER_FIELDS(X) X(version, char*, str_dup(PROTOCOL_VERSION), STR) + +#define CONFIG_WIRE_CORE_FIELDS(X) \ + X(eight_bit_output, bool, false, BOOL_8BIT) \ + X(max_alloc, unsigned long long, DEFAULT_MAX_ALLOC, RAW_MAXALLOC) \ + X(send_directory, char*, NULL, STR) \ + X(receive_root_directory, char*, NULL, STR) \ + X(save_to_disk, bool, false, BOOL) \ + X(use_multithreading, bool, false, BOOL) \ + X(use_chunk_serialization, bool, false, BOOL) \ + X(use_compression, bool, false, BOOL) \ + X(use_metadata, bool, false, BOOL) \ + X(use_executability, bool, false, BOOL) \ + X(compression_level, int, 5, INT) \ + X(chunk_size, unsigned long long, DEFAULT_CHUNK_SIZE, RAW) \ + X(use_sendfile, bool, false, BOOL) + +#define CONFIG_WIRE_DELTA_FIELDS(X) \ + X(use_delete, bool, false, BOOL) \ + X(use_incremental, bool, false, BOOL) \ + X(size_only, bool, false, BOOL) \ + X(ignore_times, bool, false, BOOL) \ + X(use_delta, bool, false, DERIVED_DELTA) \ + X(delta_block_size, uint32_t, DELTA_BLOCK_SIZE_DEFAULT, RAW) \ + X(delta_max_file_size, unsigned long long, DELTA_MAX_FILE_SIZE, RAW) + +#define CONFIG_WIRE_FILE_OPTIONS_FIELDS(X) \ + X(backup, bool, false, BOOL) \ + X(backup_dir, char*, NULL, STR_OPT) \ + X(remove_source_files, bool, false, BOOL) \ + X(follow_symlinks, bool, false, BOOL) \ + X(copy_links, bool, false, BOOL) \ + X(safe_links, bool, false, BOOL) \ + X(copy_unsafe_links, bool, false, BOOL) \ + X(preserve_hard_links, bool, false, BOOL) \ + X(preserve_acls, bool, false, BOOL) \ + X(preserve_xattrs, bool, false, BOOL) \ + X(preserve_devices, bool, false, BOOL) \ + X(preserve_sparse, bool, false, BOOL) \ + X(preserve_specials, bool, false, BOOL) \ + X(copy_devices, bool, false, BOOL) \ + X(write_devices, bool, false, BOOL) + +#define CONFIG_WIRE_SELECTION_FIELDS(X) \ + X(ignore_existing, bool, false, BOOL) \ + X(existing, bool, false, BOOL) \ + X(update, bool, false, BOOL) \ + X(inplace, bool, false, BOOL) \ + X(delay_updates, bool, false, BOOL) \ + X(append, bool, false, BOOL) \ + X(use_fsync, bool, false, BOOL) \ + X(append_verify, bool, false, BOOL) \ + X(delete_excluded, bool, false, BOOL) \ + X(force_delete, bool, false, BOOL) \ + X(delete_missing_args, bool, false, BOOL) \ + X(delete_after, bool, false, BOOL) \ + X(preallocate, bool, false, BOOL) \ + X(max_delete, int, -1, RAW) \ + X(relative, bool, false, BOOL) \ + X(prune_empty_dirs, bool, false, BOOL) \ + X(mkpath, bool, false, BOOL) \ + X(delete_during, bool, false, BOOL) \ + X(delete_delay, bool, false, BOOL) + +#define CONFIG_WIRE_RESUME_FIELDS(X) \ + X(temp_dir, char*, NULL, STR_OPT) \ + X(partial, bool, false, BOOL) \ + X(partial_dir, char*, NULL, STR_OPT) \ + X(suffix, char*, NULL, STR_OPT) \ + X(delete_before, bool, false, BOOL) \ + X(checksum, bool, false, BOOL) \ + X(modify_window, int, 0, RAW) \ + X(compress_choice, char*, NULL, STR_KEEP) \ + X(chmod_spec, char*, NULL, STR_KEEP) \ + X(skip_compress_set, bool, false, BOOL) \ + X(skip_compress_count, int, 0, INT_SKIPCOUNT) \ + X(skip_compress_suffixes, char**, NULL, BLOCK_SKIP_SUFFIXES) + +#define CONFIG_WIRE_BASIS_FIELDS(X) \ + X(basis_count, int, 0, INT_BASISCOUNT) \ + X(basis_dirs, BasisDest*, NULL, BLOCK_BASIS) + +#define CONFIG_WIRE_FUZZY_FIELDS(X) X(fuzzy, bool, false, BOOL) + +#define CONFIG_WIRE_CHECKSUM_FIELDS(X) \ + X(checksum_algo, int, CHECKSUM_ALGO_XXH64, INT_CHECKSUM_ALGO) \ + X(checksum_seed, uint64_t, 0, RAW) + +#define CONFIG_WIRE_IDENTITY_FIELDS(X) \ + X(numeric_ids, bool, false, BOOL) \ + X(chown_uid_set, bool, false, BOOL) \ + X(chown_uid, int32_t, 0, INT_IDENTITY) \ + X(chown_gid_set, bool, false, BOOL) \ + X(chown_gid, int32_t, 0, INT_IDENTITY) \ + X(usermap_count, int, 0, INT_IDMAPCOUNT) \ + X(usermap, IdentityMap*, NULL, BLOCK_IDMAP) \ + X(groupmap_count, int, 0, INT_IDMAPCOUNT) \ + X(groupmap, IdentityMap*, NULL, BLOCK_IDMAP) + +#define CONFIG_WIRE_METADATA_TIMES_FIELDS(X) \ + X(preserve_atimes, bool, false, BOOL) \ + X(preserve_crtimes, bool, false, BOOL) \ + X(omit_dir_times, bool, false, BOOL) \ + X(omit_link_times, bool, false, BOOL) + +#define CONFIG_WIRE_SYMLINK_TRUST_FIELDS(X) \ + X(munge_links, bool, false, BOOL) \ + X(keep_dirlinks, bool, false, BOOL) + +#define CONFIG_WIRE_XATTR_FIELDS(X) X(fake_super, bool, false, BOOL_XATTR_DERIVE) + +#define CONFIG_WIRE_MODULE_FIELDS(X) X(module, char*, NULL, STR_MODULE) + +#define CONFIG_WIRE_DAEMON_AUTH_FIELDS(X) X(auth_user, char*, NULL, STR_REDACTED_AUTH) + +#define CONFIG_WIRE_ICONV_FIELDS(X) X(iconv_spec, char*, NULL, STR_OPT) + +#define CONFIG_WIRE_PRIVILEGE_FIELDS(X) X(super_mode, SuperMode, SUPER_MODE_AUTO, SUPERMODE) + +#define CONFIG_WIRE_COPY_AS_FIELDS(X) \ + X(copy_as_set, bool, false, COPY_AS_PRESENCE) \ + X(copy_as_uid, int32_t, 0, COPY_AS_ID) \ + X(copy_as_gid, int32_t, 0, COPY_AS_ID) + +/* All serialized fields, in exact wire order. Concatenating the per-segment + * lists here is what keeps the declaration order = the wire order. */ +#define CONFIG_WIRE_FIELDS(X) \ + CONFIG_WIRE_HEADER_FIELDS(X) \ + CONFIG_WIRE_CORE_FIELDS(X) \ + CONFIG_WIRE_DELTA_FIELDS(X) \ + CONFIG_WIRE_FILE_OPTIONS_FIELDS(X) \ + CONFIG_WIRE_SELECTION_FIELDS(X) \ + CONFIG_WIRE_RESUME_FIELDS(X) \ + CONFIG_WIRE_BASIS_FIELDS(X) \ + CONFIG_WIRE_FUZZY_FIELDS(X) \ + CONFIG_WIRE_CHECKSUM_FIELDS(X) \ + CONFIG_WIRE_IDENTITY_FIELDS(X) \ + CONFIG_WIRE_METADATA_TIMES_FIELDS(X) \ + CONFIG_WIRE_SYMLINK_TRUST_FIELDS(X) \ + CONFIG_WIRE_XATTR_FIELDS(X) \ + CONFIG_WIRE_MODULE_FIELDS(X) \ + CONFIG_WIRE_DAEMON_AUTH_FIELDS(X) \ + CONFIG_WIRE_ICONV_FIELDS(X) \ + CONFIG_WIRE_PRIVILEGE_FIELDS(X) \ + CONFIG_WIRE_COPY_AS_FIELDS(X) + typedef struct Config { - char* version; - char* send_directory; - char* receive_root_directory; - bool save_to_disk; - bool use_multithreading; /* -j/--threads=N: number of parallel scanner worker threads for the -m * pipeline. 0 (the default, also set by bare -j/--threads) means "use the * scanner's built-in default" (4). CLIENT-ONLY: it is a local scheduling * concern and is NEVER serialized into the wire config frame. */ int scanner_threads; - bool use_chunk_serialization; - bool use_compression; - bool use_sendfile; - bool use_metadata; - bool use_executability; bool metadata_explicitly_disabled; bool show_progress; bool dry_run; - bool remove_source_files; - bool use_delete; - int compression_level; int compression_threads; - unsigned long long chunk_size; int ssh_port; TransportType transport; char* ssh_destination; - /* Daemon module selection (Wave A, protocol 2.15.0). Client-composed from a - * host::module/path destination; NULL or "" means "no module" (the ordinary - * standalone-server path). Crosses the wire as a trailing config-frame - * string so the daemon can look the module up in its own config and confine - * the connection to the module's root (never a client-chosen root). */ - char* module; - /* Daemon password authentication (A7 remediation, protocol 2.19.0). - * Client-composed from a --password-file whose first meaningful line is - * `user:password`: the client sends ONLY the username in the config frame - * (auth_user); the literal password is kept in auth_password CLIENT-SIDE for - * the duration of the SCRAM challenge/response and is NEVER serialized. Both - * are NULL when the client has no credentials to present; a module WITHOUT - * `auth users` stays open and the server ignores any credentials that do - * arrive (the client sends them opportunistically and the server decides). */ - char* auth_user; char* auth_password; /* Client-only path of --password-file (never crosses the wire; it is read to * populate auth_user/auth_password before connecting). */ char* password_file; char* fastsync_server_path; - /* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the - * charset of FILE NAMES at the wire boundary. CONVERT_SPEC is - * "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is - * the remote side's charset and defaults to LOCAL. The sender converts - * every path LOCAL->REMOTE before transmitting it; the receiver converts - * every received path back REMOTE->LOCAL before creating/writing it. The - * FULL SPEC crosses the wire as a trailing config-frame string so each end - * derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL - * (or "") means no conversion: identity with zero overhead. See charset.c - * and the PROTOCOL_VERSION note below. */ - char* iconv_spec; char** exclude_patterns; int exclude_count; char** include_patterns; int include_count; unsigned long long max_size; unsigned long long min_size; - unsigned long long max_alloc; - bool use_incremental; - bool ignore_times; - bool size_only; - bool use_delta; bool whole_file; - /* -y/--fuzzy: when a file must be transferred and the destination holds no - * usable file at the exact path, the receiver may reuse a SIMILAR-named - * existing regular file in the same destination directory as the delta - * basis so the sender transmits only the differences. Crosses the wire - * (the receiver performs the candidate search); the CLI implies - * --incremental + --delta because the similar-basis only matters on the - * receiver-driven delta path. Off by default. */ - bool fuzzy; - int modify_window; - uint32_t delta_block_size; - unsigned long long delta_max_file_size; bool use_tls; char* server_host; int server_port; @@ -170,18 +286,10 @@ typedef struct Config { /* --contimeout: connect()/accept timeout, transport layer only. */ int contimeout; bool quiet; - bool backup; - char* backup_dir; bool stats; int max_depth; FILE* log_file; - bool follow_symlinks; - bool partial; - // Issue #120: Symlink handling - bool copy_links; - bool safe_links; - bool copy_unsafe_links; /* Phase 4 symlink-trust. -k/--copy-dirlinks and --munge-links are * CLIENT/sender-side only (they decide how the SENDER scans and rewrites * symlinks; the receiver never reads them), so they never cross the wire. @@ -189,31 +297,6 @@ typedef struct Config { * symlink-to-directory as a directory) and CROSSES the wire along with * --munge-links (so the receiver knows to unmunge). */ bool copy_dirlinks; /* client-only, sender-side (-k) */ - bool munge_links; /* crosses the wire */ - bool keep_dirlinks; /* crosses the wire (-K) */ - - // Issue #121: Extended metadata preservation - bool preserve_hard_links; - bool preserve_acls; - bool preserve_xattrs; - bool preserve_devices; - bool preserve_sparse; - /* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device - * nodes on the destination by recreating them (mknod/mkfifo) instead of - * transferring content. preserve_specials mirrors rsync --specials (the - * special-file half of -D); preserve_devices mirrors --devices (the device - * half of -D); both CROSS the wire so the receiver knows a special/device - * entry must be recreated rather than written as a regular file. */ - bool preserve_specials; - /* --copy-devices: copy the CONTENT of a source device as an ordinary regular - * file on the destination (rsync's non-privileged safe mode), instead of - * recreating the device node. CROSSES the wire (receiver treats the entry as - * a regular file, which is the default, so this is belt-and-braces). */ - bool copy_devices; - /* --write-devices: write the received data directly INTO an existing device - * node on the destination instead of creating a regular file. Dangeroud; - * see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */ - bool write_devices; // Issue #122: Output/logging options bool itemize_changes; @@ -223,57 +306,17 @@ typedef struct Config { int debug_level; bool list_only; bool human_readable; - bool eight_bit_output; - // Issue #127: Transfer modes - bool existing; - bool ignore_existing; - bool update; - bool inplace; - bool delay_updates; - bool use_fsync; - bool append; - bool append_verify; - /* --preallocate: allocates the destination file's full expected space up - * front (before any data is written) so a transfer that would overflow disk - * fails fast at allocation time and the file is laid out contiguously, - * avoiding fragmentation. Receiver-side, crosses the wire. */ - bool preallocate; - - // Issue #128: Extended delete options - /* --delete-excluded: also delete destination entries that were excluded on - * the source. Default (off) matches rsync: excluded paths are protected from - * deletion. Crosses the wire (the sender encodes the choice by whether it - * transmits a protected-prefix list with the keep-set manifest). */ - bool delete_excluded; - bool delete_after; - /* --max-delete=NUM: the receiver refuses to delete more than NUM entries per - * run (all-or-nothing: when the extras would exceed NUM nothing is removed and - * the transfer fails with a distinct error). -1 == no client limit (the - * server hard bound MAX_SERVER_DELETE_COUNT still applies). */ - int max_delete; /* --ignore-errors (client-only, never serialized): a sender-side source I/O * error (an unreadable directory during the scan) normally aborts the run so * no deletion happens; with --ignore-errors the scan continues and the * (partial) keep-set is still transmitted so the deletion runs. */ bool ignore_errors; - /* --force (receiver-side): a regular file may replace a destination - * directory by removing that (possibly non-empty, symlink-safe) directory - * tree first, instead of failing the write. Crosses the wire. */ - bool force_delete; /* --ignore-missing-args (client-only, never serialized): a --files-from * entry that does not exist under the source is silently skipped instead of * failing the run. Sender-side only: nothing is sent for it and it never * enters the keep-set. Implied by --delete-missing-args. */ bool ignore_missing_args; - /* --delete-missing-args: implies --ignore-missing-args; additionally each - * missing entry's destination mirror (computed like a present entry's wire - * path) is deleted receiver-side. Crosses the wire and is gated by the - * server's --allow-delete policy like --delete. rsync-parity: independent - * of ordinary --delete processing (it does not imply --delete); a non-empty - * directory mirror is only removed with --force or --delete in effect, and - * the missing-args deletions are not counted toward --max-delete. */ - bool delete_missing_args; // Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are // never serialized to the wire (the receiver must not learn them). @@ -283,23 +326,14 @@ typedef struct Config { bool from0; /* -0/--from0: NUL-delimited *-from files */ bool cvs_exclude; /* -C/--cvs-exclude: standard CVS ignore set */ bool per_dir_filter; /* -F: apply per-directory .rsync-filter files */ - bool prune_empty_dirs; bool one_file_system; /* -x/--one-file-system: do not cross filesystem boundaries */ - /* -R/--relative: crosses the wire; with --files-from listed entries keep - * their bare relative destination path (no source-root mirror prefix). */ - bool relative; /* --no-implied-dirs: client-only. With -R + --files-from, refuse to place a * listed file whose ancestor directory is not itself explicitly listed. */ bool no_implied_dirs; /* -d/--dirs: client-only. Transfer the directory entries named by the * source argument / --files-from list without recursing into contents. */ bool dirs; - /* --mkpath: crosses the wire. Tells the server to create the destination - * root directory (and missing leading components below its authorized root) - * at connection start instead of requiring it to already exist. */ - bool mkpath; - // Issue #130: Remote shell/connection options /* -e/--rsh: the remote-shell program used to establish the SSH transport. * NULL means the default "ssh". Client-only launch concern: NEVER crosses * the wire (it is not meaningful to the daemon/server handshake). */ @@ -312,7 +346,6 @@ typedef struct Config { * concern: NEVER crosses the wire. */ int outbuf; bool old_args; - char* temp_dir; /* --remote-option=OPT (Phase 5, long form only): one or more extra command-line * options to append to the REMOTE server invocation over SSH. CLIENT-ONLY: * they are composed into the remote command line by ssh_build_remote_command() @@ -321,34 +354,6 @@ typedef struct Config { * do NOT cross the wire and are never parsed on the receiver process. */ char** remote_options; int remote_option_count; - /* Alternate basis directories, ordered by command-line appearance. Each - * entry's type selects compare/copy/link behavior on an exact match. These - * cross the wire so the receiver can consult them; they are interpreted - * relative to the destination root and confined there. */ - BasisDest* basis_dirs; - int basis_count; - - // PR #174: Partial transfer resumption - char* partial_dir; - - // PR #178: Backup versioning - char* suffix; - - // PR #179: Delete policies - bool delete_before; - - /* rsync deletion-timing family (real from Phase 3). At most one of - delete_before / delete_during / delete_delay / delete_after may be set, and - only together with use_delete (the CLI implies --delete for each of them). - delete_before and delete_during select the EARLY engine mode: the keep-set - manifest is transmitted before any file data and extras are removed then, - acknowledged, before the first data byte. delete_delay and delete_after - select the LATE commit mode: extras are removed only after the whole - transfer has succeeded (plain --delete keeps this mode). The exact - semantics and the divergences from rsync are documented in RSYNC_COMPAT.md - and in config_delete_timing_early() below. */ - bool delete_during; - bool delete_delay; // PR #181: IPv6 and bind address char* address; @@ -370,119 +375,19 @@ typedef struct Config { * MOTD is shown when a daemon offers one). */ bool no_motd; - // PR #183: Checksum comparison - bool checksum; - - // PR #184: Compression algorithm negotiation - char* compress_choice; - char* chmod_spec; - - /* --checksum-choice / --cc and --checksum-seed. checksum_algo is the id of - * the whole-file content-digest algorithm used by the per-file --incremental - * handshake (sender computes it, receiver compares it to skip unchanged - * files) and by the basis-dir content verification. checksum_seed is passed - * to xxHash64 (and to the delta block strong hash, low 32 bits); md5 has no - * seed so it is ignored there. Both cross the wire: the receiver MUST hash - * the on-disk old file with the same algorithm and seed to reach a matching - * digest. Defaults (XXH64 / seed 0) reproduce the pre-existing behavior - * byte-for-byte. */ - int checksum_algo; /* ChecksumAlgo, default CHECKSUM_ALGO_XXH64 */ - uint64_t checksum_seed; /* default 0 */ - - char** skip_compress_suffixes; - int skip_compress_count; - bool skip_compress_set; - - // Issue #131: Identity mapping. These configure whether and how the receiver - // applies ownership when it is actually preserved/applied. ALL of them cross - // the wire (protocol 2.11.0) so the receiver resolves and applies ownership - // with the exact policy the client requested. Plain -M/--preserve still does - // NOT apply ownership (FastSync's deliberate conservative default); it is - // only attempted when at least one of these is set (see identity.h). - /* --numeric-ids: no name lookup, use the transmitted numeric ids raw. */ - bool numeric_ids; - /* --chown USER (owner) override; IDENTITY_CURRENT = the receiver's euid. */ - bool chown_uid_set; - int32_t chown_uid; - /* --chown :GROUP (group) override; IDENTITY_CURRENT = the receiver's egid. */ - bool chown_gid_set; - int32_t chown_gid; - /* --usermap / --groupmap entries, in order (first match wins). */ - IdentityMap* usermap; - int usermap_count; - IdentityMap* groupmap; - int groupmap_count; - - /* --super / --no-super (P7 Wave E, protocol 2.18.0): receiver-side privilege - * policy for super-user activities confined below the authorized receive - * root. SUPER_MODE_AUTO (default) preserves the pre-existing best-effort - * behavior: the confined super-user operation is ALWAYS attempted and an - * unprivileged attempt is refused by the kernel and skipped per entry. - * SUPER_MODE_ON (--super) explicitly REQUESTS those activities (char/block - * device-node creation, --write-devices); it does NOT imply --numeric-ids and - * never enables ownership application on its own. SUPER_MODE_OFF - * (--no-super) FORBIDS them even when running as root. FastSync NEVER - * elevates privileges (no setuid/seteuid/setgid) and never bypasses the - * fd-relative confinement (file_open_secure_parent, O_NOFOLLOW, root checks); - * --super only permits an attempt that is already confined. Crosses the wire - * as a trailing int so the receiver can enforce the policy. See - * privilege_super_permitted() and identity_ownership_requested() in - * identity.h. */ - SuperMode super_mode; - // Receiver-side runtime staging registry for --delay-updates. Never sent // over the wire and never set on the sender side. DelayUpdatesContext* delay_context; - // Phase 4: metadata time preservation. -U/--atimes and -N/--crtimes capture - // and transmit the source access / birth time (both sender and receiver - // effect, so they CROSS the wire). --omit-dir-times/-O and - // --omit-link-times/-J are receiver-side prefs (CROSS the wire). Their - // exact capture/transmit/apply semantics are documented in RSYNC_COMPAT.md. - /* -U/--atimes: preserve source access times on the destination. */ - bool preserve_atimes; - /* -N/--crtimes: capture+transmit source birth time; see RSYNC_COMPAT for the - * receiver not-applied divergence. */ - bool preserve_crtimes; - /* -O/--omit-dir-times: do not apply mtimes to directories. */ - bool omit_dir_times; - /* -J/--omit-link-times: do not apply times to symlinks. */ - bool omit_link_times; /* --open-noatime: CLIENT-ONLY (never crosses the wire). The sender opens * source files with O_NOATIME so reading for transfer does not bump the * source access time. */ bool open_noatime; - // Phase 4: xattr / ACL / fake-super preservation. - /* -X/--xattrs and -A/--acls toggle the sender's capture and the receiver's - * application of per-file extended attributes (xattrs). Both cross the wire: - * the sender only transmits the bounded, whitelisted attribute set it - * captures and the receiver re-validates namespaces/sizes before applying - * fd-relative. With neither set (the default) no xattr block is sent, so the - * wire is byte-identical to prior protocol versions for unaffected runs. */ /* true when preserve_xattrs || preserve_acls; the sender/receiver gate the * xattr wire block on this single flag. */ bool use_xattrs; - /* --fake-super: receiver-only. When set, each written file additionally gets - * a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime - * so a later privileged restore could re-apply them. Crosses the wire. */ - bool fake_super; - /* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset - * implementation, a documented divergence from rsync's real identity switch: - * the receiver does NOT change its process credentials (FastSync's receiver - * is multithreaded, so a setuid/seteuid drop would be unsafe). Instead the - * receiver FORCES the ownership of every entry it writes to copy_as_uid / - * copy_as_gid through the existing confined, fd-relative identity path - * (fchown/fchownat), which REQUIRES receiver privilege (root); an - * unprivileged receiver REFUSES the whole transfer up front at the config - * handshake (never a silent wrong-ownership result). All three fields CROSS - * the wire as a trailing config-frame block so the receiver learns the - * requested ids; see the PROTOCOL_VERSION note below. */ - bool copy_as_set; - int32_t copy_as_uid; - int32_t copy_as_gid; - // Phase 5: --trust-sender /* Long-form-only, receiver-local policy. rsync's --trust-sender tells the * receiving side to trust that the sender already produced a sane file list, * relaxing the receiver's own up-front re-validation of every incoming path. @@ -501,7 +406,6 @@ typedef struct Config { * default; only relaxes validation when explicitly requested. */ bool trust_sender; - // Phase 6: --stop-after / --stop-at /* Client-only sender-side transfer stop deadlines. --stop-after=MINS stops * the transfer after a number of elapsed minutes (checked against * CLOCK_MONOTONIC so clock changes do not skew it); --stop-at=TIME stops at @@ -513,7 +417,6 @@ typedef struct Config { time_t stop_at; /* --stop-at=... absolute wall-clock deadline */ bool stop_at_set; /* true when --stop-at was given */ - // Phase 6: --write-batch / --only-write-batch / --read-batch /* Client-only residual-batch paths. A residual batch is a self-contained * single-file record of the whole source tree (full file images using the * chunk codec), independent of any live server. --write-batch=FILE runs the @@ -525,6 +428,196 @@ typedef struct Config { char* write_batch; /* --write-batch=FILE path, or NULL */ char* only_write_batch; /* --only-write-batch=FILE path, or NULL */ char* read_batch; /* --read-batch=FILE path, or NULL */ + + /* =================================================================== + * Serialized wire fields. Their members, defaults and send/receive + * sequence are generated from the CONFIG_WIRE_*_FIELDS table above (the + * single source of truth); they are declared here in exact wire order. + * The per-field notes were moved here from their original positions and + * are listed in wire order. + * =================================================================== */ + /* copy_links */ + // Issue #120: Symlink handling + /* preserve_hard_links */ + // Issue #121: Extended metadata preservation + /* preserve_specials */ + /* Phase 4 special/devices: preserve special files (FIFOs, sockets) and device + * nodes on the destination by recreating them (mknod/mkfifo) instead of + * transferring content. preserve_specials mirrors rsync --specials (the + * special-file half of -D); preserve_devices mirrors --devices (the device + * half of -D); both CROSS the wire so the receiver knows a special/device + * entry must be recreated rather than written as a regular file. */ + /* copy_devices */ + /* --copy-devices: copy the CONTENT of a source device as an ordinary regular + * file on the destination (rsync's non-privileged safe mode), instead of + * recreating the device node. CROSSES the wire (receiver treats the entry as + * a regular file, which is the default, so this is belt-and-braces). */ + /* write_devices */ + /* --write-devices: write the received data directly INTO an existing device + * node on the destination instead of creating a regular file. Dangeroud; + * see RSYNC_COMPAT.md for the tight gating. CROSSES the wire. */ + /* existing */ + // Issue #127: Transfer modes + /* delete_excluded */ + /* --delete-excluded: also delete destination entries that were excluded on + * the source. Default (off) matches rsync: excluded paths are protected from + * deletion. Crosses the wire (the sender encodes the choice by whether it + * transmits a protected-prefix list with the keep-set manifest). */ + /* force_delete */ + /* --force (receiver-side): a regular file may replace a destination + * directory by removing that (possibly non-empty, symlink-safe) directory + * tree first, instead of failing the write. Crosses the wire. */ + /* delete_missing_args */ + /* --delete-missing-args: implies --ignore-missing-args; additionally each + * missing entry's destination mirror (computed like a present entry's wire + * path) is deleted receiver-side. Crosses the wire and is gated by the + * server's --allow-delete policy like --delete. rsync-parity: independent + * of ordinary --delete processing (it does not imply --delete); a non-empty + * directory mirror is only removed with --force or --delete in effect, and + * the missing-args deletions are not counted toward --max-delete. */ + /* preallocate */ + /* --preallocate: allocates the destination file's full expected space up + * front (before any data is written) so a transfer that would overflow disk + * fails fast at allocation time and the file is laid out contiguously, + * avoiding fragmentation. Receiver-side, crosses the wire. */ + /* max_delete */ + /* --max-delete=NUM: the receiver refuses to delete more than NUM entries per + * run (all-or-nothing: when the extras would exceed NUM nothing is removed and + * the transfer fails with a distinct error). -1 == no client limit (the + * server hard bound MAX_SERVER_DELETE_COUNT still applies). */ + /* relative */ + /* -R/--relative: crosses the wire; with --files-from listed entries keep + * their bare relative destination path (no source-root mirror prefix). */ + /* mkpath */ + /* --mkpath: crosses the wire. Tells the server to create the destination + * root directory (and missing leading components below its authorized root) + * at connection start instead of requiring it to already exist. */ + /* delete_during */ + /* rsync deletion-timing family (real from Phase 3). At most one of + delete_before / delete_during / delete_delay / delete_after may be set, and + only together with use_delete (the CLI implies --delete for each of them). + delete_before and delete_during select the EARLY engine mode: the keep-set + manifest is transmitted before any file data and extras are removed then, + acknowledged, before the first data byte. delete_delay and delete_after + select the LATE commit mode: extras are removed only after the whole + transfer has succeeded (plain --delete keeps this mode). The exact + semantics and the divergences from rsync are documented in RSYNC_COMPAT.md + and in config_delete_timing_early() below. */ + /* partial_dir */ + // PR #174: Partial transfer resumption + /* suffix */ + // PR #178: Backup versioning + /* delete_before */ + // PR #179: Delete policies + /* checksum */ + // PR #183: Checksum comparison + /* compress_choice */ + // PR #184: Compression algorithm negotiation + /* basis_dirs */ + /* Alternate basis directories, ordered by command-line appearance. Each + * entry's type selects compare/copy/link behavior on an exact match. These + * cross the wire so the receiver can consult them; they are interpreted + * relative to the destination root and confined there. */ + /* fuzzy */ + /* -y/--fuzzy: when a file must be transferred and the destination holds no + * usable file at the exact path, the receiver may reuse a SIMILAR-named + * existing regular file in the same destination directory as the delta + * basis so the sender transmits only the differences. Crosses the wire + * (the receiver performs the candidate search); the CLI implies + * --incremental + --delta because the similar-basis only matters on the + * receiver-driven delta path. Off by default. */ + /* checksum_algo / checksum_seed */ + /* --checksum-choice / --cc and --checksum-seed. checksum_algo is the id of + * the whole-file content-digest algorithm used by the per-file --incremental + * handshake (sender computes it, receiver compares it to skip unchanged + * files) and by the basis-dir content verification. checksum_seed is passed + * to xxHash64 (and to the delta block strong hash, low 32 bits); md5 has no + * seed so it is ignored there. Both cross the wire: the receiver MUST hash + * the on-disk old file with the same algorithm and seed to reach a matching + * digest. */ + /* munge_links / keep_dirlinks */ + /* Phase 4 symlink-trust: both cross the wire (the receiver unmunges symlink + * targets and, with -K, follows an in-root destination symlink-to-directory); + * -k/--copy-dirlinks is sender-only and is never serialized. */ + /* numeric_ids */ + /* --numeric-ids: no name lookup, use the transmitted numeric ids raw. */ + /* chown_uid_set */ + /* --chown USER (owner) override; IDENTITY_CURRENT = the receiver's euid. */ + /* chown_gid_set */ + /* --chown :GROUP (group) override; IDENTITY_CURRENT = the receiver's egid. */ + /* usermap */ + /* --usermap / --groupmap entries, in order (first match wins). */ + /* preserve_atimes */ + /* -U/--atimes: preserve source access times on the destination. */ + /* preserve_crtimes */ + /* -N/--crtimes: capture+transmit source birth time; see RSYNC_COMPAT for the + * receiver not-applied divergence. */ + /* omit_dir_times */ + /* -O/--omit-dir-times: do not apply mtimes to directories. */ + /* omit_link_times */ + /* -J/--omit-link-times: do not apply times to symlinks. */ + /* fake_super */ + /* --fake-super: receiver-only. When set, each written file additionally gets + * a reserved user.fastsync.stat xattr recording the source uid/gid/mode/mtime + * so a later privileged restore could re-apply them. Crosses the wire. */ + /* module */ + /* Daemon module selection (Wave A, protocol 2.15.0). Client-composed from a + * host::module/path destination; NULL or "" means "no module" (the ordinary + * standalone-server path). Crosses the wire as a trailing config-frame + * string so the daemon can look the module up in its own config and confine + * the connection to the module's root (never a client-chosen root). */ + /* auth_user */ + /* Daemon password authentication (A7 remediation, protocol 2.19.0). + * Client-composed from a --password-file whose first meaningful line is + * `user:password`: the client sends ONLY the username in the config frame + * (auth_user); the literal password is kept in auth_password CLIENT-SIDE for + * the duration of the SCRAM challenge/response and is NEVER serialized. Both + * are NULL when the client has no credentials to present; a module WITHOUT + * `auth users` stays open and the server ignores any credentials that do + * arrive (the client sends them opportunistically and the server decides). */ + /* iconv_spec */ + /* --iconv=CONVERT_SPEC (protocol 2.16.0, rsync compatibility): convert the + * charset of FILE NAMES at the wire boundary. CONVERT_SPEC is + * "LOCAL[,REMOTE]": LOCAL is the charset of our own file names, REMOTE is + * the remote side's charset and defaults to LOCAL. The sender converts + * every path LOCAL->REMOTE before transmitting it; the receiver converts + * every received path back REMOTE->LOCAL before creating/writing it. The + * FULL SPEC crosses the wire as a trailing config-frame string so each end + * derives its own LOCAL and the wire (REMOTE) charset symmetrically. NULL + * (or "") means no conversion: identity with zero overhead. See charset.c + * and the PROTOCOL_VERSION note below. */ + /* super_mode */ + /* --super / --no-super (P7 Wave E, protocol 2.18.0): receiver-side privilege + * policy for super-user activities confined below the authorized receive + * root. SUPER_MODE_AUTO (default) preserves the pre-existing best-effort + * behavior: the confined super-user operation is ALWAYS attempted and an + * unprivileged attempt is refused by the kernel and skipped per entry. + * SUPER_MODE_ON (--super) explicitly REQUESTS those activities (char/block + * device-node creation, --write-devices); it does NOT imply --numeric-ids and + * never enables ownership application on its own. SUPER_MODE_OFF + * (--no-super) FORBIDS them even when running as root. FastSync NEVER + * elevates privileges (no setuid/seteuid/setgid) and never bypasses the + * fd-relative confinement (file_open_secure_parent, O_NOFOLLOW, root checks); + * --super only permits an attempt that is already confined. Crosses the wire + * as a trailing int so the receiver can enforce the policy. See + * privilege_super_permitted() and identity_ownership_requested() in + * identity.h. */ + /* copy_as_set */ + /* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Safe-subset + * implementation, a documented divergence from rsync's real identity switch: + * the receiver does NOT change its process credentials (FastSync's receiver + * is multithreaded, so a setuid/seteuid drop would be unsafe). Instead the + * receiver FORCES the ownership of every entry it writes to copy_as_uid / + * copy_as_gid through the existing confined, fd-relative identity path + * (fchown/fchownat), which REQUIRES receiver privilege (root); an + * unprivileged receiver REFUSES the whole transfer up front at the config + * handshake (never a silent wrong-ownership result). All three fields CROSS + * the wire as a trailing config-frame block so the receiver learns the + * requested ids; see the PROTOCOL_VERSION note below. */ + +#define CONFIG_STRUCT_MEMBER(name, ctype, def, kind) ctype name; + CONFIG_WIRE_FIELDS(CONFIG_STRUCT_MEMBER) +#undef CONFIG_STRUCT_MEMBER } Config; /* Phase 5 (remote-option wave): 2.13.0 -> 2.14.0. @@ -699,6 +792,10 @@ void config_delete(Config* config); void config_burn_auth(Config* config); bool config_send(int file_descriptor, const Config* config); +/* Emit the config frame BODY (every serialized field, in wire order) without + * the trailing STATUS_OK handshake. config_send() is this plus the handshake; + * the wire-compatibility golden test uses it to hash the exact byte stream. */ +bool config_send_wire_block(int file_descriptor, const Config* config); Config* config_receive(int file_descriptor); bool config_is_remote_dest(const char* s); void config_parse_ssh_dest(Config* config);