feat(identity): implement --copy-as USER[:GROUP] safe subset (P7 Wave E)
Force the receiver to apply the requested owner/group to every written entry through the confined fd-relative identity path instead of switching the process credentials (unsafe for the multithreaded receiver). An unprivileged receiver refuses the transfer up front in server_module_gate, before STATUS_OK, so no data is written with the wrong ownership. - new Config fields copy_as_set/copy_as_uid/copy_as_gid + defaults - identity_parse_copy_as (name/@N/* resolution, primary-gid default, gid==uid fallback for numeric ids with no passwd entry); implies -M - identity snapshot + highest-priority forcing in identity_resolve_targets - identity_copy_as_refused() helper - trailing config-frame block (presence int + two int32 ids, >=0 checked) - PROTOCOL_VERSION 2.17.0 -> 2.18.0; version-sensitive tests updated - unit tests for parse + wire round-trip/negative-id rejection - integration TestCopyAs: unprivileged refusal + root chown assertion - RSYNC_COMPAT.md --copy-as row updated (safe subset + divergence); README protocol version refreshed
This commit is contained in:
+40
-2
@@ -177,6 +177,9 @@ static void config_set_defaults(Config* config) {
|
||||
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;
|
||||
@@ -241,6 +244,7 @@ static bool validate_received_config(const Config* config) {
|
||||
valid_wire_bool(config->omit_dir_times) && valid_wire_bool(config->omit_link_times) &&
|
||||
valid_wire_bool(config->munge_links) && valid_wire_bool(config->keep_dirlinks) &&
|
||||
valid_wire_bool(config->fake_super) &&
|
||||
(!config->copy_as_set || (config->copy_as_uid >= 0 && config->copy_as_gid >= 0)) &&
|
||||
(!config->use_compression ||
|
||||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
||||
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
||||
@@ -1185,6 +1189,38 @@ static bool receive_iconv_spec(int fd, Config* c) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* --copy-as=USER[:GROUP] (P7 Wave E, protocol 2.18.0). Trailing block on the
|
||||
* config frame, sent after the --iconv CONVERT_SPEC string 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(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) ||
|
||||
@@ -1198,7 +1234,7 @@ bool config_send(int file_descriptor, const Config* 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_iconv_spec(file_descriptor, config) || !send_copy_as_options(file_descriptor, config))
|
||||
return false;
|
||||
Status status;
|
||||
if (!receive_status(file_descriptor, &status))
|
||||
@@ -1240,7 +1276,9 @@ Config* config_receive_with_validate(int file_descriptor, ConfigValidateFunc val
|
||||
!receive_symlink_trust_options(file_descriptor, config) ||
|
||||
!receive_phase4_xattr_options(file_descriptor, config) ||
|
||||
!receive_daemon_module(file_descriptor, config) ||
|
||||
!receive_daemon_auth(file_descriptor, config) || !receive_iconv_spec(file_descriptor, config))
|
||||
!receive_daemon_auth(file_descriptor, config) ||
|
||||
!receive_iconv_spec(file_descriptor, config) ||
|
||||
!receive_copy_as_options(file_descriptor, config))
|
||||
goto error;
|
||||
if (config->compress_choice[0] != '\0' && strcmp(config->compress_choice, "zstd") != 0 &&
|
||||
strcmp(config->compress_choice, "none") != 0) {
|
||||
|
||||
Reference in New Issue
Block a user